次のようなクラスがあるとします。
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));
私が尋ねる理由は、パフォーマンスが重要である here で説明されているように、ページングされたデータの非常に大きなコレクションを扱っているためです。 'this'ポインターは格納されているものではなく暗黙のパラメーターであるため、問題ないはずですが、他に隠しておく必要のある厄介な点はありますか?
編集:
シャープトゥースのコメントに従って、データにはハンドルや同様の参照情報は含まれていません。
Paul Rのコメントに従って、私はコードのプロファイルを作成しました。この場合、コピーコンストラクターを回避すると、約4.5倍速くなります。ここでの理由の一部は、テンプレート化された配列クラスが、与えられた単純な例よりもやや複雑であり、浅いコピーを許可しない型にメモリを割り当てるときに「新しい」配置を呼び出すためです。これは事実上、デフォルトのコンストラクターがコピーコンストラクターと同様に呼び出されることを意味します。
2番目の編集
このようにmemcpyを使用することは悪い習慣であり、一般的なケースでは回避する必要があることを十分に認めることを指摘する価値があるでしょう。それが使用されている特定のケースは、コピーコンストラクターではなくmemcpyを呼び出すパラメーター 'AllowShallowCopying'を含む、高性能のテンプレート化された配列クラスの一部としてです。これは、アレイの開始近くで要素を削除したり、セカンダリストレージにデータをページングしたり、データをページングしたりする操作など、パフォーマンスに大きな影響を与えます。より優れた理論的な解決策は、クラスを単純な構造に変換することですが、これには大きなコードベースの多くのリファクタリングが含まれ、それを回避することは私がやりたいことではありません。
標準によると、クラスのプログラマーによってコピーコンストラクターが提供されていない場合、コンパイラーはデフォルトのメンバーごとの初期化を示すコンストラクターを合成します。 (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 によれば、ここでの有効なWordは「概念的に」であり、コンパイラの設計者は、暗黙的に定義された「トリビアル」(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
を実行できます。
2)一部のコンパイラは、メンバーごとのコピーを行う簡単なコピーコンストラクタを合成する代わりに、実際にmemcpy
sを実行します。
経験的な答えを挙げましょう。私たちのリアルタイムアプリでは、これを常に実行しており、問題なく動作します。これは、WintelとPowerPCのMSVCおよびLinuxとMacのGCCの場合であり、コンストラクターを持つクラスの場合も同様です。
C++標準の章や節を引用することはできません。実験的な証拠です。
あなたできました。しかし、最初に自問してみてください。
コンパイラーが提供するコピーコンストラクターを使用して、メンバーごとのコピーを実行しないのはなぜですか。
最適化する必要がある特定のパフォーマンスの問題がありますか?
現在の実装にはすべてのPODタイプが含まれています。誰かが変更するとどうなりますか?
クラスにはコンストラクターがあるため、C構造体という意味ではPODではありません。したがって、memcpy()でコピーするのは安全ではありません。 PODデータが必要な場合は、コンストラクターを削除します。制御された構築が不可欠な非PODデータが必要な場合は、memcpy()を使用しないでください。両方を使用することはできません。
[...]しかし、他に知っておくべき隠された厄介なものはありますか?
はい:コードは、提案も文書化もされていない特定の仮定を行います(特に文書化しない限り)。これはメンテナンスのための悪夢です。
また、あなたの実装は基本的にハッキングであり(必要な場合、それは悪いことではありません)、現在のコンパイラーがどのように実装しているかに依存する可能性があります(これについては不明です)。
これは、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構造であることを明確にします(バイナリは同じか非常に近いです-私が正しく覚えていれば同じです)。これにより、コードを読んでいるプログラマーにも表示されます。
memcpy(std :: copyなどに変更する必要がある場合)の使用を2年間で一元化します。
pOD構造の実装の近くでmemcpyの使用を維持します。
memcpy
を使用して、PODタイプの配列をコピーできます。また、boost::is_pod
がtrueの場合、静的アサートを追加することをお勧めします。現在、クラスはPODタイプではありません。
算術型、列挙型、ポインター型、およびメンバー型へのポインターはPODです。
PODタイプのcv修飾バージョンは、それ自体がPODタイプです。
PODの配列自体がPODです。非静的データメンバーがすべてPODである構造体または共用体は、次のものがある場合、それ自体がPODです。
- ユーザー宣言のコンストラクタはありません。
- プライベートまたは保護された非静的データメンバーはありません。
- 基本クラスはありません。
- 仮想関数はありません。
- 参照型の非静的データメンバーはありません。
- ユーザー定義のコピー割り当て演算子はありません。
- ユーザー定義のデストラクタはありません。
ここに問題があることを認めます。そして、あなたは潜在的な欠点を認識しています。
メンテナンスの問題です。このクラスには、あなたの優れた最適化を妨げるフィールドが含まれることはないと確信していますか?私はそうではありません。私は預言者ではなくエンジニアです。
コピー操作を改善しようとする代わりに、それを完全に回避しようとしないのはなぜですか?
要素の移動を停止するために、ストレージに使用されるデータ構造を変更することはできますか...少なくともそれほどではありません。
たとえば、blist
(Pythonモジュール)を知っていますか。B+ Treeは、ベクトルと非常に似たパフォーマンス(確かに少し遅い)でインデックスアクセスを許可できます。たとえば、 、挿入/削除時にシャッフルする要素の数を最小限に抑えます。
迅速で汚いことをするのではなく、より良いコレクションを見つけることに集中すべきでしょうか?
あなたが言及しているケースについて話すとき、私はclass 'esではなくstruct' sを宣言することをお勧めします。読みやすくなり(議論の余地が少なくなります:))、デフォルトのアクセス指定子はパブリックです。
もちろん、この場合はmemcpyを使用できますが、(C++クラスなどの)構造体に他の種類の要素を追加することはお勧めできません(明らかな理由により、memcpyがどのようにそれらに影響するかわかりません)。
非PODクラスでmemcpyを呼び出すことは未定義の動作です。アリルについては、キリルのヒントに従うことをお勧めします。 memcpyを使用するとより高速になりますが、コピー操作がコードのパフォーマンスにとって重要でない場合は、ビット単位のコピーを使用してください。
(POD-)クラスはC++の構造体(完全ではなく、デフォルトのアクセスではない)と同じであるため、機能します。そして、あなたはmemcpyでPOD構造体をコピーするかもしれません。
PODの定義は、仮想関数、コンストラクター、デコンストラクター、仮想継承などではありませんでした。
John Diblingが指摘したように、memcpy
を手動で使用しないでください。代わりに、std::copy
を使用してください。クラスがmemcpyに対応している場合、std::copy
は自動的にmemcpy
を実行します。 手動のmemcpyよりも高速かもしれません 。
std::copy
を使用すると、コードは読み取り可能で、常に最速の方法でコピーできます。そして、後でmemcpyに対応しないようにクラスのレイアウトを変更しても、std::copy
を使用するコードは機能しますが、memcpyへの手動呼び出しは機能します。
では、クラスが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
は元のメモリを指します。 Rule of Three も参照してください。