内存屏障和锁前缀指令之间的区别
difference between Memory Barriers and lock prefixed instruction
在这篇文章 Memory Barriers and JVM Concurrency! 中,我了解到 volatile 是由不同的内存屏障指令实现的,而 synchronized 和 atomic 是由 lock 前缀指令实现的。但是我在其他文章中得到了以下代码:
java代码:
volatile Singleton instance = new Singleton();
汇编指令(x86):
0x01a3de1d: movb [=11=]x0,0x1104800(%esi);
0x01a3de24: lock addl [=12=]x0,(%esp);
所以哪一个是正确的?在不考虑我糟糕的英语的情况下,Memory Barriers 和 lock 前缀指令有什么区别?
简答
锁定指令用于自动执行复杂的内存指令。
内存屏障用于排序(部分或全部)内存访问。
Java易变
Java volatile
关键字保证对 volatile 变量的更改可见
所有 个线程,因为它们被写在程序中。 volatile 的全部且唯一的一点是
对 volatile 变量的访问是完全有序的,所以如果你访问变量 X
然后
变量 Y
,都是易变的,对 X 的访问先于对 Y 的访问
处理器!
这需要对内存访问进行排序,因此需要内存屏障。
IA32e 上的内存屏障可以使用“fences”指令(mfence, lfence,
sfence
)或 lock
指令来实现。但后一种选择只是一个方面
lock
的影响,而不是其主要用途。
锁定指令被序列化等
有总订单。这仅对内存访问进行排序是低效的,但有效并且被使用
在缺少“围栏”的旧处理器中。
所以你看到的锁实际上是一个屏障(Linux内核也使用了相同的指令)。
完整答案
上面的“复杂内存指令”是指读取-修改-写入指令(以英特尔命名),这些
是内部包含三个操作的指令:
从内存中获取值,更改它并存储回去。
如果在指令期间总线未保持锁定,则另一个处理器可以更改该值
after 它已从内存中读取但 before 它存储回来。
例子
x = 0
CPU 1 CPU 2
loop: loop:
inc [X] inc [x]
j loop j loop
如果每个 CPU 执行自己的循环 10 次,x 中将存储什么值?
你不能用确定性的方式来判断。伪指令inc [X]
必须用三个微操作来实现
作为
CPU 1 CPU 2
loop: loop:
mov r, [X] mov r, [X]
inc r inc r
mov [x], r mov [x], r
j loop j loop
可能出现过这种情况:
CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0
CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0
CPU2: mov r, [X] X is 0, CPU1 r is 1, CPU2 r is 0
CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0
CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0
CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0
CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0
CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 1
CPU2: mov [X], r X is 1, CPU1 r is 2, CPU2 r is 1
注意 X 是 1 而不是 3。
通过锁定 inc
指令,CPU 将系统总线锁定为 inc
开始,直到它退休。这会强制采用这样的模式(示例)
CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0, CPU2 cannot use bus
CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus
CPU2: mov r, [X] X is 2, CPU1 r is 1, CPU2 r is 2, CPU1 cannot use bus
CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus
CPU2: mov [X], r X is 3, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus
改为使用内存屏障来排序内存访问。
处理器执行指令输出
顺序,这意味着即使您发送 CPU 指令 A B C
它也可以
执行 C A B
.
但是,处理器需要遵守相关性和指令
仅当这不会改变程序行为时才会乱序执行。
要记住的一个非常重要的方面是指令执行和指令之间的区别
退役是因为处理器只对退役指令保持其架构状态(程序可以看到的状态)一致。
正常情况下,程序只在指令退出时才能看到指令的结果,即使它已经被执行了!但是对于内存访问,情况略有不同,因为它们具有修改主内存的全局可见副作用,并且无法撤消!
因此,从 CPU 上的程序来看,CPU 的所有内存访问都按程序顺序进行,但是处理器没有
努力保证 other 处理器以相同的顺序查看内存访问!他们看到 执行 顺序或最坏的传播顺序由于缓存层次结构和内存拓扑!
不同处理器的内存访问顺序不同。
因此 CPU 允许程序员控制内存访问的排序方式,屏障会阻止其他内存指令(在同一个 CPU 上)被执行,直到所有前一个指令
是 executed/retired/propagated(这取决于架构的屏障类型)。
例子
x = 0, y = 0
CPU 1 CPU2
mov [x], 1 loop:
mov [y], 1 mov r, [y]
jrz loop ;Jump if r is 0
mov s, [x]
不需要锁。但是,如果没有障碍,程序后 CPU2 s
可能为 0。
这是因为 CPU1 的 mov [y], 1
写入可以重新排序和执行
在写入 x!
之前
从 CPU 1 的角度来看没有任何变化,但是对于 CPU 2 来说顺序已经改变了!
有障碍物
x = 0, y = 0
CPU 1 CPU2
mov [x], 1 loop:
sync mov r, [y]
mov [y], 1 jrz loop ;Jump if r is 0
mov s, [x]
使用sync
作为内存屏障伪指令。现在不能写入 y
重新排序并且必须等待 x
的写入对 CPU2.
可见
事情比我这张简单的图片复杂一点,不同的处理器
有不同种类的障碍和内存排序。不同的体系结构具有不同的 cache/memory 拓扑,需要特殊处理。
抽象这并不容易,Java 有一个简单的内存模型,它使生成的代码更
复杂,C++11 有一个更精细的内存模型,可以让你更好地探索内存的影响
障碍。
在阅读像 happens-before 这样的抽象符号之前,在 Google 上搜索是很有用的
常见架构(IA32e、IA64、ARM、SPARC、Power、Alpha)的内存排序问题,因此
你可以看到真正的问题是什么以及如何解决。
并且 IA32e 架构是一个糟糕的测试架构,因为它的松散内存顺序确实非常强大,并且大多数问题都不会在该架构上发生。如果你有一个多处理器 phone 你可以在 ARM 上测试。如果您喜欢一个极端的例子,请使用 Alpha 架构,其中甚至对依赖访问进行重新排序!
在这篇文章 Memory Barriers and JVM Concurrency! 中,我了解到 volatile 是由不同的内存屏障指令实现的,而 synchronized 和 atomic 是由 lock 前缀指令实现的。但是我在其他文章中得到了以下代码:
java代码:
volatile Singleton instance = new Singleton();
汇编指令(x86):
0x01a3de1d: movb [=11=]x0,0x1104800(%esi);
0x01a3de24: lock addl [=12=]x0,(%esp);
所以哪一个是正确的?在不考虑我糟糕的英语的情况下,Memory Barriers 和 lock 前缀指令有什么区别?
简答
锁定指令用于自动执行复杂的内存指令。
内存屏障用于排序(部分或全部)内存访问。
Java易变
Java volatile
关键字保证对 volatile 变量的更改可见
所有 个线程,因为它们被写在程序中。 volatile 的全部且唯一的一点是
对 volatile 变量的访问是完全有序的,所以如果你访问变量 X
然后
变量 Y
,都是易变的,对 X 的访问先于对 Y 的访问
处理器!
这需要对内存访问进行排序,因此需要内存屏障。
IA32e 上的内存屏障可以使用“fences”指令(mfence, lfence,
sfence
)或 lock
指令来实现。但后一种选择只是一个方面
lock
的影响,而不是其主要用途。
锁定指令被序列化等
有总订单。这仅对内存访问进行排序是低效的,但有效并且被使用
在缺少“围栏”的旧处理器中。
所以你看到的锁实际上是一个屏障(Linux内核也使用了相同的指令)。
完整答案
上面的“复杂内存指令”是指读取-修改-写入指令(以英特尔命名),这些 是内部包含三个操作的指令: 从内存中获取值,更改它并存储回去。
如果在指令期间总线未保持锁定,则另一个处理器可以更改该值 after 它已从内存中读取但 before 它存储回来。
例子 x = 0
CPU 1 CPU 2
loop: loop:
inc [X] inc [x]
j loop j loop
如果每个 CPU 执行自己的循环 10 次,x 中将存储什么值?
你不能用确定性的方式来判断。伪指令inc [X]
必须用三个微操作来实现
作为
CPU 1 CPU 2
loop: loop:
mov r, [X] mov r, [X]
inc r inc r
mov [x], r mov [x], r
j loop j loop
可能出现过这种情况:
CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0
CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0
CPU2: mov r, [X] X is 0, CPU1 r is 1, CPU2 r is 0
CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0
CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0
CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0
CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0
CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 1
CPU2: mov [X], r X is 1, CPU1 r is 2, CPU2 r is 1
注意 X 是 1 而不是 3。
通过锁定 inc
指令,CPU 将系统总线锁定为 inc
开始,直到它退休。这会强制采用这样的模式(示例)
CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0, CPU2 cannot use bus
CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus
CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus
CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus
CPU2: mov r, [X] X is 2, CPU1 r is 1, CPU2 r is 2, CPU1 cannot use bus
CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus
CPU2: mov [X], r X is 3, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus
改为使用内存屏障来排序内存访问。
处理器执行指令输出
顺序,这意味着即使您发送 CPU 指令 A B C
它也可以
执行 C A B
.
但是,处理器需要遵守相关性和指令
仅当这不会改变程序行为时才会乱序执行。
要记住的一个非常重要的方面是指令执行和指令之间的区别
退役是因为处理器只对退役指令保持其架构状态(程序可以看到的状态)一致。
正常情况下,程序只在指令退出时才能看到指令的结果,即使它已经被执行了!但是对于内存访问,情况略有不同,因为它们具有修改主内存的全局可见副作用,并且无法撤消!
因此,从 CPU 上的程序来看,CPU 的所有内存访问都按程序顺序进行,但是处理器没有 努力保证 other 处理器以相同的顺序查看内存访问!他们看到 执行 顺序或最坏的传播顺序由于缓存层次结构和内存拓扑! 不同处理器的内存访问顺序不同。
因此 CPU 允许程序员控制内存访问的排序方式,屏障会阻止其他内存指令(在同一个 CPU 上)被执行,直到所有前一个指令 是 executed/retired/propagated(这取决于架构的屏障类型)。
例子
x = 0, y = 0
CPU 1 CPU2
mov [x], 1 loop:
mov [y], 1 mov r, [y]
jrz loop ;Jump if r is 0
mov s, [x]
不需要锁。但是,如果没有障碍,程序后 CPU2 s
可能为 0。
这是因为 CPU1 的 mov [y], 1
写入可以重新排序和执行
在写入 x!
之前
从 CPU 1 的角度来看没有任何变化,但是对于 CPU 2 来说顺序已经改变了!
有障碍物
x = 0, y = 0
CPU 1 CPU2
mov [x], 1 loop:
sync mov r, [y]
mov [y], 1 jrz loop ;Jump if r is 0
mov s, [x]
使用sync
作为内存屏障伪指令。现在不能写入 y
重新排序并且必须等待 x
的写入对 CPU2.
事情比我这张简单的图片复杂一点,不同的处理器 有不同种类的障碍和内存排序。不同的体系结构具有不同的 cache/memory 拓扑,需要特殊处理。 抽象这并不容易,Java 有一个简单的内存模型,它使生成的代码更 复杂,C++11 有一个更精细的内存模型,可以让你更好地探索内存的影响 障碍。
在阅读像 happens-before 这样的抽象符号之前,在 Google 上搜索是很有用的 常见架构(IA32e、IA64、ARM、SPARC、Power、Alpha)的内存排序问题,因此 你可以看到真正的问题是什么以及如何解决。
并且 IA32e 架构是一个糟糕的测试架构,因为它的松散内存顺序确实非常强大,并且大多数问题都不会在该架构上发生。如果你有一个多处理器 phone 你可以在 ARM 上测试。如果您喜欢一个极端的例子,请使用 Alpha 架构,其中甚至对依赖访问进行重新排序!