编译屏障的目的是什么?

时间:2013-06-07 03:50:48

标签: c++ multithreading concurrency atomic memory-barriers

以下内容摘自Concurrent Programming on windows,第10章Page 528~529,c ++模板双重检查实施

T getValue(){
    if (!m_pValue){
        EnterCriticalSection(&m_crst);
        if (! m_pValue){
            T pValue = m_pFactory();
            _WriteBarrier();
            m_pValue = pValue;                  
        }
        LeaveCriticalSection(&m_crst);
    }
      _ReadBarrier();
  return m_pValue;
}

正如作者所述:

  

在实例化对象之后找到_WriteBarrier,但之前   在m_pValue字段中写一个指向它的指针。这是必须的   确保在对象的初始化中写入永远不会得到   延迟写入m_pValue本身。

由于_WriteBarrier是编译屏障,如果编译知道LeaveCriticalSection的语义,我认为它没有用。编译可能省略了对pValue的写入,但从未优化过在函数调用之前移动赋值,否则会违反程序语义。我相信LeaveCriticalSection有隐式硬件围栏。因此,在分配给m_pValue之前的任何写作都将被同步。

另一方面,如果编译不知道LeaveCriticalSection的语义,则所有平台中将需要_WriteBarrier,以防止编译从临界区移出分配。

对于_ReadBarrier,作者说

  

同样,我们在返回m_value之前需要一个_ReadBarrier   在调用getValue之后加载的内容不会重新排序   在通话之前。

首先, 如果此函数包含在库中,并且没有可用的源代码,编译如何知道是否存在编译障碍?

其次,如果需要,它将被放置在错误的位置,我认为我们需要在EnterCriticalSection之后将其放置以表达获取围栏。与我上面所写的相似,它取决于编译是否理解EnterCriticalSection的语义。

并且作者还说:

  

但是,我还要指出X86上不需要围栏,   Intel64和AMD64处理器。 弱处理器很不幸   像IA64一样混淆了水域

正如我上面分析的那样,如果我们在某个平台上需要这些障碍,那么我们需要在所有平台上使用它们,因为这些障碍是编译障碍,它只是确保编译可以做正确的优化,以防他们不'理解一些函数的语义。

如果我错了,请纠正我。

另一个问题,是否有任何关于msvc和gcc的参考指出他们理解哪些函数的同步语义?

更新1 : 根据答案(m_pValue将从关键部分访问),并运行here中的示例代码,我认为:

  1. 我认为作者的意思是除编译障碍以外的硬件围栏,请参阅MSDN的以下引用。
  2. 我认为硬件围栏也有隐式编译屏障(禁用编译优化),但反之亦然(参见here,使用cpu fence不会看到任何重新排序,反之亦然)
  3.   

    障碍不是围栏..应该注意障碍效应   缓存中的一切。围栏会影响单个缓存行。

         

    除非绝对必要,否则不应增加障碍。使用   一个围栏,你可以选择一个_Interlocked内在函数。

    正如作者写道:“ X86 Intel64和AMD64处理器都不需要围栏”,这是因为这些平台只允许存储加载重新排序。

    还有一个问题,编译是否理解调用Enter / Leave关键部分的语义?如果没有,那么它可能会按照以下答案进行优化,这将导致不良行为。

    由于

2 个答案:

答案 0 :(得分:2)

_ReadBarrier和_WriteBarrier

Joe Duffy认为_ReadBarrier和_WriteBarrier编译器内在函数都是编译器和处理器级别的围栏。在Concurrent Programming on windows,第515页,他写了

  

一组编译器内在函数强制编译器和处理器级别   VC ++中的栅栏:_ReadWriteBarrier发出一个完整的栅栏,_ReadBarrier   发出只读栅栏,_WriteBarrier发出只写栅栏。

auther依赖于_ReadBarrier和_WriteBarrier编译器内在函数来防止编译器和硬件重新排序。

_ReadWriteBarrier编译器内在函数的MSDN文档不支持编译器内在函数影响硬件级别的假设。 Visual Studio 2010和Visual Studio 2008的MSDN文档明确否认编译器内在函数适用于硬件级别:

  

_ReadBarrier,_WriteBarrier和_ReadWriteBarrier编译器内在函数仅阻止编译器重新排序。要防止CPU重新排序读写操作,请使用MemoryBarrier宏。

Visual Studio 2005和Visual Studio .NET 2003的MSDN文档没有这样的说明。它没有说明内在函数是否适用于硬件级别。

如果_ReadBarrier和_WriteBarrier确实没有强制执行硬件围栏,则代码不正确。

关于术语“围栏”

Joe Duffy在他的书中使用术语 fence 来表示硬件和内存防护。在第511页,他写道:

  

围栏也常被称为障碍。英特尔似乎更喜欢“围栏”术语,而AMD更喜欢“障碍”。我也更喜欢“围栏”,这就是我在本书中使用的内容。

硬件围栏

  

我相信硬件围栏也有隐式编译障碍(禁用编译优化)

Synchronization and Multiprocessor Issues文章确认硬件障碍也会影响编译器:

  

这些说明(内存屏障)还可确保编译器禁用任何可能跨屏障重新排序内存操作的优化。

但是,MemoryBarrier macro的MSDN文档表明并不总是阻止编译器重新排序:

  

创建一个硬件内存屏障(fence),防止CPU重新排序读写操作。它还可能阻止编译器重新排序读写操作。

实际上,如果编译器可以重新排序它周围的内存操作,我不明白如何使用硬件围栏。我们不确定围栏是否占据了正确的位置。

答案 1 :(得分:1)

<强> TL; DR:
工厂调用很可能需要在分配到m_pValue后移动几个步骤。表达式!m_pValue将在工厂调用完成之前返回false,在第二个线程中给出不完整的返回值。

<强>解释

  

编译可能省略了对pValue的写入,但从未优化过在函数调用之前移动赋值,否则会违反程序语义。

不一定。将T视为int*,并且工厂方法创建一个新的int并使用42初始化它。

int* pValue = new int(42);
m_pValue = pValue;         
//m_pValue now points to anewly allocated int with value 42.

对于编译器,new表达式将是可以在另一个之前移动的几个步骤。它的语义是分配,初始化,然后将地址分配给pValue

int* pTmp = new int;
*pTmp = 42;
int* pValue = *pTmp;

在顺序程序中,如果某些命令在其他命令之后移动,则语义不会改变。特别是可以在内存分配和第一次访问之间自由移动赋值,即第一次取消引用其中一个指针,包括在新表达式之后赋值指针值之后:

int* pTmp = new int;
int* pValue = *pTmp;
m_pValue = pValue;  
*pTmp = 42;
//m_pValue now points to a newly allocated int with value 42.

编译器可能会这样做来优化大多数临时指针:

m_pValue = new int;  
*m_pValue = 42;
//m_pValue now points to a newly allocated int with value 42.

这是顺序程序的正确语义。

  

我相信LeaveCriticalSection有隐式硬件围栏。因此,在分配给m_pValue之前的任何写作都将被同步。

没有。栅栏是在赋值给m_pValue之后,但编译器仍然可以在它和栅栏之间移动整数赋值:

m_pValue = new int;  
*m_pValue = 42;
LeaveCriticalSection();

而且为时已晚,因为Thread2不需要输入CriticalSection:

Thread 1:                | Thread 2:
                         |
m_pValue = new int;      | 
                         | if (!m_pValue){     //already false
                         | }
                         | return m_pValue;
                         | /*use *m_pValue */
*m_pValue = 42;          |
LeaveCriticalSection();  |