11

私が書いたオープンソースプログラムでは、ファイルから(別のプログラムによって書かれた)バイナリデータを読み取り、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;
}

そして、必要に応じて次のようなバイト順序を交換するためのサポート関数のキャストがあります。

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);

ただし、一部のコンパイラは、「int64todouble.i」割り当てを最適化してプログラムを中断する可能性があります。このプログラムはパフォーマンスのために最適化されたままでなければならず、char *を直接doubleにキャストするための並列変換セットを記述したくないことを考慮しながら、これを行うためのより安全な方法はありますか?駄洒落の結合方法が安全である場合、それを使用するためにsnativeint64_tのような関数を書き直す必要がありますか?


変換関数がmemcpyを使用するように書き直されたため、SteveJessopの回答を使用することになりました。

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バージョンは、私がやろうとしていることをより明確に表現しており、最も単純なコンパイラーでも機能するはずです。

アダム、あなたの答えも素晴らしかったし、私はそれから多くを学びました。投稿してくれてありがとう!

4

5 に答える 5

12

Understanding Strict Aliasingを読むことを強くお勧めします。具体的には、「共用体によるキャスト」というラベルの付いたセクションを参照してください。非常に良い例がたくさんあります。この記事は Cell プロセッサに関する Web サイトに掲載されており、PPC アセンブリの例を使用していますが、そのほとんどすべては、x86 を含む他のアーキテクチャにも同様に適用できます。

于 2008-10-21T15:24:51.060 に答える
2

標準では、共用体の 1 つのフィールドへの書き込みとそのフィールドからの読み取りは未定義の動作であると規定されています。したがって、ルールブックに従うと、ユニオンベースの方法は機能しません。

通常、マクロは悪い考えですが、これはルールの例外かもしれません。入力と出力の型をパラメーターとして使用する一連のマクロを使用して、C でテンプレートのような動作を得ることができるはずです。

于 2008-10-21T15:28:40.413 に答える
2

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);

結果のコードを調べるか、単にプロファイリングします。最悪の場合でも遅くない可能性があります。

ただし、一般に、バイトスワッピングを巧妙に使いすぎると、移植性の問題が発生します。ミドル エンディアン double を使用する ABI が存在します。各単語はリトルエンディアンですが、ビッグ ワードが最初に来ます。

通常、sprintf と sscanf を使用して double を保存することを検討できますが、プロジェクトのファイル形式は制御できません。しかし、あなたのアプリケーションが、ある形式の入力ファイルから別の形式の出力ファイルにIEEE doubleをシャベルするだけの場合(問題のデータベース形式がわからないので、そうであるかどうかはわかりませんが、もしそうなら)、おそらくあなたはとにかく算術に使用していないので、それが double であるという事実を忘れることができます。ファイル形式が異なる場合にのみ、バイトスワッピングを必要とする不透明なchar [8]として扱います。

于 2008-10-21T16:29:08.573 に答える
0

非常に小さなサブ提案として、64ビットの場合にマスキングとシフトを入れ替えることができるかどうかを調査することをお勧めします。操作はバイトを交換しているので、マスクを。だけで常に回避できるはずです0xff。これは、コンパイラがそれ自体を理解するのに十分賢い場合を除いて、より速く、よりコンパクトなコードにつながるはずです。

簡単に言えば、これを変更します。

(((wrongend & 0xff00000000000000LL) >> 56)

これに:

((wrongend >> 56) & 0xff)

同じ結果が生成されるはずです。

于 2008-10-21T15:38:59.190 に答える
-2

編集:
質問者が別のプログラムが自分のデータ(重要な情報)を書き込むことについて言及していないため、データを常にビッグエンディアンに効果的に保存し、マシンエンディアンにスワップする方法に関するコメントを削除しました。

それでも、データを任意のエンディアンからビッグへ、およびビッグからホストエンディアンへの変換が必要な場合は、ntohs / ntohl / htons / htonlが最良の方法であり、最もエレガントで無敵の速度です(CPUがサポートしている場合、ハードウェアでタスクを実行するため、それを打ち負かすことはできません)。


ダブル/フロートに関しては、メモリキャスティングによって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でのプログラミングが間違って行うとバグを引き起こす可能性があるのと同じように、間違って行うとバグを引き起こす可能性があるだけです。どの言語のプログラミングでもそうです。その議論によって、あなたはプログラミングを完全にやめなければなりません。

型のパンニング
2つのポインターがメモリ内の同じ場所を参照しているが、その場所を異なるタイプとして表すポインターエイリアシングの形式。コンパイラは、両方の「駄洒落」を無関係のポインタとして扱います。型のパンニングは、両方のポインターを介してアクセスされるデータの依存関係の問題を引き起こす可能性があります。

これは本当ですが、残念ながら私のコードとはまったく関係がありません。

彼が言及しているのは、次のようなコードです。

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サイズの変数に。

于 2008-10-21T15:55:04.883 に答える