39

次のようなクラスがあるとします。

class MyClass
{
public:
  MyClass();
  int a,b,c;
  double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

クラスにポインタや仮想メソッドがない場合、次のものを使用しても安全ですか?

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

私が尋ねる理由は、ここで説明するように、ページングされたデータの非常に大きなコレクションを扱っているためです。ここでは、パフォーマンスが重要であり、memcpyは反復割り当てよりもパフォーマンスが大幅に向上します。'this'ポインターは保存されているものではなく暗黙のパラメーターであるため、問題ないはずですが、他に注意すべき隠れた厄介な点はありますか?

編集:

Sharptoothsのコメントによると、データにはハンドルや同様の参照情報は含まれていません。

Paul Rのコメントによると、私はコードのプロファイルを作成しました。この場合、コピーコンストラクターを回避する方が約4.5倍高速です。ここでの理由の一部は、テンプレート化された配列クラスが、与えられた単純な例よりもいくらか複雑であり、浅いコピーを許可しない型にメモリを割り当てるときに配置を「新規」と呼ぶことです。これは事実上、デフォルトのコンストラクターとコピーコンストラクターが呼び出されることを意味します。

2番目の編集

このようにmemcpyを使用することは悪い習慣であり、一般的な場合には避けるべきであることを私は完全に受け入れることを指摘する価値があるかもしれません。それが使用されている特定のケースは、コピーコンストラクタではなくmemcpyを呼び出すパラメータ'AllowShallowCopying'を含む高性能テンプレート配列クラスの一部としてです。これは、配列の先頭近くの要素を削除したり、セカンダリストレージにデータをページングしたり、セカンダリストレージからデータをページングしたりするなどの操作に大きなパフォーマンスの影響を及ぼします。より良い理論的解決策は、クラスを単純な構造に変換することですが、これには大きなコードベースの多くのリファクタリングが含まれるため、それを回避することは私がやりたいことではありません。

4

11 に答える 11

19

標準によると、プログラマーがクラスのコピー コンストラクターを提供しない場合、コンパイラーはデフォルトのメンバーごとの初期化を示すコンストラクターを合成します。(12.8.8) ただし、12.8.1 では、標準は次のようにも述べています。

クラス オブジェクトは、関数の引数の受け渡し (5.2.2) と関数値の戻り (6.6.3) を含む初期化 (12.1、8.5) と代入 (5.17) の 2 つの方法でコピーできます。概念的には、これら 2 つの操作は、コピー コンストラクター (12.1) とコピー代入演算子 (13.5.3) によって実装されます。

ここで有効な言葉は「概念的に」であり、Lippmanによれば、これはコンパイラの設計者に、「些細な」(12.8.6) 暗黙的に定義されたコピー コンストラクターでメンバーごとの初期化を実際に行うことへの「アウト」を与えるものです。

実際には、コンパイラは、メンバーごとの初期化を行っているかのように動作するこれらのクラスのコピー コンストラクターを合成する必要があります。しかし、クラスが「ビットごとのコピー セマンティクス」(Lippman、p. 43) を示す場合、コンパイラはコピー コンストラクターを合成する必要がなく (関数呼び出しが発生し、インライン化される可能性があります)、代わりにビットごとのコピーを実行します。この主張は明らかにARMで裏付けられていますが、私はまだ調べていません。

コンパイラを使用して何かが標準に準拠していることを検証することは常に悪い考えですが、コードをコンパイルして結果のアセンブリを表示すると、コンパイラが合成コピー コンストラクターでメンバーごとの初期化を行っていないことを確認しているように見えますが、memcpy代わりに次のことを行っています。

#include <cstdlib>

class MyClass
{
public:
    MyClass(){};
  int a,b,c;
  double x,y,z;
};

int main()
{
    MyClass c;
    MyClass d = c;

    return 0;
}

生成されるアセンブリは次のMyClass d = c;とおりです。

000000013F441048  lea         rdi,[d] 
000000013F44104D  lea         rsi,[c] 
000000013F441052  mov         ecx,28h 
000000013F441057  rep movs    byte ptr [rdi],byte ptr [rsi] 

... はどこに28hありますかsizeof(MyClass)

これは、デバッグ モードで MSVC9 の下でコンパイルされました。

編集:

この投稿の要点は次のとおりです。

1) ビットごとのコピーを実行するとメンバーごとのコピーと同じ副作用が発生する限り、標準では、単純な暗黙のコピー コンストラクターmemcpyがメンバーごとのコピーの代わりに a を実行することを許可します。

