强制执行C语句的顺序?

时间:2013-10-10 09:32:51

标签: 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;

产生这个:

 0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
 00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh 

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

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

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

那么,我如何强制编译器订购这些?

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

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

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

3 个答案:

答案 0 :(得分:6)

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

这意味着在发布之前你需要一个单向内存屏障(在另一个线程中读取它之前也是一个,尽管如果你的代码在x86上运行,你可以免费获得这些内容)来确保线程实际上看到了变化。在写入之前还需要一个,以避免在它之后重新排序写入。不只是插入程序集或使用标准兼容的volatile(MSVC volatile提供了额外的保证,虽然这在这里有所不同)是足够 - 是的这会阻止编译器转移读写,但CPU是不受它约束,可以在内部进行相同的重新排序。

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

答案 1 :(得分:3)

_ReadWriteBarrier。这是一个致力于您正在寻找的编译器内在。请务必根据MSVC的精确版本检查文档(VS2012上的“已弃用”...)。注意cpu重新排序(然后参见MemoryBarrier

_ReadBarrier,_WriteBarrier和_ReadWriteBarrier编译器内在函数(编译器重新排序)以及MemoryBarrier宏(CPU重新排序)从VS2012开始全部“弃用”的文档states。但我认为他们将继续工作一段时间......

新代码可能会使用新的C ++ 11工具(MSDN页面中的链接)

答案 2 :(得分:0)

我会使用volatile关键字。它将阻止编译器重新排序指令。 http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword