18

私は 32 ビット マシンで実行していますが、非常に迅速にヒットする次のコード スニペットを使用して、長い値が破損する可能性があることを確認できます。

        static void TestTearingLong()
        {
            System.Threading.Thread A = new System.Threading.Thread(ThreadA);
            A.Start();

            System.Threading.Thread B = new System.Threading.Thread(ThreadB);
            B.Start();
        }

        static ulong s_x;

        static void ThreadA()
        {
            int i = 0;
            while (true)
            {
                s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
                i++;
            }
        }

        static void ThreadB()
        {
            while (true)
            {
                ulong x = s_x;
                Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
            }
        }

しかし、ダブルスで似たようなことをしようとすると、引き裂くことができません。誰かが理由を知っていますか?仕様からわかる限り、フロートへの割り当てのみがアトミックです。double への割り当てには、ティアリングのリスクがあるはずです。

    static double s_x;

    static void TestTearingDouble()
    {
        System.Threading.Thread A = new System.Threading.Thread(ThreadA);
        A.Start();

        System.Threading.Thread B = new System.Threading.Thread(ThreadB);
        B.Start();
    }

    static void ThreadA()
    {
        long i = 0;

        while (true)
        {
            s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
            i++;

            if (i % 10000000 == 0)
            {
                Console.Out.WriteLine("i = " + i);
            }
        }
    }

    static void ThreadB()
    {
        while (true)
        {
            double x = s_x;

            System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue);
        }
    }
4

4 に答える 4

12

奇妙に聞こえるかもしれませんが、それは CPU に依存します。double がティアリングされないことが保証されているわけではありませんが、現在の多くのプロセッサではそうではありません。この状況でテアリングが必要な場合は、AMD Sempron を試してください。

編集:数年前に難しい方法でそれを学びました。

于 2012-01-25T18:59:16.270 に答える
11
static double s_x;

ダブルを使用すると、効果を発揮するのがはるかに難しくなります。CPU は専用の命令を使用して、倍精度の FLD と FSTP をそれぞれロードおよびストアします。32ビットモードで64ビット整数をロード/ストアする単一の命令がないため、 longを使用するとはるかに簡単になります。それを観察するには、変数のアドレスをずらして、CPU キャッシュ ラインの境界にまたがるようにする必要があります。

これは、使用した宣言では決して発生しません。JIT コンパイラは、double が適切に整列され、8 の倍数であるアドレスに格納されることを保証します。クラスのフィールドに格納できます。GC アロケータは 4 にのみ整列します32 ビット モード。しかし、それはがらくた撮影です。

これを行う最善の方法は、ポインターを使用して double の位置を意図的にずらすことです。Program クラスの前にunsafeを置き、次のようにします。

    static double* s_x;

    static void Main(string[] args) {
        var mem = Marshal.AllocCoTaskMem(100);
        s_x = (double*)((long)(mem) + 28);
        TestTearingDouble();
    }
ThreadA:
            *s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
ThreadB:
            double x = *s_x;

AllocCoTaskMem() が割り当てを cpu キャッシュ ラインの開始点に対して整列させる場所を正確に制御する方法がないため、これでも適切なミスアライメントは保証されません (hehe)。そして、それはあなたのCPUコアのキャッシュ連想性に依存します(私のものはCore i5です)。オフセットをいじる必要があります。私は実験で値 28 を得ました。値は 4 で割り切れる必要がありますが、GC ヒープの動作を真にシミュレートするには 8 で割り切れないようにする必要があります。double がキャッシュ ラインにまたがってアサートをトリガーするまで、値に 8 を追加し続けます。

人工的でないようにするには、クラスのフィールドに double を格納するプログラムを作成し、ガベージ コレクターがそれをメモリ内で移動して、位置合わせがずれないようにする必要があります。これが確実に起こるようにするサンプルプログラムを思いつくのはちょっと難しいです。

また、偽共有と呼ばれる問題をプログラムがどのように示すことができるかにも注意してください。スレッド B の Start() メソッド呼び出しをコメント アウトし、スレッド A の実行速度がどれほど速いかを確認します。CPU コア間でキャッシュ ラインの一貫性を維持するための CPU のコストが表示されます。スレッドは同じ変数にアクセスするため、ここでは共有が意図されています。スレッドが同じキャッシュ ラインに格納されているさまざまな変数にアクセスすると、真の偽共有が発生します。それ以外の場合、これがアライメントが重要な理由です。ダブルの一部が1つのキャッシュラインにあり、その一部が別のキャッシュラインにある場合にのみ、ダブルのティアリングを観察できます。

于 2012-01-29T15:08:17.353 に答える
0

このトピックとコード サンプルの価値については、こちらを参照してください。

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx

于 2012-01-29T09:47:43.590 に答える
0

掘り下げてみると、x86 アーキテクチャでの浮動小数点演算に関する興味深い読み物がいくつか見つかりました。

Wikipediaによると、x86 浮動小数点ユニットは浮動小数点値を 80 ビット レジスタに格納していました。

[...] 後続の x86 プロセッサは、この x87 機能をチップに統合し、x87 命令を x86 命令セットの事実上の不可欠な部分にしました。ST(0) ~ ST(7) と呼ばれる各 x87 レジスタは 80 ビット幅で、IEEE 浮動小数点標準の拡張倍精度形式で数値を格納します。

また、この他の SO の質問も関連しています:いくつかの浮動小数点の精度と数値制限の質問

これは、double が 64 ビットであるにも関わらず、アトミックに操作される理由を説明できます。

于 2012-01-29T09:19:21.637 に答える