符合规范的实现是否可以对这个示例进行改写,以消除加载操作?
请考虑下面这个例子:
#include <atomic>
#include <thread>
int main(){
std::atomic<int> ack = 1;
std::atomic<int> val = 0;
std::jthread t1([&]{
while(ack.load(std::memory_order::relaxed) !=0); // #0
val.store(1,std::memory_order::release); // #1
});
std::jthread t2([&]{
val.load(std::memory_order::acquire); // #2
ack.fetch_sub(1,std::memory_order::relaxed); // #3
val.load(std::memory_order::acquire); // #4
});
}
第一个问题是,在这个例子中,是否不存在一个可能的执行使得 #2 读取 #1,对吗?在那个可能的执行中,#0 将在 #3 之前发生,使得循环永远不会退出。
接着,考虑这个变体示例
#include <atomic>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
int main(){
std::atomic<int> ack = 1;
std::atomic<int> val = 0;
std::jthread t1([&]{
int count = 0;
while(ack.load(std::memory_order::relaxed) !=0){ // #0
if(++count == 5){
break;
}
std::this_thread::sleep_for(1000ms);
}
val.store(1,std::memory_order::release); // #1
});
std::jthread t2([&]{
val.load(std::memory_order::acquire); // #2
ack.fetch_sub(1,std::memory_order::relaxed); // #3
val.load(std::memory_order::acquire); // #4
});
}
在这个例子中,#2 读取 #1 是一个有效的可能执行,因为循环可以因为 break 而退出。
第二个问题是,符合规范的实现是否允许将 t1 中的整段循环改写为如下所述的形式?
while(count < 4){
++count;
std::this_thread::sleep_for(1000ms);
}
论证来自 [intro.abstract] 第6页
符合规范的实现执行一个格式正确的程序时,应产生在同一程序和同一输入下,抽象机器的相应实例的定义前缀的某个可能执行之一所对应的可观测行为。
解决方案
通常来说,是的,编译器可以静态地判断,在它生成的可执行代码中,某序列原子操作的顺序是唯一可能发生的顺序,这符合现行标准。(我不认为主流的现实世界编译器会在一个包含1 秒睡眠的循环中这么做。对普通计算机而言,那是很长的一段时间,因此如果按原样编译,实际发生的那个可能执行很可能不是它。)
目前委员会的方针是,编译器不进行此类优化,直到他们找出让代码自愿参与的方法为止,而不是到处用 volatile atomic 来表示选择退出。
- https://wg21.link/n4455:N4455没有理智的编译器会优化原子操作
- https://wg21.link/p0062:WG21/P0062R1:编译器应该在何时优化原子操作?—— 这些论文给出了一些其他循环转换的例子,比如把存储从循环内部下沉到循环外并在末尾一次性完成,即使使用
volatile atomic也会让进度条更新不理想。因此这是一个尚未解决的问题,当前标准允许的优化比某些情况所期望的要多。就目前而言,真实的编译器在原子操作的优化方面非常保守,从不把它们优化掉。 - 编译器能否并且确实会优化掉两次原子加载?
- 为什么编译器不合并冗余的std::atomic写入?
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。