问题 32位环境下64位变量的原子增量


写一个答案 另一个问题 一些有趣的事情出来了,现在我无法理解 Interlocked.Increment(ref long value) 适用于32位系统。让我解释。

本地人 InterlockedIncrement64 现在在编译32位环境时不可用,好吧,这是有道理的,因为在.NET中你不能根据需要对齐内存,它可能从 管理 然后他们放弃了。

在.NET中我们可以调用 Interlocked.Increment() 在引用64位变量时,我们对其对齐没有任何限制(例如在结构中,我们也可以使用 FieldOffset 和 StructLayout)但文档没有提到任何限制(AFAIK)。这很神奇,它有效!

汉斯帕斯特指出 Interlocked.Increment() 是一个 特别 JIT编译器识别的方法,它将发出一个调用 COMInterlocked :: ExchangeAdd64() 然后会打电话 FastInterlockExchangeAddLong 这是一个宏 InterlockedExchangeAdd64 分享相同 限制 的 InterlockedIncrement64

现在我很困惑。

忘记一秒钟的托管环境,然后回到本地。为什么 InterlockedIncrement64 不能工作但是 InterlockedExchangeAdd64 呢? InterlockedIncrement64 如果内在函数不可用,则是一个宏 InterlockedExchangeAdd64 然后它可以实现为调用 InterlockedExchangeAdd64...

让我们回到托管:如何在32位系统上实现原子64位增量?我猜是句 “这个函数是原子的 关于调用其他互锁功能 很重要,但我仍然没有看到任何代码(感谢Hans指出更深入的实现)来做到这一点。我们来挑选 InterlockedExchangedAdd64 当内在函数不可用时,从WinBase.h实现:

FORCEINLINE
LONGLONG
InterlockedExchangeAdd64(
    _Inout_ LONGLONG volatile *Addend,
    _In_    LONGLONG Value
    )
{
    LONGLONG Old;

    do {
        Old = *Addend;
    } while (InterlockedCompareExchange64(Addend,
                                          Old + Value,
                                          Old) != Old);

    return Old;
}

如何读/写原子?


6988
2018-06-01 07:21


起源

谁说的 ”InterlockedIncrement64 不能工作但是 InterlockedExchangeAdd64 “你的原始答案是正确的,说管理代码不能直接调用本机Win32 API并期望一切正常工作。它们都不会起作用。你必须使用托管助手。现在,托管助手的实现是本机代码,因此它调用本机函数。由于宏和内在函数在编译时被解析,因此重要的是CLR的位数。 - Cody Gray♦
是但是32位JIT将调用InterlockedExchangeAdd64,它与InterlockedIncrement64具有相同的限制(本机)。我不明白的是它是如何完成的(因为在调用托管代码时内存对齐)。 32位的实现使用InterlockedCompareExchange64 ......嗯......可能不是原子的(用于写回结果......) - Adriano Repetti
“阅读/写作怎么可能是原子的?” 的文档 InterlockedExchangeAdd64 提示原因,说 “此函数会生成一个完整的内存屏障(或栅栏),以确保按顺序完成内存操作。” 请注意,您在上面显示的实现调用 InterlockedCompareExchange64。在32位版本中,这会发出一个 CMPXCHG8B 教学指导 LOCK 字首。这确保了指令以原子方式执行。如果没有锁定写入,您永远不会获得锁定读取,因此写入目标是原子的。 - Cody Gray♦
@CodyGray吧! - Adriano Repetti


答案:


您必须继续跟踪,InterlockedExchangeAdd64()将您带到WinNt.h SDK头文件。在哪里可以看到它的许多版本,具体取决于目标架构。

这通常会崩溃为:

#define InterlockedExchangeAdd64 _InterlockedExchangeAdd64

它将降压传递给编译器内部函数,在vc / include / intrin.h中声明并由编译器的后端实现。

或者换句话说,CLR的不同构建将具有不同的实现。多年来已经有很多,x86,x64,Itanium,ARM,ARM8,PowerPC脱颖而出,我肯定错过了一些用于启动WindowsCE的苹果之前让它无关紧要。对于x86,这最终由LOCK CMPXCHNG8B处理,LOCK CMPXCHNG8B是一种专用处理器指令,可以处理未对齐的64位变量。我没有硬件可以看到其他32位处理器的外观。

请记住,托管代码的目标体系结构在编译时没有被确定。它是在运行时使MSIL适应目标的抖动。这对C ++ / CLI项目来说并不那么重要,因为如果使用/ clr而不是/ clr:pure编译,通常必须选择目标,并且只有x86和x64可以工作。但无论如何管道都已就位,所以宏观不是很有用。


9
2018-06-01 07:42



对不起,我没理解。假设InterlockedExchangeAdd64的实现在32位上运行良好(没有instrinsic!),那么InterlockedIncrement64宏也可以以相同的方式实现(在32位托管环境中)。对此进行扩展然后还可以在32位上可靠地实现64位上的本机功能。怎么样?!好吧,我猜这个措辞 “这个函数是原子的 关于调用其他互锁功能“。 是关键,但遵循我看不到的路径 特别 代码来做到这一点 - Adriano Repetti
不太确定挂断的位置,宏只能在编译时工作。当代码在CLR中时,这很好,您将在运行时使用与目标体系结构匹配的适当版本。因此,调用辅助函数始终可以完成工作。它在您自己的.NET程序集中编译时效果不佳,因为它可以在不同的平台上运行。当目标平台允许时,它仍然在运行时(LOCK XADD)转换为内联汇编代码当然是最佳点。处理器在任何一种情况下都提供保证。 - Hans Passant
是的,当然我理解宏必须在编译时解决。假设您有一个针对Win32的混合C ++ / CLI项目。你可以调用Interlocked :: Increment(long long)。在运行时,JIT将使用自己的帮助函数替换该调用。现在:1)如果存在辅助函数,那么也可以以相同的方式实现InterlockedIncrement64(而不是删除)。 2)如果a)你在32位系统上并且b)内存不需要对齐,它怎么能是原子的? - Adriano Repetti
@AdrianoRepetti - 一个简单的方法吧 可以 完成(不是说)。 您 没有控制托管对象的内存布局,运行时也是如此。运行时还需要保证它可以执行64位互锁操作。嗯。这里似乎涉及相同的“片段”代码。所以 可以决定始终确保64位整数 是 64位对齐, 如果 这是对运行时所针对的体系结构的要求。 - Damien_The_Unbeliever
只需使用针对x86的示例C ++项目并单步执行机器代码即可。您将发现LOCK CMPXCHNG8B,这是一种专用处理器指令,可以处理未对齐的64位变量。同样出现在Windows最低要求中,早期的AMD处理器还没有这个指令。 - Hans Passant