質問
オープンソースプログラムI 書き込み、ファイルからバイナリデータ(別のプログラムで書き込まれた)を読み取り、int、double、 およびその他の各種データ型。課題の1つは、それが必要なことです 両方のエンディアンの32ビットおよび64ビットのマシンで実行します。つまり、 結局、かなり低レベルのビット調整を行う必要があります。 (非常に)知っている 型の整理と厳密なエイリアシングについて少し説明します。 物事を正しい方法で行う。
基本的に、char *からさまざまなサイズのintへの変換は簡単です:
int64_t snativeint64_t(const char *buf)
{
/* Interpret the first 8 bytes of buf as a 64-bit int */
return *(int64_t *) buf;
}
と、必要に応じてバイトオーダーを交換するサポート関数のキャストがあります。 as:
int64_t swappedint64_t(const int64_t wrongend)
{
/* Change the endianness of a 64-bit integer */
return (((wrongend & 0xff00000000000000LL) >> 56) |
((wrongend & 0x00ff000000000000LL) >> 40) |
((wrongend & 0x0000ff0000000000LL) >> 24) |
((wrongend & 0x000000ff00000000LL) >> 8) |
((wrongend & 0x00000000ff000000LL) << 8) |
((wrongend & 0x0000000000ff0000LL) << 24) |
((wrongend & 0x000000000000ff00LL) << 40) |
((wrongend & 0x00000000000000ffLL) << 56));
}
実行時に、プログラムはマシンのエンディアンを検出し、割り当てます 関数ポインターへの上記のいずれか:
int64_t (*slittleint64_t)(const char *);
if(littleendian) {
slittleint64_t = snativeint64_t;
} else {
slittleint64_t = sswappedint64_t;
}
今、char *をdoubleにキャストしようとすると、難しい部分が生じます。したい エンディアン交換コードを次のように再利用したい:
union
{
double d;
int64_t i;
} int64todouble;
int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);
ただし、一部のコンパイラは&quot; int64todouble.i&quot;を最適化することができます。割り当て プログラムを中断します。考慮しながらこれを行うより安全な方法はありますか このプログラムはパフォーマンスのために最適化されたままである必要があり、また char *をキャストする変換の並列セットを書き込まないことを好む 直接ダブル?和解の組合方法が安全であれば、私は snativeint64_tのような関数を書き換えて使用しますか?
最終的に Steve Jessopの答えを使用することになりました。変換関数がmemcpyを使用するように書き直されたためです。
int64_t snativeint64_t(const char *buf)
{
/* Interpret the first 8 bytes of buf as a 64-bit int */
int64_t output;
memcpy(&output, buf, 8);
return output;
}
元のコードとまったく同じアセンブラーにコンパイル:
snativeint64_t:
movq (%rdi), %rax
ret
この2つのうち、memcpyバージョンは、私がやろうとしていることをより明確に表現しており、最も素朴なコンパイラーでも動作するはずです。
アダム、あなたの答えも素晴らしく、私はそこから多くを学びました。投稿していただきありがとうございます!
解決
int64_tとdoubleが同じサイズであり、適切なストレージ表現を持っていることを確認するために、実装について十分に知っているようであるため、memcpyを危険にさらす可能性があります。そうすれば、エイリアシングについて考える必要さえありません。
複数のバイナリをリリースしたい場合、簡単にインライン化できる関数に関数ポインターを使用しているため、パフォーマンスはとにかく大きな問題ではないはずですが、一部のコンパイラは非常に厄介なものになる可能性があることを知っておくとよいでしょうmemcpyの最適化-小さな整数サイズの場合、ロードとストアのセットをインライン化できます。また、変数が完全に最適化され、コンパイラーが「コピー」を行うことさえあります。単に共用体のように、変数に使用しているスタックスロットを再割り当てするだけです。
int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);
結果のコードを調べるか、プロファイルを作成します。最悪の場合でも、遅くなることはありません。
ただし、一般的に、バイトスワッピングで賢いことをすると、移植性の問題が発生します。ミドルエンディアンダブルのABIが存在します。各単語はリトルエンディアンですが、大きな単語が最初に来ます。
通常、sprintfとsscanfを使用してdoubleを保存することを検討できますが、プロジェクトのファイル形式は制御できません。しかし、アプリケーションが1つの形式の入力ファイルから別の形式の出力ファイルにIEEEをシャベルでシャベルしている場合(問題のデータベース形式がわからないので、そうであるかどうかはわかりません)、おそらくとにかく算術に使用していないので、それが二重であることを忘れることができます。不透明なchar [8]として扱うだけで、ファイル形式が異なる場合にのみバイトスワッピングが必要です。
他のヒント
厳密なエイリアシングについて。具体的には、「ユニオンのキャスト」というラベルのセクションをご覧ください。非常に良い例がたくさんあります。この記事はCellプロセッサーに関するWebサイトにあり、PPCアセンブリーの例を使用していますが、そのほとんどはx86を含む他のアーキテクチャーにも同様に適用できます。
規格では、ユニオンの1つのフィールドへの書き込みとそのフィールドからの読み取りは未定義の動作であるとされています。したがって、ルールブックを参照すると、ユニオンベースの方法は機能しません。
マクロは通常悪い考えですが、これはルールの例外かもしれません。入力および出力タイプをパラメーターとして使用する一連のマクロを使用して、Cでテンプレートのような動作を実現できるはずです。
非常に小さなサブ提案として、64ビットの場合、マスキングとシフトを交換できるかどうかを調査することをお勧めします。操作はバイトを交換するため、常に 0xff
のマスクで逃げることができるはずです。これにより、コンパイラーがそれ自体を把握できるほどスマートでない限り、より高速でコンパクトなコードになります。
簡単に言うと、これを変更します:
(((wrongend & 0xff00000000000000LL) >> 56)
これに:
((wrongend >> 56) & 0xff)
同じ結果が生成されます。
編集:
質問者が別のプログラムがデータを書き込むことを言及していないため、常にビッグエンディアンにデータを効果的に保存し、マシンのエンディアンに切り替える方法に関するコメントを削除しました(これは重要な情報です)。
データがエンディアンからの変換を必要とする場合ビッグからビッグエンディアンまで、ntohs / ntohl / htons / htonlが最良の方法であり、最もエレガントで無敵の速度です(CPUがサポートしている場合、ハードウェアでタスクを実行するため、それを打つことはできません)。
double / floatについては、メモリキャストによってintに保存するだけです:
double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);
関数にラップする
int64_t doubleToInt64(double d)
{
return *(int64_t *)&d;
}
double int64ToDouble(int64_t i)
{
return *(double *)&i;
}
質問者はこのリンクを提供しました:
http:// cocoawithlove .com / 2008/04 / using-pointers-to-recast-in-c-is-bad.html
キャスティングが悪いことの証明として...残念ながら、私はこのページのほとんどに強く反対することができるだけです。引用とコメント:
ポインターを介したキャストと同じ それは、実際には悪い習慣であり、 潜在的に危険なコード。鋳造 ポインタを介して 型の整理のためにバグを作成します。
それはまったく危険ではなく、悪い習慣でもありません。 Cでのプログラミングが間違っているとバグを引き起こす可能性があるのと同じように、誤って行うとバグを引き起こす可能性があるだけでなく、あらゆる言語のプログラミングも同様です。その議論により、プログラミングを完全に停止する必要があります。
タイプpunning
ポインタの形式 2つのポインターと参照のエイリアス メモリ内の同じ場所に その場所を異なるものとして表す タイプ。コンパイラは両方を扱います &quot; puns&quot;無関係なポインターとして。タイプ unningは引き起こす可能性があります データの依存関係の問題 両方のポインターからアクセスします。
これは事実ですが、残念ながら完全に私のコードとは無関係です。
彼が言及しているのは、次のようなコードです:
int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;
現在、doublePointerとintPointerは両方とも同じメモリ位置を指しますが、これを同じ型として扱います。これは実際に組合で解決すべき状況であり、それ以外はかなり悪いです。悪いことは私のコードが行うことではありません!
私のコードは、参照ではなく、値によってコピーします。 doubleをint64ポインター(またはその逆)にキャストし、それを即時に遵守します。関数が戻ると、何かへのポインタは保持されません。 int64とdoubleがあり、これらは関数の入力パラメーターとはまったく関係ありません。ポインタを別のタイプのポインタにコピーすることはありません(コードサンプルでこれを見た場合、書いたCコードを強く読み間違えています)、値を別のタイプの変数(独自のメモリ位置)に転送するだけです。したがって、「メモリ内の同じ場所を参照する」と書かれているように、タイプパンニングの定義はまったく適用されません。ここで同じメモリロケーションを参照するものはありません。
int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;
私のコードはメモリコピーにすぎず、外部関数なしでCで書かれているだけです。
int64_t doubleToInt64(double d)
{
return *(int64_t *)&d;
}
次のように記述できます
int64_t doubleToInt64(double d)
{
int64_t result;
memcpy(&result, &d, sizeof(d));
return result;
}
それはそれ以上のものではないので、どこにいても目に見える型はありません。また、この操作は完全に安全です。操作はCで行うことができます。doubleは常に64ビットであると定義されています(intとは異なり、サイズは変わりませんが、64ビットに固定されます)。 int64_tサイズの変数に。