我阅读了"英特尔架构的英特尔优化指南指南".
但是,我仍然不知道何时应该使用
_mm_sfence()
_mm_lfence()
_mm_mfence()
Run Code Online (Sandbox Code Playgroud)
任何人都可以解释在编写多线程代码时何时应该使用它们?
在C++和2012之后:Herb Sutter - 原子<>武器, Herb Sutter中的2个(约0:38:20)认为应该使用xchg,而不是mov/ 在x86 mfence上实现atomic_store.他似乎也暗示这个特定的指令序列是每个人都同意的.但是,海湾合作委员会使用后者.为什么GCC使用这个特定的实现?
我想知道的是lock xchg,mfence从一个线程访问内存位置的角度来看是否会有类似的行为,这个内存位置正在被其他线程进行变异(让我们随便说).它能保证我获得最新的价值吗?之后的内存读/写指令?
我混淆的原因是:
8.2.2"读取或写入不能通过I/O指令,锁定指令或序列化指令重新排序."
-Intel 64 Developers Manual Vol.3
这是否适用于线程?
mfence 状态:
对MFENCE指令之前发出的所有内存加载和存储到内存指令执行序列化操作.此序列化操作保证在MFENCE指令之前的任何加载或存储指令全局可见之前,在程序顺序之前的每条加载和存储指令都是全局可见的.MFENCE指令针对所有加载和存储指令,其他MFENCE指令,任何SFENCE和LFENCE指令以及任何序列化指令(例如CPUID指令)进行排序.
-Intel 64 Developers Manual Vol 3A
这听起来更有力.因为它听起来mfence几乎正在刷写写缓冲区,或者至少延伸到写缓冲区和其他内核以确保我未来的加载/存储是最新的.
当基准标记时,两个指令都需要约100个循环才能完成.所以我无论如何都看不出那么大的差异.
主要是我只是困惑.我的指令基于lock互斥体使用,但后来这些包含没有内存栅栏.然后,我看到锁免费使用内存栅栏编程,但没有锁.我知道AMD64有一个非常强大的内存模型,但过时的值可以在缓存中持续存在.如果lock行为与行为不同,mfence那么互斥量如何帮助您查看最新值?
x86 assembly multithreading cpu-architecture memory-barriers
我已经阅读了很多关于内存排序的文章,并且所有这些文章都只说CPU重新加载和存储.
CPU(我对x86 CPU特别感兴趣)是否仅重新排序加载和存储,并且不重新排序它具有的其余指令?
我阅读了英特尔手册,发现指令有一个锁定前缀,可以防止处理器同时写入同一个内存位置.我很兴奋.我想它可以用作硬件互斥.所以我写了一段代码来拍摄.结果非常令人沮丧.锁不支持MOV或LEA指令.手册说LOCK仅支持ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD和XCHG.而且,如果LOCK前缀与这些指令之一一起使用并且源操作数是存储器操作数,则可以生成未定义的操作码异常(#UD).
我想知道为什么这么多限制,如此多的限制使得LOCK看起来毫无用处.我不能用它来保证一般的写操作没有脏数据或并行引起的其他问题.
例如,我在C中编写了代码++(*p).p是指向共享内存的指针.相应的程序集如下:
movl 28(%esp), %eax
movl (%eax), %eax
leal 1(%eax), %edx
movl 28(%esp), %eax
movl %edx, (%eax)
Run Code Online (Sandbox Code Playgroud)
我在"movl"和"leal"之前添加了"lock",但是处理器抱怨"无效指令".:-(我想将序列化写操作的唯一方法是使用软件互斥,对吧?
以下代码来自include/asm-i386/io.h,并且是从调用的dma_map_single()。我的理解是flush_write_buffers()应该在为 DMA 映射内存之前刷新 CPU 内存缓存。但是这个汇编代码是如何刷新 CPU 缓存的呢?
static inline void flush_write_buffers(void)
{
__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory");
}
Run Code Online (Sandbox Code Playgroud) 首先我想谈一下我对此的一些理解,如有错误请指正。
MFENCE在x86中可以保证全屏障顺序一致性可防止 STORE-STORE、STORE-LOAD、LOAD-STORE 和 LOAD-LOAD 重新排序
这是根据维基百科的说法。
std::memory_order_seq_cst不保证防止 STORE-LOAD 重新排序。
这是根据Alex 的回答,“负载可能会通过早期存储重新排序到不同位置”(对于 x86),并且 mfence 不会总是被添加。
a是否std::memory_order_seq_cst表示顺序一致性?根据第2/3点,我认为这似乎不正确。std::memory_order_seq_cst仅当以下情况时才表示顺序一致性
MFENCE添加到任一LOAD或STORE否则仍有可能重新订购。
根据@LWimsey的评论,我在这里犯了一个错误,如果 和LOAD都是STORE,memory_order_seq_cst则没有重新排序。Alex 可能指出使用非原子或非 SC 的情况。
std::atomic_thread_fence(memory_order_seq_cst)总是产生一个完整的屏障
这是根据Alex的回答。所以我总是可以替换asm volatile("mfence" ::: "memory")为std::atomic_thread_fence(memory_order_seq_cst)
这对我来说很奇怪,因为memory_order_seq_cst原子函数和栅栏函数之间的用法似乎有很大不同。
现在我在MSVC 2015的标准库的头文件中找到这段代码,它实现了std::atomic_thread_fence
inline void _Atomic_thread_fence(memory_order _Order)
{ /* …Run Code Online (Sandbox Code Playgroud) 为什么是std::atomic的 store:
std::atomic<int> my_atomic;
my_atomic.store(1, std::memory_order_seq_cst);
Run Code Online (Sandbox Code Playgroud)
在xchg请求具有顺序一致性的商店时执行?
从技术上讲,具有读/写内存屏障的普通商店不应该足够吗?相当于:
_ReadWriteBarrier(); // Or `asm volatile("" ::: "memory");` for gcc/clang
my_atomic.store(1, std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
我明确地谈论x86和x86_64.商店有隐含的获取围栏.
我正在检查编译器如何为x86_64上的多核内存屏障发出指令。以下代码是我正在测试的代码gcc_x86_64_8.3。
std::atomic<bool> flag {false};
int any_value {0};
void set()
{
any_value = 10;
flag.store(true, std::memory_order_release);
}
void get()
{
while (!flag.load(std::memory_order_acquire));
assert(any_value == 10);
}
int main()
{
std::thread a {set};
get();
a.join();
}
Run Code Online (Sandbox Code Playgroud)
使用时std::memory_order_seq_cst,我可以看到该MFENCE指令用于任何优化-O1, -O2, -O3。该指令确保刷新了存储缓冲区,因此在L1D缓存中更新了它们的数据(并使用MESI协议确保其他线程可以看到效果)。
但是,当我std::memory_order_release/acquire不进行优化MFENCE使用时,也会使用指令,但是使用-O1, -O2, -O3优化会忽略该指令,并且不会看到其他刷新缓冲区的指令。
在MFENCE不使用的情况下,如何确保将存储缓冲区数据提交给高速缓存以确保内存顺序语义?
以下是使用get / set函数的汇编代码-O3,例如我们在Godbolt编译器资源管理器中获得的代码:
set():
mov DWORD PTR any_value[rip], 10
mov BYTE PTR flag[rip], 1
ret
.LC0:
.string …Run Code Online (Sandbox Code Playgroud) 为什么我们需要定义两种具有相同实现的障碍?
#if defined(__x86_64) || defined(__i386__)
#define read_barrier() __asm__ __volatile__("":::"memory")
#define write_barrier() __asm__ __volatile__("":::"memory")
#else
Run Code Online (Sandbox Code Playgroud) C++ 自旋锁可以使用std::atomic_flag轻松实现,它可以粗略地编码(没有特殊功能),如下所示:
std::atomic_flag f = ATOMIC_FLAG_INIT;
while (f.test_and_set(std::memory_order_acquire)); // Acquire lock
// Here do some lock-protected work .....
f.clear(std::memory_order_release); // Release lock
Run Code Online (Sandbox Code Playgroud)
另外,正如人们在 uops.info(此处的屏幕)上看到的那样,XCHG 可能会占用30相当流行的 Skylake 上的 CPU 周期。这是相当慢的。
通过这样的程序可以测量自旋锁的整体速度。
是否可以在没有 XCHG 的情况下实现自旋锁定?主要关心的是速度,而不仅仅是使用另一条指令。
最快的自旋锁是什么?是否可以将其改为 10 个周期而不是 30 个?还有5个周期?也许是一些平均运行速度很快的概率自旋锁?
它应该以严格的方式实施,这意味着在 100% 的情况下它可以正确保护代码和数据。如果它是概率性的,那么它应该运行可能的时间,但每次运行后仍能 100% 正确地保护。
对我来说,这种自旋锁的主要目的是保护多个线程内非常小的操作,这些操作运行十几个或两个周期,因此 30 个周期的延迟太大了。当然可以说我可以使用原子或其他无锁技术来实现所有操作。但这种技术并不适用于所有情况,并且还需要花费大量工作才能在许多类和方法的庞大代码库中严格实现。因此,还需要一些通用的东西,比如常规的自旋锁。
x86 ×8
assembly ×4
c ×4
c++ ×4
gcc ×3
atomic ×2
linux-kernel ×2
stdatomic ×2
c++11 ×1
dma ×1
intel ×1
intrinsics ×1
linux ×1
lock-free ×1
memory-model ×1
performance ×1
spinlock ×1
x86-64 ×1