【问题标题】:Simulate tearing a double in C#在 C# 中模拟撕裂双精度
【发布时间】:2012-02-18 23:25:23
【问题描述】:

我在 32 位机器上运行,我可以使用以下代码 sn-p 来确认长值可能会撕裂。

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

【问题讨论】:

  • 愚蠢的问题 - 什么是撕裂?
  • 在多线程访问方面保证对整数的操作是原子的。多头并非如此。撕裂是混合了两个中间值(坏)。他想知道为什么在双打中没有看到同样的情况,因为双打也不能保证原子操作。
  • @Oded:在 32 位机器上,一次只写入 32 位。如果您在 32 位机器上写入 64 位值,并在两个不同的线程上同时写入同一地址,您实际上有 四次 写入,而不是 两次,因为一次写入 32 位。因此线程有可能竞争,当烟雾消失时,变量包含一个线程写入的前 32 位和另一个线程写入的低 32 位。因此,您可以在一个线程上写入 0xDEADBEEF00000000,在另一个线程上写入 0x00000000BAADF00D,最终在内存中写入 0x0000000000000000。
  • @EricLippert - 所以,本质上是对 64 位值的操作在 32 位机器上不是原子的问题?

标签: c# .net multithreading atomic double-precision


【解决方案1】:

听起来很奇怪,这取决于您的 CPU。虽然 不保证双打不会撕裂,但它们不会在许多当前的处理器上。如果您想在这种情况下撕裂,请尝试 AMD Sempron。

编辑:几年前通过艰难的方式了解到这一点。

【讨论】:

  • TBH 我没有丝毫的想法,从未调查过。我的一个守护进程(所有语言的 Free Pascal)开始在一台机器(可能是 100 台)中的一台机器上虚假地产生荒谬的结果,所有机器都是从同一个图像等设置的。原来它是一个由主线程和 GTK 创建的辅助线程。然后在 FPK 中没有锁定原语......(咒骂,咒骂)
  • 是的,如果 CPU 上的 MMX 或 SSE 扩展与此有关,我不会怀疑。
  • 我正在测试的机器显示“Intel Xeon CPU E5620 @ 2.40 GHz(2 个处理器)”。知道在 Intel Xeons 上运行时我是否可以预期双打一般不会撕裂?
  • AFAIK doubles 不会撕裂所有更新的东西,包括英特尔“核心”架构,但请不要认为这是理所当然的 - 下一代可能会因为一些模糊的性能原因恢复到旧模型。跨度>
  • @MichaelCovelli - 听起来你真的想从这个应用程序中榨取一些性能。如果这真的那么重要,我建议您在程序中提供这两种实现;当它启动时,让它运行这个精确的测试来找出要打开的实现。如果测试成本很高,您可以尝试在安装软件时对其进行缓存,或者在每次机器启动时读取 CPUID,如果发生更改则重新运行测试。
【解决方案2】:
static double s_x;

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

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

最好的方法是通过使用指针故意错误对齐双精度。把 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 内核之间保持高速缓存线一致。由于线程访问相同的变量,因此此处旨在共享。当线程访问存储在同一缓存行中的不同变量时,就会发生真正的错误共享。这就是为什么对齐很重要的原因,只有当它的一部分在一个缓存行中而一部分在另一个缓存行中时,您才能观察到双精度的撕裂。

【讨论】:

  • 我不明白缓存行边界交叉如何导致撕裂。我认为这只是由于值占用的空间大于寄存器的大小。你能详细说明一下吗?
  • @Tudor - 这是一种完全不同的效果,与寄存器大小无关。重点看最后一段,注意cpu缓存同步如何以缓存行为更新单位。跨行的未对齐 double 需要 两次 更新,类似于 long 需要两次寄存器写入的方式。这需要足够的时间让运行在另一个内核上的代码观察到撕裂。
【解决方案3】:

通过一些挖掘,我发现了一些关于 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 位,但它们是原子操作的。

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 那篇文章只讲long,不讲double。
    • 同意。实际上,我认为我在问题中发布的示例代码来自该帖子(双重内容除外)。 (我在一个测试项目中有它,但有一段时间忘记了)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-10
    • 2012-12-09
    • 2011-04-04
    • 2012-06-15
    • 2011-04-09
    • 1970-01-01
    相关资源
    最近更新 更多