Total Store Order 对计划的影响
Total Store Order impact on the program
我在阅读内存一致性模型资料时遇到了两个程序示例。不知道我的理解是否正确以及下划线的原因。
一般问题是:
函数调用use()中的数据可以当成0吗?
节目 1
int data = 0, ready = 0;
void p1 (void *ignored) {
data = 2000;
ready = 1;
}
void p2 (void *ignored) {
while (!ready)
;
use (data);
}
我认为 data 在 p2() 中使用时必须为 2000,因为 data 和 ready 在 p1() 中具有存储顺序。
计划 2
int a = 0, b = 0;
void p1 (void *ignored) { a = 1; }
void p2 (void *ignored) {
if (a == 1)
b = 1;
}
void p3 (void *ignored) {
if (b == 1)
use (a);
}
我认为a必须在p3()中用作1,因为在p3()中,a不会被使用,除非b == 1;在 p2() 中,除非 a == 1,否则 b 不会被存储。因此当在 p2 中使用 a 时,a 必须为 1。
我的理解对吗?
我正在考虑配备 3 级高速缓存的 Intel Haswell 处理器。让我们考虑两种情况:NUMA 和 UMA。
是的,我可以创建一个多线程程序来测试它,但我更愿意从理论上了解它为什么有效以及为什么无效的原理,这样我才能了解事实背后的秘密。 :-D
[另一个答案]
如果我们考虑 Intel 处理器中的读取预取和缓存一致性模型,一个线程可能会在数据在另一个内核上存储为 1 并通过缓存控制器标记为无效之前从其私有缓存中预取变量 a。在这种情况下,两个程序都可以使用变量数据作为1。在UMA和NUMA模型下可能是相同的情况。
非常感谢您的帮助!
Program 1
如果这是文字 C,而不是伪代码,那么:
- The compiler is free to reorder the stores in
p1
at compile time.
- 编译器可以自由地提升
while (!ready);
的负载,因为 ready
不是 volatile
。 (因此循环总是运行零次或无限次。)通常的方法是使用 C11 atomic_load_explicit(&ready, memory_order_acquire)
。 (在 x86 上,每个加载都是一个加载获取,所以它是免费的。以这种方式编写而不只是 *(volatile int*)&ready
将使您的代码可移植到任何 C11 实现,无论体系结构如何。)
您错误地认为针对强序 ISA 的 C 实现在源代码级别具有强序。 C 程序以 C 抽象机为目标。编译器使产生结果的可执行代码 就好像 它实际上是 运行 抽象机上的 C 源代码,具有抽象机的内存排序规则。请参阅上一段中 Jeff Preshing 的博客 link。
Program 2
只要 p3
中的负载是获取负载,那么是的,您的推理是合理的。 (在 x86 上,这是免费发生的,并且对于这样的代码,编译时推测重新排序不太可能产生行为不同的代码。但这是可能的:值推测通常是允许的。)
我不确定 p2
中的 b=1
商店是否需要成为发布商店。我想是的,否则在一个弱排序的系统上,它可能会在找到 a==1
的负载之前变得全局可见。 (同样,这在 x86 上是免费的。)
I'm considering Intel Haswell processor with 3 Level of cache. Let's consider two situations: NUMA and UMA.
NUMA 不影响 ISA 的顺序保证。它可能使重新排序更有可能,或者可能以在现有单核 CPU 上实际上不会发生的方式进行。 (尽管请注意,超线程是一种 NUMA,因为共享同一逻辑核心的线程看到彼此的内存访问比其他核心快得多)。
在 NUMA 系统上崩溃的代码在任何系统上都是错误的,句号,不应被信任。
如果您正在编写新代码,请使用 C11 原子。您需要一些东西来防止编译时重新排序/提升,而 C11 stdatomic 或等效的 C++11 std::atomic
是实现此目的的现代方法。
您的代码不仅会避免编译器障碍的任何特定于编译器的内容(以防止重新排序),而且您的代码将根据它实际依赖的内存排序要求进行自我记录。它甚至可以移植到 ARM 或任何其他架构,因为它明确地在需要的地方使用获取加载,并在需要的地方使用释放存储。
不过,原子类型的默认排序是 memory_order_seq_cst
,因此您通常需要包含存储的函数的显式排序版本,以阻止它们在您执行完整内存屏障时发出指令不需要它(mfence
on x86)。 x86 原子读-修改-写总是必须使用 lock
前缀,所以在 x86 上,比 mo_seq_cst
更弱的排序没有任何好处,但使用使你的算法最弱的排序并没有坏处正确的。 (除了你不能在 x86 硬件上测试看你是否使用了太弱的顺序)。
例如my_var = 1
将编译为 mov [my_var], 1
/ mfence
,所以你必须使用 atomic_store_explicit( &my_var, 1, memory_order_release )
才能得到它 compile to just a normal x86 store.
例如见
我在阅读内存一致性模型资料时遇到了两个程序示例。不知道我的理解是否正确以及下划线的原因。
一般问题是: 函数调用use()中的数据可以当成0吗?
节目 1
int data = 0, ready = 0;
void p1 (void *ignored) {
data = 2000;
ready = 1;
}
void p2 (void *ignored) {
while (!ready)
;
use (data);
}
我认为 data 在 p2() 中使用时必须为 2000,因为 data 和 ready 在 p1() 中具有存储顺序。
计划 2
int a = 0, b = 0;
void p1 (void *ignored) { a = 1; }
void p2 (void *ignored) {
if (a == 1)
b = 1;
}
void p3 (void *ignored) {
if (b == 1)
use (a);
}
我认为a必须在p3()中用作1,因为在p3()中,a不会被使用,除非b == 1;在 p2() 中,除非 a == 1,否则 b 不会被存储。因此当在 p2 中使用 a 时,a 必须为 1。
我的理解对吗?
我正在考虑配备 3 级高速缓存的 Intel Haswell 处理器。让我们考虑两种情况:NUMA 和 UMA。
是的,我可以创建一个多线程程序来测试它,但我更愿意从理论上了解它为什么有效以及为什么无效的原理,这样我才能了解事实背后的秘密。 :-D
[另一个答案] 如果我们考虑 Intel 处理器中的读取预取和缓存一致性模型,一个线程可能会在数据在另一个内核上存储为 1 并通过缓存控制器标记为无效之前从其私有缓存中预取变量 a。在这种情况下,两个程序都可以使用变量数据作为1。在UMA和NUMA模型下可能是相同的情况。
非常感谢您的帮助!
Program 1
如果这是文字 C,而不是伪代码,那么:
- The compiler is free to reorder the stores in
p1
at compile time. - 编译器可以自由地提升
while (!ready);
的负载,因为ready
不是volatile
。 (因此循环总是运行零次或无限次。)通常的方法是使用 C11atomic_load_explicit(&ready, memory_order_acquire)
。 (在 x86 上,每个加载都是一个加载获取,所以它是免费的。以这种方式编写而不只是*(volatile int*)&ready
将使您的代码可移植到任何 C11 实现,无论体系结构如何。)
您错误地认为针对强序 ISA 的 C 实现在源代码级别具有强序。 C 程序以 C 抽象机为目标。编译器使产生结果的可执行代码 就好像 它实际上是 运行 抽象机上的 C 源代码,具有抽象机的内存排序规则。请参阅上一段中 Jeff Preshing 的博客 link。
Program 2
只要 p3
中的负载是获取负载,那么是的,您的推理是合理的。 (在 x86 上,这是免费发生的,并且对于这样的代码,编译时推测重新排序不太可能产生行为不同的代码。但这是可能的:值推测通常是允许的。)
我不确定 p2
中的 b=1
商店是否需要成为发布商店。我想是的,否则在一个弱排序的系统上,它可能会在找到 a==1
的负载之前变得全局可见。 (同样,这在 x86 上是免费的。)
I'm considering Intel Haswell processor with 3 Level of cache. Let's consider two situations: NUMA and UMA.
NUMA 不影响 ISA 的顺序保证。它可能使重新排序更有可能,或者可能以在现有单核 CPU 上实际上不会发生的方式进行。 (尽管请注意,超线程是一种 NUMA,因为共享同一逻辑核心的线程看到彼此的内存访问比其他核心快得多)。
在 NUMA 系统上崩溃的代码在任何系统上都是错误的,句号,不应被信任。
如果您正在编写新代码,请使用 C11 原子。您需要一些东西来防止编译时重新排序/提升,而 C11 stdatomic 或等效的 C++11 std::atomic
是实现此目的的现代方法。
您的代码不仅会避免编译器障碍的任何特定于编译器的内容(以防止重新排序),而且您的代码将根据它实际依赖的内存排序要求进行自我记录。它甚至可以移植到 ARM 或任何其他架构,因为它明确地在需要的地方使用获取加载,并在需要的地方使用释放存储。
不过,原子类型的默认排序是 memory_order_seq_cst
,因此您通常需要包含存储的函数的显式排序版本,以阻止它们在您执行完整内存屏障时发出指令不需要它(mfence
on x86)。 x86 原子读-修改-写总是必须使用 lock
前缀,所以在 x86 上,比 mo_seq_cst
更弱的排序没有任何好处,但使用使你的算法最弱的排序并没有坏处正确的。 (除了你不能在 x86 硬件上测试看你是否使用了太弱的顺序)。
例如my_var = 1
将编译为 mov [my_var], 1
/ mfence
,所以你必须使用 atomic_store_explicit( &my_var, 1, memory_order_release )
才能得到它 compile to just a normal x86 store.
例如见