什么时候使用仅编译器内存屏障(例如std::atomic_signal_fence)是有用的?

36

当我阅读关于内存模型、屏障、顺序、原子操作等相关内容时,经常会遇到“编译器栅栏”的概念,但通常它都是与“CPU栅栏”配对使用的。

然而,偶尔我也会看到一些仅适用于编译器的栅栏结构。例如C++11中的std::atomic_signal_fence函数,cppreference.com上指出:

std::atomic_signal_fence相当于std::atomic_thread_fence,但不会发出任何用于内存排序的CPU指令。只有编译器对指令重新排序被禁止。

我有五个与此主题相关的问题:

  1. 如其名称所示std::atomic_signal_fence,在异步中断(例如线程被内核抢占以执行信号处理程序)的情况下,是否是使用仅编译器栅栏有用的唯一情况?

  2. 它的实用性是否适用于所有体系结构,包括强排序的体系结构,如x86

  3. 能否提供一个具体的示例来证明仅编译器栅栏的实用性?

  4. 当使用std::atomic_signal_fence时,使用acq_relseq_cst排序是否有任何区别?(我希望这不会有任何区别。)

  5. 这个问题可能已经被第一个问题涵盖了,但我还是很好奇,特别是关于它:是否必须thread_local访问中使用栅栏?(如果需要的话,我期望仅编译器栅栏,如atomic_signal_fence,是首选工具。)

谢谢。


2
你有检查过吗?http://preshing.com/20120625/memory-ordering-at-compile-time。 - oblitum
3
引用preshing.com的话:“正如我之前提到的,编译器屏障足以防止单处理器系统中的内存重排。但是现在已经是2012年了,多核计算成为了常态。如果我们想要确保在多处理器环境下,并且在任何CPU架构上我们的交互发生在所需的顺序中,那么仅使用编译器屏障是不够的。[...]" - oblitum
1
@chico:很好的观点——如果程序员知道应用程序在非SMP系统上运行(即,单CPU与单核心由于某种原因在内核中禁用了SMP),这是编译器不可能知道或假设的,那么atomic_signal_fence(或其他仅限编译器的栅栏构造)可以用作潜在的优化。正如文章所述,Linux内核具有实现此方式的函数smp_rmbsmp_wmb。然而,我仍然对听到不受此假设限制的答案(如果存在的话)感兴趣。 - etherice
我认为,在架构应用程序中利用处理器亲和性运行也可能很有用,其中多个实例在它们各自的特定核心中独立并行运行,因此,仅编译器屏障可以是一种优化,只是必要的。 - oblitum
@chico:提到处理器亲和性也是个好观点,但实际上这与之前的假设基本相同,因为如果应用程序严格绑定到单个核心,则将SMP环境降级为非SMP环境(对于该应用程序而言)。 - etherice
为了避免围绕这个主题的回复,您可以添加/更改问题以表明您对问题的观点不感兴趣,以便更好地聚焦。 - oblitum
2个回答

26
为了回答这5个问题:

1)编译器栅栏(单独使用,没有CPU栅栏)只在两种情况下有用:

  • 强制执行内存顺序约束 在一个线程和绑定到同一线程的异步中断处理程序之间(例如信号处理程序)。

  • 强制执行内存顺序约束 在多个线程之间,保证每个线程都将在同一CPU核心上执行。换句话说,应用程序仅在单核系统上运行,或者应用程序通过处理器亲和性采取特殊措施确保共享数据的每个线程都绑定到同一核心。


2)底层架构的内存模型,无论是强序还是弱序,都与是否需要编译器栅栏无关。


3)这里是伪代码,演示了单独使用编译器栅栏来足够地同步线程和绑定到同一线程的异步信号处理程序之间的内存访问:

void async_signal_handler()
{
    if ( is_shared_data_initialized )
    {
        compiler_only_memory_barrier(memory_order::acquire);
        ... use shared_data ...
    }
}

void main()
{
// initialize shared_data ...
    shared_data->foo = ...
    shared_data->bar = ...
    shared_data->baz = ...
// shared_data is now fully initialized and ready to use
    compiler_only_memory_barrier(memory_order::release);
    is_shared_data_initialized = true;
}

注意: 本例假设async_signal_handler与初始化shared_data并设置is_initialized标志的线程绑定相同,这意味着应用程序是单线程的,或者它设置了线程信号掩码。否则,编译器屏障将不足以防止重排序,需要使用CPU屏障


4) 它们应该是相同的。 acq_relseq_cst都应该导致完整(双向)的编译器屏障,没有任何与屏障有关的CPU指令被发出。当涉及多个核心和线程时,"顺序一致性"的概念才会发挥作用,并且atomic_signal_fence仅涉及一个执行线程。


5) 不需要。 (除非当然,如果从异步信号处理程序访问线程本地数据,则可能需要编译器屏障。)否则,不应该在使用线程本地数据时需要使用屏障,因为编译器(和CPU)只允许以不改变程序在单线程观点下对其序列点的可观察行为的方式重新排序内存访问。可以将多线程程序中的线程本地静态变量逻辑地视为单线程程序中的全局静态变量。在两种情况下,数据仅由一个线程访问,这可以防止数据竞争。


1
信息丰富但不准确。在某些情况下,编译器屏障对于特定处理器的预C11代码是有用的。例如,如果您使用的是x86并且满足获取-释放,但希望允许编译器重新排序块内不同地址的内存操作,则编译器围绕该块进行屏障(但保留内存访问非易失性)是实现此目的的唯一方法。 - user2949652
@user2949652 但是 atomic_signal_fence 只适用于原子类型,而不适用于普通对象,因此您必须更改所有数据类型。 - curiousguy

3

实际上,在一些非可移植但有用的C编程习惯中,即使在多核代码(特别是在C11之前的代码)中,编译器栏障也很有用。典型情况是程序正在进行一些访问,这些访问通常会被设置为易失性(因为它们是共享变量),但您希望编译器能够移动这些访问。如果您知道目标平台上的访问是原子的(并且采取了其他预防措施),则可以将访问设置为非易失性,但使用编译器栏障来包含代码移动。

值得庆幸的是,C11/C++11放松原子操作已经使大多数此类编程过时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接