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

但是当我用双打尝试类似的东西时,我无法得到任何撕裂。有谁知道为什么?据我从规范中可以看出,只有对浮点数的赋值是原子的。分配给替身应该有撕裂的风险。

    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。虽然不能保证双打不会撕裂,但它们不会在许多当前的处理器上。如果您想在这种情况下撕裂,请尝试使用 AMD Sempron。

编辑:几年前很难学到这一点。

于 2012-01-25T18:59:16.270 回答
11
static double s_x;

当您使用双精度时,要演示效果要困难得多。CPU 使用专用指令来加载和存储双精度,分别为 FLD 和 FSTP。使用long会容易得多,因为没有一条指令可以在 32 位模式下加载/存储 64 位整数。要观察它,您需要使变量的地址未对齐,以便它跨越 cpu 缓存行边界。

您使用的声明永远不会发生这种情况,JIT 编译器确保双精度正确对齐,存储在 8 的倍数的地址中。您可以将其存储在类的字段中,GC 分配器仅与 4 对齐32 位模式。但那是一个废话拍摄。

最好的方法是通过使用指针故意错误对齐双精度。把unsafe放在 Program 类前面,让它看起来像这样:

    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 缓存行的开头对齐的位置。它取决于您的 cpu 核心中的缓存关联性(我的是 Core i5)。你必须修改偏移量,我通过实验得到了 28 的值。该值应该能被 4 整除,但不能被 8 整除,才能真正模拟 GC 堆行为。继续向该值添加 8,直到您获得双倍以跨越高速缓存行并触发断言。

为了减少人工,您必须编写一个程序,将双精度值存储在类的字段中,并让垃圾收集器在内存中移动它,使其不对齐。很难想出一个示例程序来确保这种情况发生。

还要注意您的程序如何演示一个称为错误共享的问题。注释掉线程 B 的 Start() 方法调用,并注意线程 A 运行的速度有多快。您将看到 cpu 的成本在 cpu 内核之间保持高速缓存线一致。由于线程访问相同的变量,因此此处旨在共享。当线程访问存储在同一缓存行中的不同变量时,就会发生真正的错误共享。这就是为什么对齐很重要的原因,当它的一部分在一个缓存行中而一部分在另一个缓存行中时,您只能观察到双精度的撕裂。

于 2012-01-29T15:08:17.353 回答
0

通过一些挖掘,我发现了一些关于 x86 架构上的浮点操作的有趣读物:

根据Wikipedia,x86 浮点单元将浮点值存储在 80 位寄存器中:

[...] 随后的 x86 处理器随后在芯片上集成了这种 x87 功能,这使得 x87 指令成为 x86 指令集事实上的组成部分。每个 x87 寄存器(称为 ST(0) 到 ST(7))都是 80 位宽,并以 IEEE 浮点标准双扩展精度格式存储数字。

这另一个 SO 问题也相关:Some floating point precision and numeric limits question

这可以解释为什么尽管双精度是 64 位的,但它们是原子操作的。

于 2012-01-29T09:19:21.637 回答
0

可以在此处找到此主题和代码示例的价值。

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

于 2012-01-29T09:47:43.590 回答