有人可以解释一下这个说法:
shared variables
x = 0, y = 0
Core 1 Core 2
x = 1; y = 1;
r1 = y; r2 = x;
怎么可能拥有r1 == 0
x86r2 == 0
处理器?
有人可以解释一下这个说法:
shared variables
x = 0, y = 0
Core 1 Core 2
x = 1; y = 1;
r1 = y; r2 = x;
怎么可能拥有r1 == 0
x86r2 == 0
处理器?
该问题可能是由于涉及指令重新排序的优化而出现的。换句话说,两个处理器都可以在分配变量之前r1
分配和,如果他们发现这会产生更好的性能。这可以通过添加一个内存屏障来解决,这将强制执行排序约束。r2
x
y
引用您在帖子中提到的幻灯片:
现代多核/语言打破了顺序一致性。
关于 x86 架构,最好的阅读资源是Intel® 64 and IA-32 Architectures Software Developer's Manual(第8.2 章内存订购)。8.2.1 和 8.2.2 节描述了由 Intel486、Pentium、Intel Core 2 Duo、Intel Atom、Intel Core Duo、Pentium 4、Intel Xeon 和 P6 系列处理器实现的内存排序:称为处理器排序的内存模型,如与旧 Intel386 架构的程序排序(强排序)相反(其中读取和写入指令始终按照它们在指令流中出现的顺序发出)。
该手册描述了处理器排序内存模型的许多排序保证(例如,Loads 不会与其他负载一起重新排序,Stores 不会与其他存储一起重新排序,Stores 不会与较旧的负载一起重新排序等),但它也描述了允许的重新排序规则这导致了 OP 帖子中的竞争条件:
8.2.3.4 加载可能会与早期存储重新排序到不同的位置
另一方面,如果指令的原始顺序被切换:
shared variables
x = 0, y = 0
Core 1 Core 2
r1 = y; r2 = x;
x = 1; y = 1;
r1 = 1
在这种情况下,处理器保证这种r2 = 1
情况是不允许的(由于8.2.3.3 Stores Are Not Reordered With Early Load保证),这意味着这些指令永远不会在单个内核中重新排序。
要将其与不同的架构进行比较,请查看这篇文章:现代微处理器中的内存排序。您可以看到 Itanium (IA-64) 比 IA-32 架构进行了更多的重新排序:
在内存一致性模型较弱的处理器(例如 SPARC、PowerPC、Itanium、ARM 等)上,由于在没有显式内存屏障指令的情况下写入时缺乏强制缓存一致性,可能会发生上述情况。所以基本上Core1
看到x
之前写的y
,而Core2
看到y
之前写的x
. 在这种情况下,不需要完整的栅栏指令......基本上,您只需要在这种情况下强制执行写入或释放语义,以便所有写入都已提交并对所有处理器可见,然后再对那些已被读取的变量进行读取。写给。具有强内存一致性模型(如 x86)的处理器架构通常不需要这样做,但正如 Groo 所指出的,编译器本身可以重新排序操作。您可以volatile
在 C 和 C++ 中使用关键字来防止编译器在给定线程中对操作进行重新排序。这并不是说这volatile
将创建线程安全的代码来管理线程之间读写的可见性......这将需要一个内存屏障。所以虽然使用volatile
仍然可以创建不安全的线程代码,在给定的线程中,它将在编译的机器代码级别强制执行顺序一致性。
问题是两个线程都没有强制它的两个语句之间的任何顺序,因为它们不是相互依赖的。
编译器知道x和y没有别名,因此不需要对操作进行排序。
CPU 知道x和y没有别名,因此它可能会重新排序它们以提高速度。发生这种情况的一个很好的例子是 CPU 检测到写入组合的机会。如果它可以在不违反其一致性模型的情况下将一个写入与另一个写入合并,它可能会这样做。
相互依赖看起来很奇怪,但实际上与任何其他竞争条件没有什么不同。直接编写共享内存线程代码是相当困难的,这就是为什么要开发并行语言和消息传递并行框架的原因,以便将并行危害隔离到一个小内核并从应用程序本身中消除危害。