内存屏障是否涉及内核

4
在问了这个问题后,我理解到原子指令(如test-and-set)不会涉及内核。只有当进程需要休眠(等待锁)或唤醒(因为无法获取锁但现在可以),内核才需要介入执行调度操作。
如果是这样,那么内存屏障(例如C++11中的std::atomic_thread_fence)是否也不会涉及内核呢?

你能解释一下为什么你想知道那些实现细节吗?作为一个给定系统(操作系统/编译器/库)的用户,你无法改变其行为。如果你需要同步内存访问,你必须使用它。我只是想知道任何答案将如何改变你的代码或其他内容...谢谢。 - Klaus
@Klaus 是因为优化。基于汇编语言的同步原语具有约100个周期的延迟(与主存储器访问量级相同,但这取决于许多因素)。如果涉及内核调度,可能需要几毫秒。在某些情况下存在实时应用程序,其中内核调度可能太长,最好使用自旋锁而不是yield()。 - Sigi
@Klaus 嗯,我正在研究C++11中mutexatomic之间的区别,特别是它们的性能。如果atomic和内存屏障不涉及内核,至少我可以确保atomic+内存屏障会产生更少的上下文切换。 - Yves
@Sigismondo:很明显,将上下文切换到内核模式所需的时间比一些汇编指令要长得多,而且会导致缓存行同步等操作。但是用户必须使用同步,因此无论如何都无法更改设计... - Klaus
好的,但是对于所有问题,您必须告诉我们使用的操作系统/编译器/库,因为所有这些都是特定于给定平台的。在小型嵌入式设备上,通常根本没有用于内存同步的汇编指令。因此,必须将这些屏障转发到某种底层操作系统调用。对于通常给定的Xx86 CPU,可以在汇编级别上处理它。所以是的,std::atomic_thread_fence,它通常不会在X86上调用任何操作系统函数。但是,您可以简单地查看编译器生成的代码。如果编译器发出了一些OS调用,您将看到所有OS调用。 - Klaus
你可以选择使用pthread_wait()或自旋循环。而正是在自旋循环中,许多不同的内存栅栏(在支持这种细粒度同步的架构上,例如ARM)变得有用于优化。然而,显然这对于实现内核同步本身非常有用-请参见我的答案中的引用。 - Sigi
2个回答

6

std::atomic不涉及内核1

在几乎所有正常的CPU上(我们在现实生活中编写程序的那种),内存屏障指令是非特权的,直接由编译器使用。编译器知道如何生成像x86 lock add [rdi], eax这样的指令来执行fetch_add(或者如果您使用返回值,则使用lock xadd)。或者在其他ISA上,它们使用相同的屏障指令在加载、存储和RMWs之前/之后进行排序。https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

在任意假设的硬件和/或编译器上,当然可以发生任何事情,即使对性能造成灾难性影响。

在汇编语言中,屏障只是使核心等待,直到一些先前的(程序顺序)操作对其他核心可见为止。这是一个纯本地操作。(至少,这是实际CPU的设计方式,以便通过控制本地加载和/或存储操作的本地排序来恢复顺序一致性。所有核心共享一个一致的缓存视图,通过类似MESI的协议维护。非一致的共享内存系统存在,但实现不会在它们之间运行C++ std::thread,并且通常不会运行单一系统映像内核。)

注1:(即使是非无锁原子操作通常也使用轻量级锁定)。

此外,ARMv7之前的ARM显然没有适当的内存屏障指令。 在ARMv6上,GCC使用mcr p15,0,r0,c7,c10,5作为屏障。
在此之前(g++ -march=armv5及更早版本),GCC不知道该怎么做,并调用__sync_synchronize(一个libatomic GCC辅助函数),希望它在代码实际运行的任何机器上都能够实现。这可能涉及到一个假设的ARMv5多核系统上的系统调用,但更有可能的是二进制文件将在ARMv7或v8系统上运行,其中库函数可以运行dmb ish。或者如果它是单核系统,则可能是无操作,我想。(C++内存排序关心其他C++线程,而不关心可能的硬件设备/ DMA所看到的内存顺序。通常实现假定是多核系统,但这个库函数可能是一个单核系统的情况下可以使用的例子。)


例如在x86架构中,std::atomic_thread_fence(std::memory_order_seq_cst)编译成了mfence。较弱的屏障例如std::atomic_thread_fence(std::memory_order_release)只需要阻止编译时的重新排序;x86的运行时硬件内存模型已经是acq/rel (seq-cst + 存储缓冲器),所以没有与屏障相对应的汇编指令。(C++库的一种可能实现是GNU C asm("" ::: "memory");,但GCC / clang确实有内置的屏障。) std::atomic_signal_fence只需阻止编译时的重新排序,即使在弱序的ISA上也是如此,因为所有真实的ISA都保证单个线程内的执行都按照程序顺序进行。(硬件通过在当前核心的存储缓冲区中监视加载操作来实现此操作)。具有延迟可见性加载的VLIW和IA-64 EPIC或其他显式并行ISM机制(例如Mill)仍然使得编译器可以生成遵守任何涉及屏障的C ++排序保证的代码,如果异步信号(或内核代码的中断)在任何指令之后到达。
你可以在Godbolt编译器探索器上查看代码生成:
#include <atomic>
void barrier_sc(void) {
    std::atomic_thread_fence(std::memory_order_seq_cst);
}

x86: mfence(内存屏障)
POWER:sync(同步指令)
AArch64:dmb ish(在“内部可共享”一致性域上进行完整的屏障操作)
使用gcc -mcpu=cortex-a15或者-march=armv7编译的ARM:dmb ish
RISC-V: fence iorw,iorw(内存栅栏指令)

void barrier_acq_rel(void) {
    std::atomic_thread_fence(std::memory_order_acq_rel);
}

x86: 没有任何操作
POWER: lwsync (轻量级同步).
AArch64: 仍然是 dmb ish
ARM: 仍然是 dmb ish
RISC-V: 仍然是 fence iorw,iorw

void barrier_acq(void) {
    std::atomic_thread_fence(std::memory_order_acquire);
}

x86:无需操作
POWER:lwsync(轻量级同步)
AArch64:dmb ishld(加载屏障,不必清空存储缓冲区)
ARM:即使使用-mcpu=cortex-a53(一个ARMv8),仍然是dmb ish :/
RISC-V:仍然是fence iorw,iorw


1
在这个问题和相关问题中,您混淆了以下两个方面:
  • 汇编范围内的同步原语,例如cmpxchg和fences
  • 进程/线程同步,例如futexes
“它涉及内核”是什么意思?我猜你的意思是“(p)threads同步”:线程被置于睡眠状态,并在另一个进程/线程满足给定条件时唤醒。
然而,像cmpxchg和内存栅栏这样的测试和设置原语是微处理器汇编提供的功能。内核同步原语最终基于它们来提供系统和进程同步,使用内核空间中隐藏在内核调用后面的共享状态。
您可以查看futex源码以获得证据。

但是,内存栅栏并不涉及内核:它们被转换为简单的汇编操作。与cmpxchg一样。


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