为什么 CompareAndSwap 指令被认为是昂贵的?
我在一本书上读到:
“内存屏障很昂贵,大约为 作为原子的比较昂贵() 指示。”
谢谢!
“CAS 与普通存储没有明显不同。有关 CAS 的一些错误信息可能源于 Intel 处理器上的 lock:cmpxchg (CAS) 的原始实现。lock: 前缀导致 LOCK# 信号被断言,获取对总线的独占访问权限。这当然无法扩展。lock:cmpxchg 的后续实现利用缓存一致性协议(通常基于探听的 MESI),并且不断言 LOCK#。” - David Dice,Biased锁定热点
“内存屏障很昂贵,大约与原子compareAndSet()指令一样昂贵。”
这是千真万确的。
例如。在 x86 上,多处理器系统上的正确 CAS 具有锁定前缀。
锁前缀会导致完整的内存屏障:
“...锁定操作序列化所有未完成的加载和存储操作(即等待它们完成)。” ...“锁定操作相对于所有其他内存操作和所有外部可见事件而言是原子的。只有取指和页表访问才能传递锁定指令。锁定指令可用于同步一个处理器写入的数据和另一处理器读取的数据”。 - 英特尔® 64 和 IA-32 架构软件开发人员手册,第 8.1.2 章。
内存屏障实际上是在 x86/x64 上的 .NET
和 JAVA JIT中作为虚拟
LOCK OR
或 LOCK AND
实现的。
在 x86 上,CAS 会导致完全内存屏障。
对 - lwarx
和
stwcx
- 可用于将内存操作数加载到寄存器中,然后如果没有其他存储到目标位置,则将其写回,或者重试整个过程如果有的话循环。 LL/SC 可以被中断。它也不意味着自动全围栏。 不同架构上的性能特征和行为可能非常不同。
int
的时间很容易比添加两个寄存器花费的时间长 50 倍。
因此,由于内存屏障基本上强制直接 RAM 访问(可能针对多个 CPU),因此它们相对昂贵。
每个 getAndSet() 都会广播到总线。因为 所有线程必须使用总线进行通信 内存,这些 getAndSet() 调用会延迟所有线程(核心), 即使那些不等待锁定的人。
更糟糕的是, getAndSet() 调用会强制其他 处理器丢弃自己的缓存副本 锁,所以每个旋转线程都会遇到一个 缓存几乎每次都未命中,并且必须使用 总线来获取新的但未更改的值。
主题 1
while (1) x++;
主题 2
while (1) x++;
因为增量不是原子操作或受内存屏障保护,所以其结果几乎是未定义的。你不知道 x 将如何递增,或者它甚至可能被损坏。
主题 1
while (1) atomicIncrement(&x);
主题 2
while (1) atomicIncrement(&x);
现在,您正在尝试获得明确定义的行为 - 无论顺序如何,x 都必须一一递增。如果两个线程在不同的 CPU 上运行,它们必须减少允许的缓存量,或者以其他方式“比较笔记”以确保发生合理的事情。
这种额外的开销可能非常昂贵,并且这是原子操作缓慢的主要原因。