• volatile和原子量atomic如何对抗编译器优化?


    废话文学上一次这么流行的时候,还是在上一次流行废话文学的时候。

    抖音上有个相声演员,每天的更新就是各种片汤话和废话,絮絮叨叨一大堆,一句有用的信息都没有。评论区都是调侃:“哎吓死我了,他差点就把正事说出来了”,“有领导开会那味儿了”。

    还有一个最近爆火的叫陈依涵的小姐姐,每天更新一款无用的软件,诸如输入自己身高就能得出自己身高的计算器、百进制的分秒转换器等,也是废话文学的另一种表现形式。

    ​与热衷于废话文学,被各种无厘头戳中笑点的我们相比,编译器就像个严肃高冷,追求效率,没有任何生活情趣的老头子一样,极其讨厌废话文学。

    考虑这样一种情况:

    1. x = 1;
    2. x = 0;
    3. x = 1;
    4. x = 0;
    5. x = 1;

    编译器一看,好家伙,这不纯纯的废话嘛!

    于是编译器优化时很有可能会将上述五行代码优化为一行:

    x = 1;

    因为它觉得你这个人的水平太挫,说话也啰嗦,它要帮帮你。

    可是,我这个x是表示板载LED的地址啊,我这么干,其实是想让这个LED闪烁...

    这种编译器优化带来的问题,即使是原子量atomic也无能为力。此时我们从武器库里找到了另一把武器:volatile。

    volatile

    volatile会告知编译器,对这种情况不要进行优化,保留这些看似冗余加载和废弃存储的无效操作,虽然是废话,但是我爱听。

    这几乎是volatile最适用的场景了,其他场景我觉得volatile都不算是最优解。

    我们来回顾一下一些教材上对C/C++里volatile关键字的解释:

    volatile提醒编译器它后面所修饰的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

    翻译翻译就是volatile会保证本线程对变量做的修改,都会立即被其他线程感知到。说官方点就是对其他线程可见,说人话就是其他线程拿到的值都是最新的,是修改生效的。

    看了上篇文章的读者,一定会想到内存序。这不就是内存序的功能嘛?

    而且内存序还要比它强大的多,因为volatile只能保证被volatile修饰的变量对其他线程可见,而原子量+内存序不仅可以保证对原子变量的修改对其他线程可见,还可以确保内存里其他变量的修改都对所有线程可见!

    atomic

    以上一篇文章《如何帮罗小猪的时间管理进一步提高性能?无锁并发!》提到过的这样一种情况为例:

    ‍// 计算值

    data = calculate();

    //设置flag,通知其他任务值已可用

    flag = true;

    编译器优化很有可能会把flag=true这句放到calculate计算值之前!因为它觉得两者似乎是不同的内存不同的寄存器。

    编译器说我估计calculate函数还要花费一点时间才能执行完,索性就先把下面的flag=true给执行了。反正内存地址不是同一个,不冲突。

    但这却有可能会让其他监听flag状态的线程提前执行,完全违背了我们的设计初衷。

    这时如果我们将flag用原子量atomic来定义,用store(memory_order_release)来赋值,则可以保证上述代码的正常执行次序,防止被其他线程在flag未设置之前拿到计算值。

    但这种情况,volatile是无能为力的。假如用volatile来修饰flag的话,它最多保证flag的变化能立刻被另一线程感知到,但却无法保证另一个线程拿到的data是被calculate过的。即C/C++里的volatile对于指令重排序无法进行限制。

    况且,volatile还有个致命的问题是,它并不能保证变量操作的原子性。也就是说,虽然你对其他线程可见,但你的值并不一定符合预期。

    比如用volatile修饰一个变量value:

    volatile int value = 0;

    value++;

    value--;

    std::cout<<value;

    如果有一个线程正在执行这段代码,另一个线程去读取value的值的话,那这个值会出现无数种可能,甚至可能是负数或者一个特别大的数。

    因为某个线程刚刚在value这块内存上写了一半,就被另一个线程读取,那么它读到的这个值属于未定义行为,没有任何意义。

    基于以上两点,即:

    1.volatile不能保证原子操作。

    2.volatile不能限制指令重排序。

    所以并发多任务的情况下用volatile可能并不是一个好的选择。

    总结一下,像状态寄存器、映射到内存地址上的 I/O 操作、涉及硬件操作的变量需要加volatile,因为对它们的每一次操作都有其意义,并不是废话文学。

    而并发时,多线程多任务环境下各任务间共享的标志,其实更应该用原子量和内存序,或者直接加互斥锁,以确保共享区操作的原子性和顺序性。

    所以其实volatile和atomic是应用于不同场景的,甚至可以叠加使用。比如:

    volatile std::atomic<int> value;

    这个式子表示对value的操作都是原子性的,并且它的废话文学也不可以被优化掉。

    后 记

    最近发的文章关于C/C++语言方面的比较多,事实上我是打算先从语言基础开始,后续关于编译器、单片机、RTOS、Linux应用、内核、驱动等嵌入式相关的技术知识和经验技巧,以及博主从事过的物联网、半导体行业的实战经验和职场分享,都会陆续在这个号上发布。

    还请关注。

    上次用还请关注这句话结尾的时候,还是在上次文章结尾的时候。

  • 相关阅读:
    简历自动生成工具
    数据加载及预处理
    JavaWeb实战:基础CRUD+批量删除+分页+条件
    循环结构 while dowhile for
    【Android Studio】常用布局 --- 滚动视图ScrollView
    Codeforces Round #830 (Div. 2) D1. Balance (Easy version)
    关于《web课程设计》网页设计 用html css做一个漂亮的网站 仿新浪微博个人主页
    Java常问面试题概要答案
    程序化交易(二)level2行情数据源接入
    排序算法复杂度
  • 原文地址:https://blog.csdn.net/qq_38639426/article/details/125617824