17

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

我还没有看到编译器重新排序两次写入(还),但我还没有很努力地寻找(还);我刚刚被这个绊倒了。当然,优化只是因为你在这个编译中没有看到它并不意味着它不会出现在下一个。

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

我知道我可以将结构中的内存插槽声明为易失性。它们仍然是独立的存储位置,所以我看不出这如何阻止优化。也许我误解了 volatile 的含义?

编辑(10 月 20 日):感谢所有响应者。我当前的实现使用 volatile(用作初始解决方案)、_ReadWriteBarrier(标记编译器不应该发生重新排序的代码)和一些 MemoryBarriers(发生读取和写入的地方),这似乎已经解决了问题.

编辑:(11 月 2 日):为了干净,我最终定义了 ReadBarrier、WriteBarrier 和 ReadWriteBarrier 的宏集。有用于前后锁定、前后解锁和一般用途的套装。其中一些是空的,一些包含 _ReadWriteBarrier 和 MemoryBarrier,适用于 x86 和基于 XCHG 的典型自旋锁 [XCHG 包含一个隐式 MemoryBarrier,因此避免了对锁前集/后集的需要)。然后,我将这些放在代码中记录基本(非)重新排序要求的适当位置。

4

3 回答 3

7

因此,据我了解,pNext = plog2sizeChunk->pNext发布块以便其他线程可以看到它,并且您必须确保他们看到正确的繁忙标志。

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

MSVC 和 gcc 都有内在函数/宏来创建内存屏障(参见例如此处)。MSVC 还为足以解决您的问题的 volatile 提供了更强的保证。最后 C++11 原子也可以工作,但我不确定 C 本身是否有任何可移植的方式来保证内存屏障。

于 2013-10-10T13:21:52.350 回答
4

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

文档指出_ReadBarrier、_WriteBarrier 和 _ReadWriteBarrier 编译器内在函数(编译器重新排序)和 MemoryBarrier 宏(CPU 重新排序)从 VS2012 开始都“不推荐使用”。但我认为他们会在一段时间内继续正常工作......

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

于 2013-10-10T13:23:38.657 回答
0

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

于 2013-10-10T13:27:07.377 回答