4

次の12のQ/Aを読み、GCCとMSVCを使用するx86アーキテクチャで長年にわたって以下で説明する手法を使用し、問題が発生しなかった後、何が正しいと思われるかについて非常に混乱していますが、また、C++を使用してバイナリデータをシリアル化してから逆シリアル化するための重要な「最も効率的な」方法です。

次の「間違った」コードがあるとします。

int main()
{
   std::ifstream strm("file.bin");

   char buffer[sizeof(int)] = {0};

   strm.read(buffer,sizeof(int));

   int i = 0;

   // Experts seem to think doing the following is bad and
   // could crash entirely when run on ARM processors:
   i = reinterpret_cast<int*>(buffer); 

   return 0;
}

私が理解しているように、再解釈キャストは、バッファ内のメモリを整数として処理でき、その後、問題のデータの特定の配置を必要とする/想定する整数互換の命令を自由に発行できることをコンパイラに示します-唯一のオーバーヘッドはCPUがアライメント指向の命令を実行しようとしているアドレスを検出すると、余分な読み取りとシフトが実際にはアライメントされません。

とはいえ、上記の回答は、C ++に関する限り、これがすべて未定義の動作であることを示しているようです。

キャストが発生するバッファ内の場所の配置が適合していないと仮定すると、この問題の唯一の解決策はバイトを1つずつコピーすることであるというのは本当ですか?おそらくもっと効率的なテクニックはありますか?

さらに、ポッドだけで構成された構造体(コンパイラ固有のプラグマを使用してパディングを削除)がchar *にキャストされ、その後ファイルまたはソケットに書き込まれ、後でバッファに読み戻される多くの状況を長年にわたって見てきました。バッファが元の構造体のポインタにキャストバックされ(マシン間の潜在的なエンディアンおよびfloat / double形式の問題を無視)、この種のコードも未定義の動作と見なされますか?

以下は、より複雑な例です。

int main()
{
   std::ifstream strm("file.bin");

   char buffer[1000] = {0};

   const std::size_t size = sizeof(int) + sizeof(short) + sizeof(float) + sizeof(double);

   const std::size_t weird_offset = 3;

   buffer += weird_offset;

   strm.read(buffer,size);

   int    i = 0;
   short  s = 0;
   float  f = 0.0f;
   double d = 0.0;

   // Experts seem to think doing the following is bad and
   // could crash entirely when run on ARM processors:
   i = reinterpret_cast<int*>(buffer); 
   buffer += sizeof(int);

   s = reinterpret_cast<short*>(buffer); 
   buffer += sizeof(short);

   f = reinterpret_cast<float*>(buffer); 
   buffer += sizeof(float);

   d = reinterpret_cast<double*>(buffer); 
   buffer += sizeof(double);

   return 0;
}
4

1 に答える 1

7

まず、char [sizeof(int)]の代わりにstd :: aligned_storage :: value> :: typeを使用して(または、C ++がない場合は)、アライメントの問題を正しく、移植可能に、効率的に解決できます。 11、同様のコンパイラ固有の機能がある場合があります)。

aligned_stored複雑なPODを扱っている場合でも、PODを出し入れしたり、構築したりalignment_ofできるバッファーを提供します。memcpy

より複雑なケースでは、コンパイル時の演算やテンプレートベースの静的スイッチなどを使用して、より複雑なコードを作成する必要がありますが、私が知る限り、C++11の審議中に誰もケースを思いつきませんでした。これは、新機能では処理できませんでした。

ただし、reinterpret_castランダムな文字整列バッファで使用するだけでは十分ではありません。理由を見てみましょう:

再解釈キャストは、バッファー内のメモリーを整数として処理できることをコンパイラーに示します。

はい。ただし、バッファが整数に対して適切に整列されていると想定できることも示しています。あなたがそれについて嘘をついているなら、壊れたコードを自由に生成することができます。

その後、問題のデータの特定の配置を必要とする/想定する整数互換の命令を自由に発行できます

はい、それらの調整を必要とする、またはそれらがすでに処理されていることを前提とする指示を発行するのは無料です。

唯一のオーバーヘッドは、CPUがアラインメント指向の命令を実行しようとしているアドレスを検出したときの余分な読み取りとシフトであり、実際にはアラインメントされていません。

はい、追加の読み取りとシフトを伴う命令を発行する場合があります。しかし、そうする必要はないとあなたが言ったので、それはそれらをしない指示を出すかもしれません。したがって、アラインされていないアドレスで使用されると割り込みを発生させる「アラインされたワードの読み取り」命令を発行する可能性があります。

一部のプロセッサには「整列された単語の読み取り」命令がなく、整列された場合の方がそうでない場合よりも「単語の読み取り」が速くなります。他のものは、トラップを抑制し、代わりに遅い「単語の読み取り」にフォールバックするように構成できます。しかし、ARMのような他のものは失敗するでしょう。

キャストが発生するバッファ内の場所の配置が適合していないと仮定すると、この問題の唯一の解決策はバイトを1つずつコピーすることであるというのは本当ですか?おそらくもっと効率的なテクニックはありますか?

バイトを1つずつコピーする必要はありません。たとえば、memcpy各変数を1つずつ適切に配置されたストレージにコピーできます。(これは、すべての変数が1バイト長の場合にのみ、バイトを1つずつコピーすることになります。この場合、そもそもアライメントについて心配する必要はありません…)

PODをchar*にキャストし、コンパイラ固有のプラグマを使用して戻すことに関しては…まあ、コンパイラ固有のプラグマに(たとえば効率ではなく)正確さを依存するコードは、明らかに正しくなく、移植性のあるC++です。ユースケースには、「IEEE64ビットdoubleを使用する64ビットリトルエンディアンプラットフォームでg++ 3.4以降で修正する」だけで十分な場合もありますが、実際に有効なC++と同じではありません。そして、たとえば、80ビットのダブルを備えた32ビットのビッグエンディアンプラットフォームでSun ccが動作することを期待することはできませんが、動作しないと文句を言います。

後で追加した例の場合:

// Experts seem to think doing the following is bad and
// could crash entirely when run on ARM processors:
buffer += weird_offset;

i = reinterpret_cast<int*>(buffer); 
buffer += sizeof(int);

専門家は正しいです。同じことの簡単な例を次に示します。

int i[2];
char *c = reinterpret_cast<char *>(i) + 1;
int *j = reinterpret_cast<int *>(c);
int k = *j;

変数iは、4で割り切れるアドレス、たとえば0x01000000に配置されます。したがって、j0x01000001になります。したがって、この行int k = *jは、0x01000001から4バイトに整列された4バイトの値を読み取るための命令を発行します。たとえば、PPC64の場合、これには約8倍の時間がかかりますint k = *iが、たとえばARMの場合、クラッシュします。

だから、あなたがこれを持っているなら:

int    i = 0;
short  s = 0;
float  f = 0.0f;
double d = 0.0;

そして、あなたはそれをストリームに書きたいのですが、どうやってそれをしますか?

writeToStream(&i);
writeToStream(&s);
writeToStream(&f);
writeToStream(&d);

ストリームからどのように読み返しますか?

readFromStream(&i);
readFromStream(&s);
readFromStream(&f);
readFromStream(&d);

ifstreamおそらく、使用しているストリームの種類(、、FILE*など)にバッファが含まれているので、使用可能なバイトreadFromStream(&f)があるかどうかを確認しsizeof(float)、使用可能なバイトがない場合は次のバッファを読み取り、最初のsizeof(float)バイトをバッファからアドレスにコピーしますのf。(実際、それはさらに賢いかもしれません。たとえば、バッファの終わりに近づいているかどうかを確認し、その場合、ライブラリの実装者がそれが良い考えだと考えた場合は、非同期の先読みを発行することができます。 。)標準では、コピーをどのように実行する必要があるかについては規定されていません。標準ライブラリはどこでも実行する必要はありませんが、それらが含まれている実装では、プラットフォームで、またはifstreamを使用できます。memcpy*(float*)、またはコンパイラ組み込み関数、またはインラインアセンブリ—そしておそらくプラットフォーム上で最速のものを使用します。

では、アラインされていないアクセスは、これを最適化または簡素化するのにどの程度役立ちますか?

ほとんどすべての場合、適切な種類のストリームを選択し、その読み取りおよび書き込みメソッドを使用することが、読み取りおよび書き込みの最も効率的な方法です。また、標準ライブラリからストリームを選択した場合は、それも正しいことが保証されています。だから、あなたは両方の世界の長所を持っています。

アプリケーションに特有の何かが何かをより効率的にする場合、またはあなたが標準ライブラリを書いている人である場合は、もちろん先に進んでそれを行う必要があります。あなた(およびあなたのコードの潜在的なユーザー)があなたが標準に違反している場所とその理由を知っている限り(そしてあなたは「もっと速いはずだ」という理由で何かをするのではなく、実際に物事を最適化している)、これは完全に合理的です。

それらをある種の「パック構造体」に入れてそれを書くことができれば役立つと思われるかもしれませんが、C++標準には「パック構造体」のようなものはありません。一部の実装には、そのために使用できる非標準の機能があります。たとえば、MSVCとgccはどちらも、上記をi386で18バイトにパックすることができ、パックされた構造体とmemcpyそれをreinterpret_cast使用しchar *て、ネットワーク経由で送信することができます。ただし、コンパイラの特別なプラグマを理解しない別のコンパイラによってコンパイルされたまったく同じコードとは互換性がありません。同じものを20バイトにパックするARM用のgccのような関連コンパイラとも互換性がありません。標準に対して移植性のない拡張機能を使用すると、結果は移植性がありません。

于 2012-11-09T05:28:36.883 に答える