2) 一部のコンパイラmemcpyは、メンバごとのコピーを行う自明なコピー コンストラクタを合成する代わりに、実際に s を実行します。

于 2010-06-11T11:15:19.143 に答える
12

経験に基づいた回答をさせてください。私たちのリアルタイム アプリでは、これを常に行っており、問題なく動作しています。これは、Wintel および PowerPC の MSVC と、Linux および Mac の GCC の場合であり、コンストラクターを持つクラスであっても同様です。

これについて C++ 標準の章と節を引用することはできません。実験的な証拠にすぎません。

于 2010-06-11T10:34:59.127 に答える
9

あなたはできます。しかし、最初に自問してみてください。

コンパイラーが提供するコピーコンストラクターを使用して、メンバーごとのコピーを実行してみませんか?

最適化する必要のある特定のパフォーマンスの問題がありますか?

現在の実装にはすべてのPODタイプが含まれています。誰かがそれを変更するとどうなりますか?

于 2010-06-11T08:36:24.813 に答える
9

クラスにはコンストラクターがあるため、C 構造体であるという意味で POD ではありません。したがって、memcpy() でコピーするのは安全ではありません。POD データが必要な場合は、コンストラクターを削除します。制御された構築が不可欠な非 POD データが必要な場合は、memcpy() を使用しないでください。両方を使用することはできません。

于 2010-06-11T08:48:52.220 に答える
8

[...] ですが、他に知っておくべき隠れた問題はありますか?

はい: あなたのコードは、提案も文書化もされていない特定の仮定を行っています (具体的に文書化しない限り)。これはメンテナンスにとって悪夢です。

また、実装は基本的にハッキングであり (必要な場合は悪いことではありません)、現在のコンパイラが実装する方法に依存する可能性があります (これについては不明です)。

これは、今から 1 年 (または 5 年) 後にコンパイラ/ツールチェーンをアップグレードした場合 (または現在のコンパイラの最適化設定を変更しただけの場合)、誰もこのハックを覚えていないことを意味します (目に見えるようにするために多大な努力をしない限り)。あなたの手には未定義の動作があり、開発者は数年後に「誰がこれをしたか」を罵倒します.

決定が不健全であるということではなく、メンテナにとって予想外である (または予想外である) ということです。

これを最小限に抑えるために (予期せぬことでしょうか?)、クラスの現在の名前に基づいて、クラスを名前空間内の構造に移動し、構造内に内部関数をまったく配置しません。次に、メモリブロックを見て、それをメモリブロックとして扱っていることを明確にしています。

それ以外の:

class MyClass
{
public:
    MyClass();
    int a,b,c;
    double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

あなたが持っている必要があります:

namespace MyClass // obviously not a class, 
                  // name should be changed to something meaningfull
{
    struct Data
    {
        int a,b,c;
        double x,y,z;
    };

    static const size_t PageSize = 1000000; // use static const instead of #define


    void Copy(Data* a1, Data* a2, const size_t count)
    {
        memcpy( a1, a2, count * sizeof(Data) );
    }

    // any other operations that you'd have declared within 
    // MyClass should be put here
}

MyClass::Data Array1[MyClass::PageSize],Array2[MyClass::PageSize];
MyClass::Copy( Array1, Array2, MyClass::PageSize );

このようにして:

  • MyClass::Data はクラスではなく POD 構造であることを明確にしてください (バイナリは同じか、非常に近いものになります。私の記憶が正しければ同じです)。

  • 2年間でmemcpyの使用を集中化します(std::copyなどに変更する必要がある場合)。これを1つのポイントで行います。

