【发布时间】:2013-10-17 23:53:10
【问题描述】:
我在 MS C 编译器重新排序某些语句时遇到问题,这在多线程上下文中很关键,在高级别的优化中。我想知道如何在仍然使用高级优化的同时强制在特定位置进行排序。 (在低优化级别,此编译器不会重新排序语句)
以下代码:
ChunkT* plog2sizeChunk=...
SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
x = plog2sizeChunk->pNext;
产生这个:
0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh
其中对 pPoolAndBusyFlag 的写入由编译器重新排序以在 pNext 提取之后发生。
SET_BUSY 本质上是
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
我认为编译器正确地认为重新排序这些访问是可以的,因为它们是针对同一结构的两个不同成员的,并且这种重新排序对单线程执行的结果没有影响:
typedef struct chunk_tag{
unsigned pPoolAndBusyFlag; // Contains pointer to owning pool and a busy flag
natural log2size; // holds log2size of the chunk if Busy==false
struct chunk_tag* pNext; // holds pointer to next block of same size
struct chunk_tag* pPrev; // holds pointer to previous block of same size
} ChunkT, *pChunkT;
出于我的目的,必须先设置 pPoolAndBusyFlag,然后才能在多线程/多核上下文中对该结构的其他访问有效。我不认为这个 特定的访问对我来说是有问题的,但编译器可以重新排序这一事实 意味着我的代码的其他部分可能具有相同类型的重新排序,但它可能 在那些地方要挑剔。 (想象这两个语句是对这两个语句的更新 成员而不是一次写入/一次读取)。我希望能够强制执行操作的顺序。
理想情况下,我会写如下内容:
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
#pragma no-reordering // no such directive appears to exist
pNext = plog2sizeChunk->pNext;
我已经通过实验验证了我可以用这种丑陋的方式获得这种效果:
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
asm { xor eax, eax } // compiler won't optimize past asm block
pNext = plog2sizeChunk->pNext;
给予
0040130F 83 22 FE and dword ptr [edx],0FFFFFFFEh
00401312 33 C0 xor eax,eax
00401314 8B 5A 08 mov ebx,dword ptr [edx+8]
我注意到 x86 硬件可能会重新排序这些特定指令,因为它们不引用相同的内存位置,并且读取可能会通过写入;要真正修复 this 示例,我需要某种类型的内存屏障。回到我之前的评论,如果它们都是写入,x86 不会重新排序它们,其他线程将按照该顺序看到写入顺序。所以在那种情况下,我认为我不需要内存屏障,只需要强制排序。
我还没有看到编译器重新排序两次写入(还),但我还没有很努力地寻找(还);我刚刚被这个绊倒了。当然,优化只是因为你在这个编译中没有看到它并不意味着它不会出现在下一个。
那么,我如何强制编译器订购这些?
我知道我可以将结构中的内存槽声明为易失性。它们仍然是独立的存储位置,所以我看不出这是如何阻止优化的。也许我误解了 volatile 的含义?
编辑(10 月 20 日):感谢所有响应者。我当前的实现使用 volatile(用作初始解决方案)、_ReadWriteBarrier(标记编译器不应该发生重新排序的代码)和一些 MemoryBarriers(发生读取和写入的地方),这似乎已经解决了问题.
编辑:(11 月 2 日):为了清楚起见,我最终定义了 ReadBarrier、WriteBarrier 和 ReadWriteBarrier 的宏集。有用于前后锁定、前后解锁和一般用途的套装。其中一些是空的,一些包含 _ReadWriteBarrier 和 MemoryBarrier,适用于 x86 和基于 XCHG 的典型自旋锁 [XCHG 包含一个隐式 MemoryBarrier,因此避免了对锁前集/后集的需要)。然后,我将它们放在代码中记录基本(非)重新排序要求的适当位置。
【问题讨论】:
-
+1 个经过充分研究的问题。
-
嗯,你不知道这是多么痛苦的研究:-{
-
SET_BUSY 是否比
&=更多,或者此代码是否受互斥锁保护或以其他方式设为线程安全?因为如果您尝试使用基本上相当于if (!busy) { busy = true; DoStuff(); busy = false; }的线程来同步线程,那么无论顺序如何,这都不是线程安全的。 -
嗯。 C++ 标准似乎说,对 volatile 变量的读取和写入不能相互移动,即使它们不在同一个位置。也许这就是治疗方法,假设它适用于 C。
-
这对于 standard volatiles 是正确的,但是如果你使用
volatile:ms标志(这恰好是除 ARM 之外的所有东西的默认值),MSVC 会为 volatiles 提供更强的保证,因此我的问题。使用该标志集,volatile 的行为就像在 JMM 或 .NET 中一样。 IE。写有释放,读有语义。
标签: c visual-c++ synchronization memory-barriers lock-free