一般地,对于int num,num++(或++num),作为读-修改-写操作中,是不是原子.但我经常看到编译器,例如GCC,为它生成以下代码(在这里尝试):
由于第5行对应于num++一条指令,我们可以得出结论,在这种情况下num++ 是原子的吗?
如果是这样,是否意味着如此生成num++可以在并发(多线程)场景中使用而没有任何数据争用的危险(例如,我们不需要制作它,std::atomic<int>并强加相关成本,因为它是无论如何原子)?
UPDATE
请注意,这个问题不是增量是否是原子的(它不是,而且是问题的开头行).它是否可以在特定场景中,即在某些情况下是否可以利用单指令性质来避免lock前缀的开销.而且,作为公认的答案约单处理器的机器,还有部分提到这个答案,在其评论和其他人谈话解释,它可以(尽管不是C或C++).
如果数据结构中包含多个元素,则它的原子版本不能(始终)无锁.我被告知这对于较大的类型是正确的,因为CPU不能在不使用某种锁的情况下以原子方式更改数据.
例如:
#include <iostream>
#include <atomic>
struct foo {
double a;
double b;
};
std::atomic<foo> var;
int main()
{
std::cout << var.is_lock_free() << std::endl;
std::cout << sizeof(foo) << std::endl;
std::cout << sizeof(var) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
输出(Linux/gcc)是:
0
16
16
Run Code Online (Sandbox Code Playgroud)
由于原子和foo大小相同,我不认为锁存储在原子中.
我的问题是:
如果一个原子变量使用一个锁,它存储在哪里,这对该变量的多个实例意味着什么?
在 C++ 中,有一种原子类型std::atomic<T>。该原子类型可能是无锁的,也可能不是,具体取决于类型 T 和当前平台。如果某个类型的无锁实现在类型 T 的平台上可用,那么大多数编译器都会提供无锁atomic<T>。在这种情况下,即使我想要非无锁atomic<T>我也无法拥有它。
C++ 标准决定只保留一个,std::atomic<T>而不是一std::atomic<T>加一std::lock_free<T>(部分针对特定类型实现)。这是否意味着“在任何情况下,当后者可用时,使用非无锁原子类型都会比使用无锁原子类型更好”?(主要是在性能方面而不是易用性方面)。
我正在尝试 C++ 原子std::atomic<T>::is_always_lock_free和std::atomic<T>::is_lock_free.
我写了一个简单的结构体A,想知道 的原子版本是否A是无锁的:
#include <iostream>
#include <atomic>
using namespace std;
struct A {
int x;
int y;
int z;
};
int main() {
atomic<A> b;
cout << boolalpha;
cout << "b.is_always_lock_free = " << b.is_always_lock_free << endl;
cout << "b.is_lock_free = " << b.is_lock_free() << endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在x86-64 Linux上,我用g++ 9.4.0和C++17编译它,输出正常:
b.is_always_lock_free = false
b.is_lock_free = false
Run Code Online (Sandbox Code Playgroud)
然而,我也在我的Mac(ARM64 )上用clang++ 16.0.0编译它,输出很奇怪:
b.is_always_lock_free = true …Run Code Online (Sandbox Code Playgroud) 8.1.2总线锁定
Intel 64和IA-32处理器提供LOCK#信号,该信号在某些关键存储器操作期间自动置位,以锁定系统总线或等效链路.当该输出信号被断言时,来自其他处理器或总线代理的用于控制总线的请求被阻止.软件可以指定在遵循LOCK语义的其他情况下将LOCK前缀添加到指令之前.
它来自英特尔手册,第3卷
听起来内存上的原子操作将直接在内存(RAM)上执行.我很困惑,因为当我分析装配输出时,我看到"没什么特别的".基本上,生成的汇编输出std::atomic<int> X; X.load()只会产生"额外"的影响.但是,它负责正确的内存排序,而不是原子性.如果我理解得X.store(2)恰到好处mov [somewhere], $2.就这样.它似乎没有"跳过"缓存.我知道将对齐(例如int)移动到内存是原子的.但是,我很困惑.
所以,我提出了疑问,但主要问题是:
考虑像一个数组atomic<int32_t> shared_array[].如果你想SIMD矢量化for(...) sum += shared_array[i].load(memory_order_relaxed)怎么办?或者在数组中搜索第一个非零元素,或者将其范围归零?这可能很少见,但考虑一下不允许在元素内撕裂的任何用例,但在元素之间重新排序很好. (也许是寻找CAS候选人的搜索).
我认为 x86对齐的向量加载/存储在实践中可以安全地用于带有mo_relaxed操作的SIMD ,因为任何撕裂只会发生在当前硬件上最坏的8B边界(因为这是自然对齐的8B访问原子1的原因).不幸的是,英特尔的手册只说:
"可以使用多个存储器访问来实现访问大于四字的数据的x87指令或SSE指令."
无法保证这些组件访问是自然对齐,不重叠或其他任何内容.(有趣的事实:根据Agner Fog,大概是qword + word,fld m80在Haswell上用2个加载uops和2个ALU uops完成x87 10字节加载.)
如果你想在面向未来的方式,当前的x86手册上说,未来所有的x86 CPU将努力向量化,你可以在8B块与加载/存储movq/ movhps.
或者你可以使用vpmaskmovd带有全真掩码的256b,因为手册的操作部分用多个独立的32位负载来定义它,比如Load_32(mem + 4).这是否意味着每个元素都作为一个单独的32位访问,保证该元素内的原子性?
(在实际硬件上,它是Haswell上的1个负载和2个端口5 uops,或者Ryzen上只有1或2个负载+ ALU uops(128/256).我认为这是针对不需要从元素中抑制异常的情况进入一个未映射的页面,因为它可能会更慢(但IDK如果它需要微代码辅助).无论如何,这告诉我们它至少与vmovdqaHaswell上的正常负载一样原子,但这告诉我们没有关于x86 Deathstation 9000 16B的信息/ 32B向量访问被分解为单字节访问,因此每个元素内可能会有撕裂.
我认为实际上可以安全地假设你不会在16,32或64位元素中撕裂任何真正的x86 CPU上的对齐矢量加载/存储,因为这对于已经有效的实现是没有意义的必须保持自然对齐的64位标量存储原子,但知道手册中的保证到底有多远是有趣的.)
收集(AVX2,AVX512)/ Scatter(AVX512)
类似vpgatherdd的指令显然由多个独立的32b或64b访问组成.AVX2表格被记录为多次执行,FETCH_32BITS(DATA_ADDR);因此可能会被通常的原子性保证所覆盖,并且如果它不跨越边界,则每个元素将以原子方式收集.
AVX512褶裥都记录在英特尔公司的PDF的insn参考手册作为
DEST[i+31:i] <- MEM[BASE_ADDR + SignExtend(VINDEX[i+31:i]) * …
我需要原子地读/写16个字节.我只使用cmpxchg16进行写入,cmpxchg16可以在所有x64处理器上使用,除了我认为对于一个不起眼的AMD处理器.
现在的问题是对齐的16字节值,只使用cmpxchg16进行修改(它就像一个完整的内存屏障)是否有可能读取一个半字节数据和一半新数据的16字节位置?
只要我用SSE指令读取(因此线程不能在读取过程中被中断),我认为读取不可能(甚至在多处理器numa系统中)看不一致的数据.我认为它必须是原子的.
我假设当执行cmpxchg16时,它会原子地修改16个字节,而不是通过编写两个8字节块,其他线程可能在其间进行读取(老实说,我不知道它是如何工作的,如果它不是原子的.)
我对吗?如果我错了,有没有办法在不诉诸锁定的情况下进行原子16字节读取?
注意:这里有几个类似的问题,但它们没有处理只用cmpxchg16进行写入的情况,所以我觉得这是一个单独的,没有答案的问题.
编辑:其实我认为我的推理是错误的.SSE加载指令可以作为两个64位读取执行,并且cmpxchg16可以在两次读取之间由另一个处理器执行.
在Java中,更新double和long变量可能不是原子变量,因为double/long被视为两个独立的32位变量.
http://java.sun.com/docs/books/jls/second_edition/html/memory.doc.html#28733
在C++中,如果我使用的是32位Intel Processor + Microsoft Visual C++编译器,那么更新双(8字节)操作原子?
我找不到关于这种行为的规范.
当我说"原子变量"时,这就是我的意思:
线程A试图将1写入变量x.线程B试图将2写入变量x.
我们将从变量x得到值1或2,但不是未定义的值.
我有一个AVX CPU(不支持AVX2),我想计算两个256位整数的按位xor.
由于_mm256_xor_si256仅在AVX2上可用,我可以将这256位加载为__m256使用_mm256_load_ps,然后执行a _mm256_xor_ps.这会产生预期的结果吗?
我主要担心的是,如果内存内容不是有效的浮点数,_mm256_load_ps那么寄存器中的位不会与寄存器中的位完全相同吗?
谢谢.