在 C++ 中写入相同值的竞争条件?
Race Condition with writing same value in C++?
当操作写入单个常量值时,代码中存在竞争条件是否有任何问题?例如,如果有一个并行循环为另一个数组 arr
中的每个值填充一个 seen
数组(假设越界索引没有问题)。关键部分可以是以下代码:
//parallel body with index i
int val = arr[i];
seen[val] = true;
因为写入的唯一值是 true
是否不需要互斥锁,并且可能对性能有害?即使线程互相踩踏,它们也只会用相同的值填充地址,对吗?
可能存在依赖于体系结构的约束,要求将您看到的数组元素分隔一定数量,以防止竞争线程破坏在同一机器字(或缓存行,甚至)中发生冲突的值。
也就是说,如果 seen
定义为 bool seen[N];
,那么 seen
的长度为 N 个字节,并且每个元素与其相邻元素直接相邻。如果一个线程更改元素 0 而另一个线程更改元素 2,则这两个更改都发生在同一个 64 位机器字中。如果这两个更改由不同的内核同时进行(或者甚至在多 cpu 系统的不同 CPU 上),它们将尝试将冲突解决为整个 64 位机器字(或在某些情况下更大)。这样做的结果是,已写入的 true
之一将通过获胜线程对相邻元素的更新返回到其先前状态(可能 false
)。
相反,如果您将 seen 定义为一个结构数组,每个结构都与缓存行一样大,那么您可能会让竞争线程在该结构中混合一个 bool 值...但这是有风险的,因为不所有 CPU 都将共享相同的缓存冲突验证策略、行大小等...并且不可避免地会有一个 CPU 会失败。
C++ 内存模型不会为您提供写入相同值的免费通行证。
如果两个线程在没有同步的情况下写入非原子对象,那只是竞争条件。竞争条件意味着您的程序执行未定义的行为。在程序执行过程中任何地方发生的未定义行为意味着程序的行为,无论是在未定义行为点之前还是之后,都不受 C++ 标准的任何限制。
给定的编译器可以免费提供更自由的内存模型。我不知道有什么。
您必须了解的一件事是 C++ 不是汇编宏语言。它不必产生您脑海中想象的天真的汇编程序。相反,C++ 试图让您的编译器更容易生成汇编程序,这是一个非常不同的事情。
编译器可以并且确实在生成代码时确定 "if X happens, we get undefined behavior; so I'll optimize around the fact that X does not happen"。在这种情况下,编译器可以 证明 具有定义行为的程序在两个不同的未同步线程中可以具有相同的 val
。
所有这些都可能发生在任何程序集生成之前很久。
并且在汇编级别,某些硬件可能会对多字节值进行未对齐赋值,从而做出有趣的事情。当声称是单线程写入的指令出现在相同字节的两个不同内核中时,某些硬件可能(理论上;我不知道在实践中有任何)引发陷阱。
所以这是 C++ 中的 UB。一旦你有了 UB,你必须在接触它的编译器可以看到的任何地方审计你的程序产生的汇编代码。如果您执行 LTO,这意味着在您的整个程序中,至少在任何调用或与您执行 UB 的代码交互的地方,到一个不清楚的距离。
只需编写定义的行为。只有当这被证明是一个关键任务性能瓶颈时,你才应该花更多的精力来优化它(首先定义更快的行为,只有当失败时你才会考虑 UB)。
当操作写入单个常量值时,代码中存在竞争条件是否有任何问题?例如,如果有一个并行循环为另一个数组 arr
中的每个值填充一个 seen
数组(假设越界索引没有问题)。关键部分可以是以下代码:
//parallel body with index i
int val = arr[i];
seen[val] = true;
因为写入的唯一值是 true
是否不需要互斥锁,并且可能对性能有害?即使线程互相踩踏,它们也只会用相同的值填充地址,对吗?
可能存在依赖于体系结构的约束,要求将您看到的数组元素分隔一定数量,以防止竞争线程破坏在同一机器字(或缓存行,甚至)中发生冲突的值。
也就是说,如果 seen
定义为 bool seen[N];
,那么 seen
的长度为 N 个字节,并且每个元素与其相邻元素直接相邻。如果一个线程更改元素 0 而另一个线程更改元素 2,则这两个更改都发生在同一个 64 位机器字中。如果这两个更改由不同的内核同时进行(或者甚至在多 cpu 系统的不同 CPU 上),它们将尝试将冲突解决为整个 64 位机器字(或在某些情况下更大)。这样做的结果是,已写入的 true
之一将通过获胜线程对相邻元素的更新返回到其先前状态(可能 false
)。
相反,如果您将 seen 定义为一个结构数组,每个结构都与缓存行一样大,那么您可能会让竞争线程在该结构中混合一个 bool 值...但这是有风险的,因为不所有 CPU 都将共享相同的缓存冲突验证策略、行大小等...并且不可避免地会有一个 CPU 会失败。
C++ 内存模型不会为您提供写入相同值的免费通行证。
如果两个线程在没有同步的情况下写入非原子对象,那只是竞争条件。竞争条件意味着您的程序执行未定义的行为。在程序执行过程中任何地方发生的未定义行为意味着程序的行为,无论是在未定义行为点之前还是之后,都不受 C++ 标准的任何限制。
给定的编译器可以免费提供更自由的内存模型。我不知道有什么。
您必须了解的一件事是 C++ 不是汇编宏语言。它不必产生您脑海中想象的天真的汇编程序。相反,C++ 试图让您的编译器更容易生成汇编程序,这是一个非常不同的事情。
编译器可以并且确实在生成代码时确定 "if X happens, we get undefined behavior; so I'll optimize around the fact that X does not happen"。在这种情况下,编译器可以 证明 具有定义行为的程序在两个不同的未同步线程中可以具有相同的 val
。
所有这些都可能发生在任何程序集生成之前很久。
并且在汇编级别,某些硬件可能会对多字节值进行未对齐赋值,从而做出有趣的事情。当声称是单线程写入的指令出现在相同字节的两个不同内核中时,某些硬件可能(理论上;我不知道在实践中有任何)引发陷阱。
所以这是 C++ 中的 UB。一旦你有了 UB,你必须在接触它的编译器可以看到的任何地方审计你的程序产生的汇编代码。如果您执行 LTO,这意味着在您的整个程序中,至少在任何调用或与您执行 UB 的代码交互的地方,到一个不清楚的距离。
只需编写定义的行为。只有当这被证明是一个关键任务性能瓶颈时,你才应该花更多的精力来优化它(首先定义更快的行为,只有当失败时你才会考虑 UB)。