我读过this,我的问题非常相似但有些不同。
注意,我知道C ++ 0x并不能保证这一点,但我特别要求像x86-64这样的多核机器。
假设我们有2个线程(固定到2个物理内核)运行以下代码:
// I know people may delcare volatile useless, but here I do NOT care memory reordering nor synchronization/
// I just want to suppress complier optimization of using register.
volatile int n;
void thread1() {
for (;;)
n = 0xABCD1234;
// NOTE, I know ++n is not atomic,
// but I do NOT care here.
// what I cares is whether n can be 0x00001234, i.e. in the middle of the update from core-1's cache lines to main memory,
// will core-2 see an incomplete value(like the first 2 bytes lost)?
++n;
}
}
void thread2() {
while (true) {
printf('%d', n);
}
}
线程2是否有可能看到n
类似0x00001234,即在从core-1的缓存行更新到主内存的过程中,core-2会看到一个不完整的值?
我知道单个4字节int
绝对适合通常长度为128字节的缓存行,如果int
确实存储在一个缓存行中,那么我相信这里没有问题。但是,如果它超越缓存线边界呢?也就是说,某些char
是否已经位于该缓存行内,这使得n
的第一部分位于一个缓存行而另一部分位于下一行?如果是这种情况,那么core-2可能有机会看到一个不完整的值,对吗?
另外,我认为除非将每个char
或short
或其他less-than-4-bytes
类型填充为4字节长,否则永远无法保证单个int
没有通过缓存行边界,不是吗?
如果是这样,那么在x86-64多核机器上,即使设置单个int
通常也不保证是原子的吗?
我得到了这个问题,因为当我研究这个主题时,不同帖子中的各个人似乎都同意这一点,只要机器架构是正确的(例如x86-64),设置int
应该是原子的。但正如我上面提到的那样,不成立,对吗?
我想提出一些问题的背景知识。我正在处理一个实时系统,它正在对一些信号进行采样并将结果放入一个全局int中,这当然是在一个线程中完成的。在另一个线程中,我读取了这个值并处理它。 我不关心set和get的排序,我只需要一个完整的(相对于一个被破坏的整数值)值。
答案 0 :(得分:6)
x86保证这一点。 C ++没有。如果你写x86程序集你会没事的。如果你编写C ++,那就是未定义的行为。既然你无法解释未定义的行为(毕竟它是未定义的),你必须降低并查看生成的汇编程序指令。如果他们做你想做的事情那么这很好。但请注意,当您更改编译器,编译器版本,编译器标志或可能更改优化程序行为的任何代码时,编译器会更改生成的程序集,因此您将不断检查汇编程序代码以确保它是仍然是正确的。
更简单的方法是使用std::atomic<int>
,这将保证生成正确的汇编程序指令,因此您不必经常检查。
答案 1 :(得分:5)
为什么这么担心?
依靠您的实施。如果std::atomic<int>
在您的平台上是原子的,那么int
将减少为int
(如果正确对齐,则在x86-64中为{}}。
如果我是你,我也会担心int
溢出你的代码(这是未定义的行为)的可能性。
换句话说,std::atomic<unsigned>
是适当的类型。
答案 2 :(得分:4)
另一个问题涉及变量&#34;正确对齐&#34;。如果它跨越缓存行,则变量不正确对齐。除非您特别要求编译器打包结构,否则int
将不会这样做。
您还假设使用volatile int
优于atomic<int>
。如果volatile int
是在您的平台上同步变量的完美方式,那么库实现者肯定也知道并在volatile x
内存储atomic<x>
。
没有要求atomic<int>
因为标准而必须特别慢。 : - )
答案 3 :(得分:3)
如果您正在寻找原子性保证,std::atomic<>
是您的朋友。不要依赖volatile
限定词。
答案 4 :(得分:2)
问题几乎与Why is integer assignment on a naturally aligned variable atomic on x86?重复。那里的答案确实回答了你所要求的一切,但这个问题更侧重于是否 int
(或其他类型?)的ABI /编译器问题是否足够对齐,而不是会发生什么事。此问题还有其他一些内容值得回答。
是的,它们几乎总是在int
适合单个寄存器的机器上(例如不是AVR:8位RISC),因为编译器通常选择在使用时不使用多个存储指令1。
正常的x86 ABI将int
与4B边界对齐,甚至在结构内部(除非你使用GNU C __attribute__((packed))
或其他方言的等价物)。但请注意,i386 System V ABI仅将double
与4个字节对齐;现代编译器可以超越它并使其自然对齐,making load/store atomic。
但是你在C ++中合法做的事情都不能依赖于这个事实(因为根据定义,它将涉及非atomic
类型的数据竞争,所以它是未定义的行为)。幸运的是,有一些有效的方法可以获得相同的结果(即,相同的编译器生成的asm,没有mfence
指令或其他缓慢的东西)不会导致未定义的行为。
您应该使用atomic
而不是volatile
,或者希望编译器不会在非易失性int
上优化存储或加载,因为异步修改的假设是volatile
和atomic
重叠的方式之一。
<{>我正在处理一个实时系统,它正在对一些信号进行采样并将结果放入一个全局int中,这当然是在一个线程中完成的。在另一个线程中,我读取了这个值并进行处理。
std::atomic
.store(val, std::memory_order_relaxed)
和.load(std::memory_order_relaxed)
将在此提供您想要的内容。 HW-access线程可以自由运行,并将普通的x86存储指令放入共享变量中,而读取器线程则执行普通的x86加载指令。
这是C ++ 11表达这是你想要的方式,你应该期望它编译成与volatile
相同的asm。 (如果使用clang,可能会有一些指令差异,但没有什么重要。)如果volatile int
没有足够的对齐或任何其他角落情况,atomic<int>
将起作用(除非编译器错误)。除了可能在一个打包的结构中; IDK,如果编译器通过在结构中打包原子类型来阻止你破坏原子性。
理论上,您可能希望使用volatile std::atomic<int>
来确保编译器不会将多个存储优化为同一个变量。见Why don't compilers merge redundant std::atomic writes?。但就目前而言,编译器并没有做那种优化。 (volatile std::atomic<int>
仍应编译为相同的轻量级asm。)
我知道一个4字节的int肯定适合一个典型的128字节长的缓存行,如果那个int存储在一个缓存行中,那么我相信这里没有问题......
自PentiumIII以来,所有主流x86 CPU的缓存行均为64B;在那之前32B线是典型的。 (好AMD Geode still uses 32B lines ...)Pentium4使用64B线,虽然它更喜欢成对传输它们?尽管如此,我认为它确实使用64B线而不是128B是准确的。 This page将其列为每行64B。
AFAIK,没有x86微体系结构在任何级别的缓存中使用128B线。
此外,只有英特尔CPU保证缓存的未对齐存储/加载是非原子的,如果它们不跨越缓存行边界。 x86(AMD / Intel / other)的基线原子性保证不会超过8字节边界。有关Intel / AMD手册中的引用,请参阅Why is integer assignment on a naturally aligned variable atomic on x86?。
自然对齐适用于几乎任何ISA(不仅仅是x86),最大保证原子宽度。
您的问题中的代码需要非原子读取 - 修改写入,其中加载和存储是单独原子的,并且不对周围的加载/存储施加任何排序。
正如大家所说的那样,正确的方法是使用atomic<int>
,但没有人指出如何。如果您只是n++
atomic_int n
,您将获得(对于x86-64)lock add [n], 1
,这将比您使用volatile
慢得多,因为它会使整个RMW操作原子。 (也许这就是为什么你要避免std::atomic<>
?)
#include <atomic>
volatile int vcount;
std::atomic <int> acount;
static_assert(alignof(vcount) == sizeof(vcount), "under-aligned volatile counter");
void inc_volatile() {
while(1) vcount++;
}
void inc_separately_atomic() {
while(1) {
int t = acount.load(std::memory_order_relaxed);
t++;
acount.store(t, std::memory_order_relaxed);
}
}
来自the Godbolt compiler explorer with gcc7.2 and clang5.0
的asm输出不出所料,他们都使用gcc / clang为x86-32和x86-64编译为等效的asm。除了要递增的地址外,gcc对两者都使用相同的asm:
# x86-64 gcc -O3
inc_volatile:
.L2:
mov eax, DWORD PTR vcount[rip]
add eax, 1
mov DWORD PTR vcount[rip], eax
jmp .L2
inc_separately_atomic():
.L5:
mov eax, DWORD PTR acount[rip]
add eax, 1
mov DWORD PTR acount[rip], eax
jmp .L5
clang优化得更好,并使用
inc_separately_atomic():
.LBB1_1:
add dword ptr [rip + acount], 1
jmp .LBB1_1
注意缺少lock
前缀,因此在CPU内部解码为单独加载,ALU添加和存储uops。 (见Can num++ be atomic for 'int num'?)。
除了较小的代码大小外,其中一些uop在来自同一指令时可以进行微融合,从而减少前端瓶颈。 (这里完全不相关;存储/重新加载的5或6周期延迟的循环瓶颈。但如果用作更大循环的一部分,它将是相关的。)与寄存器操作数不同,add [mem], 1
更好比英特尔CPU上的inc [mem]
要好,因为它更加微融合:INC instruction vs ADD 1: Does it matter?。
有趣的是,clang对inc dword ptr [rip + vcount]
使用效率较低的inc_volatile()
。
实际的原子RMW是如何编译的?
void inc_atomic_rmw() {
while(1) acount++;
}
# both gcc and clang do this:
.L7:
lock add DWORD PTR acount[rip], 1
jmp .L7
结构内部的对齐:
#include <stdint.h>
struct foo {
int a;
volatile double vdouble;
};
// will fail with -m32, in the SysV ABI.
static_assert(alignof(foo) == sizeof(double), "under-aligned volatile counter");
但atomic<double>
或atomic<unsigned long long>
将保证原子性。
对于32位计算机上的64位整数加载/存储,gcc使用SSE2指令。不幸的是,其他一些编译器使用lock cmpxchg8b
,这对于单独的存储或加载来说效率要低得多。 volatile long long
不会给你那个。
volatile double
通常通常是原子加载/存储,因为正常的方法是使用单个8B加载/存储指令。