【问题标题】:Force order of execution of C statements?强制执行 C 语句的顺序?
【发布时间】: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


【解决方案1】:

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

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

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

【讨论】:

  • 为什么我需要一个内存屏障来防止写入重新排序? x86 体系结构确保写入按程序顺序完成。同意,在代码混合读取和写入的地方,我需要一个内存屏障来防止读取通过关键写入。
  • @Ira 是的,x86 有非常严格的订购要求。这意味着许多内存屏障对于该 ISA 来说都是一个问题。我只是认为最好为所有架构编写正确的代码,让编译器编写者担心给定架构隐含哪些障碍,哪些不是。
  • 是时候奖励答案了。感谢您的帮助。
  • 为了记录,C11 有 <stdatomic.h> (en.cppreference.com/w/c/atomic),它提供了与 C++11 <atomic> 基本相同的功能。这使您可以编写具有获取/释放(或 seq_cst)语义的可移植代码,让编译器根据目标 ISA 的需要使用或不使用屏障指令。或者在 ARM 上,使用获取加载和/或释放存储 指令 比普通加载或存储 + 单独的屏障内在效率要高得多。 (如果你想要像 Q 中的 and dword ptr [edx],0FFFFFFFEh 这样的非原子 RMW,你确实需要单独的原子加载和存储)
【解决方案2】:

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

文档states 说明 _ReadBarrier、_WriteBarrier 和 _ReadWriteBarrier 编译器内在函数(编译器重新排序)和 MemoryBarrier 宏(CPU 重新排序)从 VS2012 开始都已“弃用”。但我认为它们会在一段时间内继续正常工作......

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

【讨论】:

  • _WriteBarrier 没用,因为:The _ReadBarrier, _WriteBarrier, and _ReadWriteBarrier compiler intrinsics prevent compiler re-ordering only
  • 我知道,看到我提到的 CPU 重新排序了吗?我刚刚回答了 OP 的问题...
  • 是但是:That's a compiler intrinsic dedicated to what you are looking for 显然是错误的,因为它没有解决 OPs 问题,那为什么还要提到 _WriteBarrier 呢?
  • 我明白你的意思,相信我。但是 OP 的问题”是“那么,我如何强制编译器订购这些?”
  • +1 这非常有用。特别是,它让我在不应该发生这种重新排序的地方注释代码。我不高兴听到它在 VS 2012 中被“弃用”;他们打算用什么代替它?
【解决方案3】:

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

【讨论】:

  • +1 谢谢,确实有帮助。我发现@manuell 对 ReadWriteBarrier 的回答非常有用,而不是大锤。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-18
  • 2011-10-01
  • 1970-01-01
  • 2012-05-28
  • 2018-02-18
相关资源
最近更新 更多