gpt4 book ai didi

c++ - x86(_64) 上的原子计数器和自旋锁的成本

转载 作者:IT老高 更新时间:2023-10-28 22:35:16 25 4
gpt4 key购买 nike

前言

我最近遇到了一些同步问题,这导致我访问了 spinlocksatomic counters .然后我又搜索了一下,这些是如何工作的,发现 std::memory_order和内存屏障(mfencelfencesfence)。

所以现在看来​​,我应该对自旋锁使用 acquire/release,对计数器使用 relaxed

一些引用

x86 MFENCE - Memory Fence
x86 LOCK - Assert LOCK# Signal

问题

默认情况下,这三个操作(锁定 = test_and_set,解锁 = clear,增量 = operator++ = fetch_add)的机器代码是什么(编辑:见下文) ( seq_cst )内存顺序和获取/释放/放松(按这三个操作的顺序)。 有什么区别(哪些内存屏障在哪里)以及成本(多少 CPU 周期)?

目的

我只是想知道我的旧代码 (未指定内存顺序 = 使用的 seq_cst) 到底有多糟糕,以及我是否应该创建一些从 派生的 class atomic_counter std::atomic 但使用宽松的内存排序 (以及在某些地方使用获取/释放而不是互斥锁的良好自旋锁......或使用来自boost库的东西 -到目前为止,我一直避免提升)

我的知识

到目前为止,我确实理解自旋锁保护的不仅仅是自身(但也有一些共享资源/内存),因此,必须有一些东西可以使多个线程/内核的一些内存 View 保持一致< em>(就是那些获取/释放和内存栅栏)。原子计数器只为自己而存在,只需要那个原子增量(不涉及其他内存,当我阅读它时我并不真正关心它的值,它提供信息并且可能是几个周期旧的,没问题)。有一些 LOCK 前缀和一些像 xchg 这样的指令隐含地有它。我的知识到此结束,我不知道缓存和总线的真正工作原理以及背后的原理(但我知道 现代 CPU 可以重新排序指令,并行执行它们并使用内存缓存和一些同步)。 感谢您的解释。

P.S.:我现在有旧的 32 位电脑,只能看到 lock addl 和简单的 xchg,没有别的 - 所有版本看起来都一样(解锁除外),memory_order 对我的旧 PC 没有任何影响(解锁除外,release 使用 move 而不是 xchg)。对于 64 位 PC 来说会是这样吗? (编辑:见下文)我必须关心内存顺序吗? (回答:不,不多,解锁时释放可以节省几个周期,仅此而已。)

代码:

#include <atomic>
using namespace std;

atomic_flag spinlock;
atomic<int> counter;

void inc1() {
counter++;
}
void inc2() {
counter.fetch_add(1, memory_order_relaxed);
}
void lock1() {
while(spinlock.test_and_set()) ;
}
void lock2() {
while(spinlock.test_and_set(memory_order_acquire)) ;
}
void unlock1() {
spinlock.clear();
}
void unlock2() {
spinlock.clear(memory_order_release);
}

int main() {
inc1();
inc2();
lock1();
unlock1();
lock2();
unlock2();
}

g++ -std=c++11 -O1 -S(32bit Cygwin,缩短输出)

__Z4inc1v:
__Z4inc2v:
lock addl $1, _counter ; both seq_cst and relaxed
ret
__Z5lock1v:
__Z5lock2v:
movl $1, %edx
L5:
movl %edx, %eax
xchgb _spinlock, %al ; both seq_cst and acquire
testb %al, %al
jne L5
rep ret
__Z7unlock1v:
movl $0, %eax
xchgb _spinlock, %al ; seq_cst
ret
__Z7unlock2v:
movb $0, _spinlock ; release
ret

x86_64bit 更新:(参见 unlock1 中的 mfence)

_Z4inc1v:
_Z4inc2v:
lock addl $1, counter(%rip) ; both seq_cst and relaxed
ret
_Z5lock1v:
_Z5lock2v:
movl $1, %edx
.L5:
movl %edx, %eax
xchgb spinlock(%rip), %al ; both seq_cst and acquire
testb %al, %al
jne .L5
ret
_Z7unlock1v:
movb $0, spinlock(%rip)
mfence ; seq_cst
ret
_Z7unlock2v:
movb $0, spinlock(%rip) ; release
ret

最佳答案

x86 主要有 strong memory model ,所有常见的存储/加载都隐含了释放/获取语义。只有 SSE 非临时存储操作异常(exception),需要像往常一样订购 sfence。所有带有 LOCK 的读-修改-写 (RMW) 指令前缀意味着完整的内存屏障,即 seq_cst。

因此在 x86 上,我们有

  • test_and_set 可以用 lock bts 编码(用于按位运算),lock cmpxchg , 或 lock xchg (或者只是 xchg,这意味着 lock)。其他自旋锁实现可以使用像 lock inc 这样的指令。 (或 dec)如果他们需要,例如公平。 try_lock 无法使用 release/acquire fence 来实现(至少你需要独立的内存屏障 mfence)。
  • clear 编码为 lock and (按位)或 lock xchg ,不过,更高效的实现将使用普通写入 (mov) 而不是锁定指令。
  • fetch_add 编码为 lock add .

移除 lock 前缀不会保证 RMW 操作的原子性,因此此类操作不能严格解释为在 C++ View 中具有 memory_order_relaxed。但是在实践中,您可能希望在安全的情况下(在构造函数中,处于锁定状态)通过更快的非原子操作来访问原子变量。

根据我们的经验,执行哪个 RMW 原子操作并不重要,它们执行的周期数几乎相同(mfence 大约是锁操作的 x0.5)。您可以通过计算原子操作(和 mfence)的数量以及内存间接(缓存未命中)的数量来估计同步算法的性能。

关于c++ - x86(_64) 上的原子计数器和自旋锁的成本,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25363286/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com