提问者:小点点

为什么没有一个主要的编译器优化这个条件移动!=要分配的值?


我偶然发现了这篇Reddit帖子,这是对以下代码片段的一个笑话,

void f(int& x) {
    if (x != 1) {
        x = 1;
    }
}
void g(int& x) {
    x = 1;
}

说这两个函数不等同于“编译器”。我确信任何主要的C编译器都会将条件赋值优化为无条件存储,从而为fg发出相同的汇编代码。

然而,他们没有。

谁能给我解释一下为什么会这样?

我想的是:无条件存储很可能会更快,因为我们无论如何都要访问内存才能读取比较的值,分支代码会给分支预测器带来压力。编译器(AFAIK)也不应该认为存储是副作用,即使后续的内存访问可能会更快或更慢,这取决于是否采取了f中的分支,由于缓存局部性。

那么编译器就是不能弄清楚这一点吗?虽然fg的等价性可能不一定是微不足道的证明,但我觉得这些编译器能够解决更难的问题。所以我可能错了,这些函数毕竟不是等价的,或者这里发生了什么?


共3个答案

匿名用户

静态const int val=1是不安全的;生活在只读内存中。无条件存储版本将分段错误尝试写入只读内存。

首先检查的版本可以安全地在C抽象机器中调用该对象(通过const_cast),因此优化器必须尊重任何未写入的对象最初是const并且在只读内存中的可能性。

  • 是否允许在const定义的对象上抛弃const,只要它没有实际修改?引用标准来支持这一点。

这也可能不是线程安全的。一般来说,编译器不能发明对抽象机器不写入的对象的写入,以防另一个线程也在写入它,我们会踩到值。(除了原子RMW是安全的,就像比较交换一样。)

由于我们已经读取了对象,我们可能会假设没有其他线程写入,因为我们的无条件读取已经是数据竞争UB。

但总的来说,在实践中,为编译器发明非原子加载存储相同的值是一个线程安全问题(例如,我似乎记得读过IA-64GCC为奇数长度memcpy或位字段或其他东西的数组末尾的字节做到了这一点,当它在uint8_t锁旁边的结构中时,这是个坏消息。)所以编译器开发人员有理由不愿意发明存储。

    null

如果许多线程在同一个对象上运行此代码,则无条件写入在正常的CPU架构上是安全的,但速度要慢得多(争夺缓存行的MESI独占所有权与共享所有权)。

弄脏缓存行也是不可取的。

(之所以安全,仅仅是因为它们都存储相同的值。如果甚至一个线程存储了不同的值,如果它碰巧不是修改顺序中的最后一个,它可能会覆盖该存储,修改顺序是由获得缓存行所有权的CPU提交存储的顺序决定的。)

这种写前检查的习惯用法实际上是一些多线程代码会做的一件真实的事情,以避免对变量进行缓存行乒乓,如果每个线程都写了已经存在的值,这些变量将被高度竞争:

>

  • C优化:条件存储以避免弄脏缓存行

    奇怪的优化?在'libuv'中。请解释

    分支错误预测与缓存未命中

    x86如何处理存储条件指令?(它没有,除了AVX或AVX-512屏蔽存储。)

  • 匿名用户

    这是否构成优化取决于x非1的频率,这是C编译器事先不知道的。如果x几乎总是1,那么if(x!=1)back可能会比x=1更快。

    (有趣的是,一些虚拟机,如Java虚拟机,确实在运行时分析执行模式,并即时执行此类优化,如果事实证明他们的假设是错误的,他们甚至可能撤销此类优化,因此理论上,如果我们相信在运行时分析执行模式的开销小于它们节省的开销,它们可以在某些边际情况下优于C。我真的不知道。我只是觉得他们这样做很有趣。)

    匿名用户

    编译器可以指定其输出必须以这样一种方式链接和使用,即所有可以从代码中访问的对象——包括标记为const的对象——都将位于存储中,该存储被配置为一旦执行任何此类操作,任意数量的写入相同值的操作将无效。指示的优化转换在编译器上是合理的,它记录了对其输出如何使用的限制。

    然而,大多数编译器供应商可能会认为生成的代码在更广泛的环境中可用的价值高于由于这些限制而可能实现的任何额外优化的价值,特别是因为依赖于这些限制的代码将无法与其他实现或环境处理的代码互操作,而这些实现或环境并非旨在支持它们。