  • POD 構造の実装の近くで memcpy の使用を維持します。

于 2010-06-11T13:00:23.497 に答える
6

memcpyPOD タイプの配列のコピーに使用できます。boost::is_podまた、 is true の静的アサートを追加することをお勧めします。あなたのクラスは現在PODタイプではありません。

算術型、列挙型、ポインター型、およびメンバー型へのポインターは POD です。

POD 型の cv 修飾バージョンは、それ自体が POD 型です。

POD の配列は、それ自体が POD です。すべての非静的データ メンバーが POD である構造体または共用体は、次の場合、それ自体が POD です。

  • ユーザー宣言のコンストラクターはありません。
  • プライベートまたは保護された非静的データ メンバーはありません。
  • 基本クラスはありません。
  • 仮想機能はありません。
  • 参照型の非静的データ メンバーはありません。
  • ユーザー定義のコピー代入演算子はありません。
  • ユーザー定義のデストラクタはありません。
于 2010-06-11T08:42:59.147 に答える
3

ここに問題があることを認めていることに気付くでしょう。そして、あなたは潜在的な欠点を認識しています。

私の質問は、メンテナンスの 1 つです。あなたの素晴らしい最適化を台無しにするようなフィールドをこのクラスに含める人は誰もいないと確信していますか? 私は預言者ではなくエンジニアです。

したがって、コピー操作を改善しようとする代わりに....それを完全に回避しようとしないのはなぜですか?

ストレージに使用されるデータ構造を変更して、要素の移動を停止することは可能でしょうか...または少なくともそれほどではありません。

たとえば、blist(Python モジュール) を知っていますか。B+Tree は、たとえば、挿入/削除時にシャッフルする要素の数を最小限に抑えながら、ベクトルに非常に似たパフォーマンス (確かに少し遅い) でインデックス アクセスを許可できます。

手っ取り早いやり方ではなく、より良いコレクションを見つけることに集中するべきではないでしょうか?

于 2010-06-11T13:42:34.463 に答える
1

あなたが言及しているケースについて話すとき、私はあなたがクラスの代わりに構造体を宣言することを提案します。これにより、読みやすくなり(そして議論の余地が少なくなります:))、デフォルトのアクセス指定子はパブリックです。

もちろん、この場合はmemcpyを使用できますが、構造体に他の種類の要素(C ++クラスなど)を追加することはお勧めしません(明らかな理由により、memcpyがそれらにどのように影響するかはわかりません)。

于 2010-06-11T09:17:29.017 に答える
1

POD 以外のクラスで memcpy を呼び出すと、未定義の動作になります。アサーションについては、キリルのヒントに従うことをお勧めします。memcpy を使用すると高速になる可能性がありますが、コードでコピー操作のパフォーマンスが重要でない場合は、ビットごとのコピーを使用してください。

于 2010-06-11T08:52:47.770 に答える
1

John Dibling が指摘したように、memcpy手動で使用しないでください。代わりに、を使用してstd::copyください。クラスが memcpy 対応の場合、std::copy自動的にmemcpy. 手動の memcpy よりもさらに高速になる場合があります

を使用するstd::copyと、コードが読み取り可能になり、常に最速のコピー方法が使用されます。また、後でクラスのレイアウトを変更して memcpy を使用できないようにしても、 memcpy を手動で呼び出しても、 を使用するコードstd::copyは壊れません。

では、クラスが memcpy 対応かどうかをどのように判断しますか? 同様に、std::copyそれを検出します。使用します: std::is_trivially_copyable. を使用しstatic_assertて、このプロパティが維持されていることを確認できます。

std::is_trivially_copyableタイプ情報のみを確認できることに注意してください。セマンティクスを理解していません。次のクラスは自明にコピー可能なtypeですが、ビットごとのコピーはバグになります。

#include <type_traits>

struct A {
  int* p = new int[32];
};

static_assert(std::is_trivially_copyable<A>::value, "");

ビットごとのコピーの後、ptrコピーの は引き続き元のメモリを指します。3 つのルールも参照してください。

于 2019-11-27T16:24:54.373 に答える
0

(POD-)クラスはC ++の構造体(完全ではなく、デフォルトアクセス...)と同じであるため、機能します。memcpy で POD 構造体をコピーすることもできます。

POD の定義は、仮想関数なし、コンストラクターなし、デコンストラクターなし、仮想継承なしなどでした。

于 2010-06-11T08:42:28.220 に答える