为什么这段代码不能certificate读/写的非primefaces性?

阅读这个问题 ,我想测试一下我是否能够certificate对这种操作的primefaces性无法保证的类型的读写非primefaces性。

private static double _d; [STAThread] static void Main() { new Thread(KeepMutating).Start(); KeepReading(); } private static void KeepReading() { while (true) { double dCopy = _d; // In release: if (...) throw ... Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails } } private static void KeepMutating() { Random rand = new Random(); while (true) { _d = rand.Next(2) == 0 ? 0D : double.MaxValue; } } 

令我惊讶的是,即使在执行了整整三分钟之后,断言也拒绝失败。 是什么赋予了?

  1. 测试不正确。
  2. 测试的具体时序特性使得断言不太可能/不可能失败。
  3. 概率是如此之低,以至于我必须运行测试更长时间以使其可能触发。
  4. 与C#规范相比,CLR提供了更强的primefaces性保证。
  5. 我的OS /硬件提供比CLR更强的保证。
  6. 别的什么?

当然,我不打算依赖规范没有明确保证的任何行为,但我想更深入地了解这个问题。

仅供参考,我在Debug和Release上运行了这个if(..) throw在两个不同的环境Debug.Assert更改为if(..) throw )配置文件:

  1. Windows 7 64位+ .NET 3.5 SP1
  2. Windows XP 32位+ .NET 2.0

编辑:为了排除John Kugelman的评论“调试器不是Schrodinger安全”的可能性,我添加了一行someList.Add(dCopy);KeepReading方法并validation此列表没有从缓存中看到单个过时值。

编辑:基于丹·布莱恩特的建议:使用long而不是double打破它几乎立即。

您可以尝试通过CHESS运行它以查看它是否可以强制交错打破测试。

如果您查看x86 diassembly(从调试器中可见),您可能还会看到抖动是否正在生成保持primefaces性的指令。


编辑:我继续前进并运行反汇编(强制目标x86)。 相关的路线是:

  double dCopy = _d; 00000039 fld qword ptr ds:[00511650h] 0000003f fstp qword ptr [ebp-40h] _d = rand.Next(2) == 0 ? 0D : double.MaxValue; 00000054 mov ecx,dword ptr [ebp-3Ch] 00000057 mov edx,2 0000005c mov eax,dword ptr [ecx] 0000005e mov eax,dword ptr [eax+28h] 00000061 call dword ptr [eax+1Ch] 00000064 mov dword ptr [ebp-48h],eax 00000067 cmp dword ptr [ebp-48h],0 0000006b je 00000079 0000006d nop 0000006e fld qword ptr ds:[002423D8h] 00000074 fstp qword ptr [ebp-50h] 00000077 jmp 0000007E 00000079 fldz 0000007b fstp qword ptr [ebp-50h] 0000007e fld qword ptr [ebp-50h] 00000081 fstp qword ptr ds:[00159E78h] 

它使用单个fstp qword ptr在两种情况下执行写操作。 我的猜测是Intel CPU保证了这个操作的primefaces性,尽管我还没有找到任何支持这个的文档。 任何x86大师谁可以证实这一点?


更新:

如果使用Int64,它会按预期失败,后者使用x86 CPU上的32位寄存器而不是特殊的FPU寄存器。 你可以在下面看到:

  Int64 dCopy = _d; 00000042 mov eax,dword ptr ds:[001A9E78h] 00000047 mov edx,dword ptr ds:[001A9E7Ch] 0000004d mov dword ptr [ebp-40h],eax 00000050 mov dword ptr [ebp-3Ch],edx 

更新:

我很好奇如果我在内存中强制非双字段对齐8字节会失败,所以我把这段代码放在一起:

  [StructLayout(LayoutKind.Explicit)] private struct Test { [FieldOffset(0)] public double _d1; [FieldOffset(4)] public double _d2; } private static Test _test; [STAThread] static void Main() { new Thread(KeepMutating).Start(); KeepReading(); } private static void KeepReading() { while (true) { double dummy = _test._d1; double dCopy = _test._d2; // In release: if (...) throw ... Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails } } private static void KeepMutating() { Random rand = new Random(); while (true) { _test._d2 = rand.Next(2) == 0 ? 0D : double.MaxValue; } } 

它不会失败,生成的x86指令基本上与以前相同:

  double dummy = _test._d1; 0000003e mov eax,dword ptr ds:[03A75B20h] 00000043 fld qword ptr [eax+4] 00000046 fstp qword ptr [ebp-40h] double dCopy = _test._d2; 00000049 mov eax,dword ptr ds:[03A75B20h] 0000004e fld qword ptr [eax+8] 00000051 fstp qword ptr [ebp-48h] 

我尝试交换_d1和_d2以用于dCopy / set并且还尝试了FieldOffset 2.所有生成相同的基本指令(上面有不同的偏移)并且几秒后都没有失败(可能数十亿次尝试)。 鉴于这些结果,我谨慎地相信,至少英特尔x86 CPU提供双重加载/存储操作的primefaces性,无论是否对齐。

允许编译器优化_d的重复读取。 据他所知,只是静态分析你的循环, _d永远不会改变。 这意味着它可以缓存该值并且永远不会重新读取该字段。

要防止这种情况,您需要同步访问_d (即用lock语句包围它),或将_d标记为volatile 。 使其变为volatile会告诉编译器它的值可能随时发生变化,因此它永远不会缓存该值。

不幸的是(或幸运的是),你不能将double字段标记为volatile ,正是因为你试图测试的重点 – double字段无法primefaces访问! 同步对_d访问是强制编译器重新读取该值,但这也会破坏测试。 那好吧!

您可以尝试删除’dCopy = _d’并在断言中使用_d。

这样两个线程同时读/写同一个变量。

您当前的版本制作了_d的副本,它创建了一个新实例,所有这些都在同一个线程中,这是一个线程安全的操作:

http://msdn.microsoft.com/en-us/library/system.double.aspx

此类型的所有成员都是线程安全的。 似乎修改实例状态的成员实际上返回使用新值初始化的新实例。 与任何其他类型一样,必须通过锁保护对包含此类实例的共享变量的读写,以保证线程安全。

但是,如果两个线程都在读/写同一个变量实例,那么:

http://msdn.microsoft.com/en-us/library/system.double.aspx

分配此类型的实例在所有硬件平台上都不是线程安全的,因为该实例的二进制表示可能太大而无法在单个primefaces操作中分配。

因此,如果两个线程都在读取/写入同一个变量实例,则需要一个锁来保护它(或者Interlocked.Read/Increment/Exchange。,不确定它是否适用于双精度数)

编辑

正如其他人所指出的,在Intel CPU读/写上,double是primefaces操作。 但是,如果程序是为X86编译并使用64位整数数据类型,则操作不是primefaces操作。 如以下程序所示。 用双重替换Int64它似乎工作。

  Public Const ThreadCount As Integer = 2 Public thrdsWrite() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} Public thrdsRead() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} Public d As Int64  _ Sub Main() For i As Integer = 0 To thrdsWrite.Length - 1 thrdsWrite(i) = New Threading.Thread(AddressOf Write) thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA) thrdsWrite(i).IsBackground = True thrdsWrite(i).Start() thrdsRead(i) = New Threading.Thread(AddressOf Read) thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA) thrdsRead(i).IsBackground = True thrdsRead(i).Start() Next Console.ReadKey() End Sub Public Sub Write() Dim rnd As New Random(DateTime.Now.Millisecond) While True d = If(rnd.Next(2) = 0, 0, Int64.MaxValue) End While End Sub Public Sub Read() While True Dim dc As Int64 = d If (dc <> 0) And (dc <> Int64.MaxValue) Then Console.WriteLine(dc) End If End While End Sub 

IMO的正确答案是#5。

double是8个字节长。

存储器接口是64位=每个模块每个时钟8个字节(即,对于双通道存储器,它变为16个字节)。

还有CPU缓存。 在我的机器上,缓存行是64字节,在所有CPU上,它是8的倍数。

如上面的注释所述,即使CPU以32位模式运行,也只需加载1个指令即可加载和存储双变量。

这就是为什么只要您的双变量对齐(我怀疑公共语言运行时虚拟机为您做对齐),双读和写是primefaces的。