32位环境下对64位变量进行原子递增操作

11

在回答另一个问题时,一些有趣的事情浮出水面,现在我不明白Interlocked.Increment(ref long value)在32位系统上是如何工作的。让我解释一下。

当编译为32位环境时,本地InterlockedIncrement64现在不可用,这是可以理解的,因为在.NET中无法按照要求对齐内存,它可能被从托管调用,所以他们放弃了它。

在.NET中,我们可以使用对64位变量的引用调用Interlocked.Increment(),我们仍然没有任何关于其对齐方式的约束(例如在结构中,也可以使用FieldOffsetStructLayout),但文档没有提到任何限制(据我所知)。这是一种神奇的方法,它起作用了!

Hans Passant指出,Interlocked.Increment()是JIT编译器识别的一种特殊方法,它会发出对COMInterlocked::ExchangeAdd64()的调用,然后调用FastInterlockExchangeAddLong,这是InterlockedExchangeAdd64的一个宏,它与InterlockedIncrement64具有相同的限制
现在我很困惑。
请先忘记托管环境并回到本机环境。为什么InterlockedIncrement64无法工作,但InterlockedExchangeAdd64可以?InterlockedIncrement64是一个宏,如果内部函数不可用且InterlockedExchangeAdd64起作用,则可能会将其实现为对InterlockedExchangeAdd64的调用...
让我们回到托管环境:在32位系统上如何实现原子64位增量?我想这句话“此函数与调用其他互锁函数相关”很重要,但我仍然没有看到任何代码(感谢Hans指出更深入的实现)来完成它。让我们从WinBase.h中选择InterlockedExchangedAdd64的实现,当内部函数不可用时:
FORCEINLINE
LONGLONG
InterlockedExchangeAdd64(
    _Inout_ LONGLONG volatile *Addend,
    _In_    LONGLONG Value
    )
{
    LONGLONG Old;

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

    return Old;
}

如何使其在读取/写入时是原子的?


谁说过"InterlockedIncrement64不能工作,但InterlockedExchangeAdd64可以"? 你原来的回答是正确的,因为托管代码不能直接调用本地Win32 API并期望一切正常。它们都不会起作用。你必须使用托管助手。现在,托管助手的实现是本地代码,因此它调用本地函数。由于宏和内置函数在编译时解析,所以CLR的位数很重要。 - Cody Gray
是的,但32位JIT将调用InterlockedExchangeAdd64,其在本机中具有与InterlockedIncrement64相同的限制。我不明白的是如何实现它(因为在为托管代码调用时存在内存对齐问题)。32位实现使用InterlockedCompareExchange64,但...嗯...可能不是原子操作(用于写回结果...) - Adriano Repetti
1
“如何进行读/写的原子操作?” InterlockedExchangeAdd64 的文档提供了一些线索,它说:“此函数生成完整的内存屏障(或栅栏),以确保内存操作按顺序完成。”请注意,您上面展示的实现调用了 InterlockedCompareExchange64。在 32 位构建中,这会发出带有 LOCK 前缀的 CMPXCHG8B 指令。这确保了指令的原子执行。您永远不会获得一个锁定的读取而没有锁定的写入,因此写入目标是原子的。 - Cody Gray
1个回答

9

您需要继续跟踪,InterlockedExchangeAdd64() 将带您进入 WinNt.h SDK 头文件。在那里,您将看到许多版本,具体取决于目标体系结构。

通常情况下,这可以简化为:

#define InterlockedExchangeAdd64 _InterlockedExchangeAdd64

这实际上把任务交给了一个编译器内部函数实现,该函数在vc/include/intrin.h中声明,在编译器的后端实现。

换句话说,不同版本的CLR将具有不同的实现方式。多年来,例如x86、x64、Itanium、ARM、ARM8和PowerPC等处理器都有许多实现。对于x86,在处理器指令LOCK CMPXCHNG8B的支持下,可以处理不对齐的64位变量。我没有硬件设备来查看其他32位处理器的情况。

请记住,托管代码的目标架构不是在编译时确定的。它是即时编译器在运行时将MSIL适配到目标架构上。对于C++ / CLI项目来说,这并不太重要,因为通常需要选择一个目标,如果使用/clr而不是/clr:pure进行编译,则只能使用x86和x64。但是,基础结构已经就位,因此宏不是非常有用。


抱歉,我不明白。假设在32位上实现的InterlockedExchangeAdd64工作正常(没有内部函数!),那么InterlockedIncrement64宏也可以以同样的方式实现(在32位托管环境中)。通过扩展这一点,64位本机函数也可以可靠地在32位上实现。怎么做?嗯,我想措辞“该函数原子性地与对其他交错函数的调用有关。”是关键,但是沿着这条路走,我看不到任何“特殊”的代码来执行它。 - Adriano Repetti
@AdrianoRepetti - 有一种简单的方法可以实现(并不是说它就是这样做的)。您无法控制托管对象的内存布局,而运行时会控制。运行时还需要保证它可以执行64位交错操作。嗯。这里涉及到相同的“代码”部分。因此,如果对于该运行时所针对的架构来说这是一个要求,那么它可能决定始终确保64位整数64位对齐的。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 这是有道理的,在32位系统上写回值不会是原子性的,但是嗯嗯嗯 - Adriano Repetti
1
只需使用针对x86的示例C++项目,并逐步执行机器代码进行尝试。您将发现LOCK CMPXCHNG8B,这是一条专用处理器指令,可以处理不对齐的64位变量。此外,在Windows的最低要求中也有它的身影,早期的AMD处理器还没有这个指令。 - Hans Passant
@HansPassant 谢谢,就是这样!请在您的答案中也包含它,它解决了我已经添加到问题中的进一步疑问。 - Adriano Repetti
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接