对共享变量的内联 asm 访问算作 C++11 中的数据竞争吗?
Does inline asm access to shared variables count as a data race in C++11?
#include <thread>
int i;
int main()
{
std::thread t1([&i]() {
asm volatile("movl , %0"
: "=m"(i));
});
std::thread t2([&i]() {
asm volatile("movl , %0"
: "=m"(i));
});
t2.join();
t1.join();
return 0;
}
这是 C++ 中的数据竞争(结果是 UB)吗?让我们假设 i
的地址是对齐的,并且在我们的 CPU load/store 上对齐的双字是原子的。
很可能,它满足数据竞争的定义。
出于显而易见的原因,该标准保留内联 asm 实现定义。要对此发表任何看法,我们必须编造一些东西并挥舞一下。
更重要的是,我们不再考虑纯 ISO C++,而是 C++ 的 GNU 方言,它定义了许多 ISO C++ 未定义的行为。例如the gcc manual says type-punning by writing one union member and reading another is well-defined in GNU C++, even though it's UB in ISO C++. A lot of things are still UB in GNU C++, and "whatever g++ actually does" doesn't count as a definition. See the manual's implementation-defined behaviour section in the table of contents.
gcc 的 C++ -> asm 阶段甚至不理解 inline-asm 语句中的指令,它只是填充操作数并将其传递给汇编程序。它不 "think about" 指令在做什么;它将其视为由输出、输入和破坏约束描述的黑盒。
由于您使用了 "=m"(i)
输出操作数,您的 asm 语句 确实 以 C++ 方式与 C++ 变量交互(而不是在编译器的背后asm("mov , i");
)。我认为编译器将其视为 i = __builtin_my_asm_statement();
之类的东西。加上防止编译时重新排序/提升/死代码消除的 volatile
关键字。
并非每个数据争用都是导致数据争用的 C++ 标准未定义行为。例如,如果 i
是 std::atomic 类型,由于 race condition,i
的最终值仍然不确定。 (不过,该程序将没有 C++ UB,并且 i
将是 2 或 1,没有撕裂。当然,未定义的行为在技术上意味着任何事情都可能发生。)
那么,关于这段代码我们能说些什么:
- 我们可以假设
i
是自然对齐的,因为所有常用的 x86 ABI 都保证了这一点。
- 我们知道 asm 会将存储作为一条指令包含在内存中,而不是一次复制一个字节。 (无论如何,任何理智的编译器都不会这样做)。
- 我不是 100%确定我们可以保证store直接进入
i
的共享值,而不是从头开始space 在编译器将从中复制的堆栈上。这样做的邪恶编译器会破坏 很多 代码,包括任何像 Linux 内核这样使用内联 asm 到 运行 lock
ed 指令的东西在使用内存操作数的共享变量上。
因此,如果我们可以假设一个非恶意编译器,我们就可以非常确定编译器输出将做什么。或者 "=m"
操作数对共享值的行为应该被认为是 GNU C 中明确定义的行为,所以我们可以说这是明确定义的。
#include <thread>
int i;
int main()
{
std::thread t1([&i]() {
asm volatile("movl , %0"
: "=m"(i));
});
std::thread t2([&i]() {
asm volatile("movl , %0"
: "=m"(i));
});
t2.join();
t1.join();
return 0;
}
这是 C++ 中的数据竞争(结果是 UB)吗?让我们假设 i
的地址是对齐的,并且在我们的 CPU load/store 上对齐的双字是原子的。
很可能,它满足数据竞争的定义。
出于显而易见的原因,该标准保留内联 asm 实现定义。要对此发表任何看法,我们必须编造一些东西并挥舞一下。
更重要的是,我们不再考虑纯 ISO C++,而是 C++ 的 GNU 方言,它定义了许多 ISO C++ 未定义的行为。例如the gcc manual says type-punning by writing one union member and reading another is well-defined in GNU C++, even though it's UB in ISO C++. A lot of things are still UB in GNU C++, and "whatever g++ actually does" doesn't count as a definition. See the manual's implementation-defined behaviour section in the table of contents.
gcc 的 C++ -> asm 阶段甚至不理解 inline-asm 语句中的指令,它只是填充操作数并将其传递给汇编程序。它不 "think about" 指令在做什么;它将其视为由输出、输入和破坏约束描述的黑盒。
由于您使用了 "=m"(i)
输出操作数,您的 asm 语句 确实 以 C++ 方式与 C++ 变量交互(而不是在编译器的背后asm("mov , i");
)。我认为编译器将其视为 i = __builtin_my_asm_statement();
之类的东西。加上防止编译时重新排序/提升/死代码消除的 volatile
关键字。
并非每个数据争用都是导致数据争用的 C++ 标准未定义行为。例如,如果 i
是 std::atomic 类型,由于 race condition,i
的最终值仍然不确定。 (不过,该程序将没有 C++ UB,并且 i
将是 2 或 1,没有撕裂。当然,未定义的行为在技术上意味着任何事情都可能发生。)
那么,关于这段代码我们能说些什么:
- 我们可以假设
i
是自然对齐的,因为所有常用的 x86 ABI 都保证了这一点。 - 我们知道 asm 会将存储作为一条指令包含在内存中,而不是一次复制一个字节。 (无论如何,任何理智的编译器都不会这样做)。
- 我不是 100%确定我们可以保证store直接进入
i
的共享值,而不是从头开始space 在编译器将从中复制的堆栈上。这样做的邪恶编译器会破坏 很多 代码,包括任何像 Linux 内核这样使用内联 asm 到 运行lock
ed 指令的东西在使用内存操作数的共享变量上。
因此,如果我们可以假设一个非恶意编译器,我们就可以非常确定编译器输出将做什么。或者 "=m"
操作数对共享值的行为应该被认为是 GNU C 中明确定义的行为,所以我们可以说这是明确定义的。