强制执行C语句的顺序?

Ira*_*ter 16 c multithreading synchronization volatile memory-barriers

我遇到了MS C编译器重新排序某些语句的问题,这些语句在多线程上下文中非常关键,处于高优化级别.我想知道如何在特定的地方强制订购,同时仍然使用高水平的优化.(在低优化级别,此编译器不重新排序语句)

以下代码:

 ChunkT* plog2sizeChunk=...
 SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
 x = plog2sizeChunk->pNext;
Run Code Online (Sandbox Code Playgroud)

产生这个:

 0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
 00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh 
Run Code Online (Sandbox Code Playgroud)

其中pPoolAndBusyFlag的写入由编译器重新排序,在pNext fetch 之后发生.

SET_BUSY本质上是

  plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
Run Code Online (Sandbox Code Playgroud)

我认为编译器已经正确地决定重新排序这些访问是可行的,因为它们是同一结构的两个独立成员,并且这种重新排序对单线程执行的结果没有影响:

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;
Run Code Online (Sandbox Code Playgroud)

出于我的目的,必须在对此结构的其他访问在多线程/多核上下文中有效之前设置pPoolAndBusyFlag.我不认为这个 特定的访问对我来说是有问题的,但是编译器可以重新排序这一事实意味着我的代码的其他部分可能具有相同类型的重新排序,但在这些地方它可能是关键的.(想象一下,这两个语句是对两个成员的更新,而不是一个写入/一个读取).我希望能够强制执行动作的顺序.

理想情况下,我会写一些类似于:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 #pragma no-reordering // no such directive appears to exist
 pNext = plog2sizeChunk->pNext;
Run Code Online (Sandbox Code Playgroud)

我已经通过实验证实我可以用这种丑陋的方式获得这种效果:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 asm {  xor eax, eax }  // compiler won't optimize past asm block
 pNext = plog2sizeChunk->pNext;
Run Code Online (Sandbox Code Playgroud)

 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]  
Run Code Online (Sandbox Code Playgroud)

我注意到x86硬件可能会重新排序这些特定的指令,因为它们没有引用相同的内存位置,并且读取可以通过写入; 要真正修复这个例子,我需要某种类型的内存屏障.回到我之前的评论,如果它们都是写入,x86将不会对它们重新排序,并且其他线程将按顺序看到写入顺序.因此,在这种情况下,我认为我不需要内存屏障,只需强制订购.

我还没有看到编译器重新订购两个写(但是)但是我还没有看起来很难(还); 我刚刚绊倒了这个.当然,仅仅因为你没有在这个编译中看到它而进行优化并不意味着它不会出现在下一个.

那么,我如何强制编译器对它们进行排序呢?

我知道我可以声明结构中的内存槽是volatile.它们仍然是独立的存储位置,因此我不知道这是如何阻止优化的.也许我错误地解释了挥发性意味着什么?

编辑(10月20日):感谢所有响应者.我当前的实现使用volatile(用作初始解决方案),_ ReadWriteBarrier(用于标记编译器不应该进行重新排序的代码),以及一些MemoryBarriers(其中发生读写),这似乎解决了问题.

编辑:(11月2日):为了清洁,我最终为ReadBarrier,WriteBarrier和ReadWriteBarrier定义了一组宏.有锁定前后锁定,解锁前后解锁以及一般用法.其中一些是空的,一些包含_ReadWriteBarrier和MemoryBarrier,适用于x86和基于XCHG的典型自旋锁[XCHG包括一个隐式的MemoryBarrier,从而避免了锁定前置/后置的需要).然后我将这些文件停放在适当的代码中,记录了基本(非)重新排序的要求.

Voo*_*Voo 6

所以我理解它pNext = plog2sizeChunk->pNext发布块,以便其他线程可以看到它,你必须确保它们看到正确的忙标志.

这意味着发布它之前你需要一个单向的内存屏障(在另一个线程中读取它之前也是一个,尽管如果你的代码在x86上运行你可以免费获得它们)以确保线程实际看到了这个变化.在写入之前还需要一个,以避免在它之后重新排序写入.不只是插入组件或使用符合标准的挥发性(MSVC挥发性提供额外的保证,虽然使这里差)是足够的-是的,这会阻止移动读取和周围写的编译器,但CPU并不受其约束并能做到内部相同的重新排序.

MSVC和gcc都有内在函数/宏来创建内存屏障(参见此处).MSVC还为易于解决问题的挥发物提供更强的保证.最后C++ 11原子也会起作用,但我不确定C本身是否有任何可移植的方法来保证内存障碍.

  • @Ira 是的 x86 有很强的订购要求。这意味着许多内存屏障对于该 ISA 来说是一个 nop 是真的。我只是认为最好为所有架构编写正确的代码,让编译器编写者担心给定架构隐含哪些障碍,哪些不是。 (2认同)