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

问题描述 投票:0回答:2

我偶然发现了this Reddit 帖子,这是关于以下代码片段的笑话,

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

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

f
g
.

发出相同的汇编代码

然而,他们没有。

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

我在想的是:无条件存储很可能会更快,因为无论如何我们都必须访问内存来读取比较值,并且分支代码会对分支预测器施加压力。此外,存储不应被编译器(AFAIK)视为副作用,即使后续内存访问可能更快或更慢,具体取决于是否采用了

f
中的分支,由于缓存位置。

那么编译器就是无法解决这个问题吗?虽然

f
g
的等价性可能不一定容易证明,但我觉得这些编译器能够解决更难的问题。那么我是不是错了,这些功能毕竟不是等价的,或者这里发生了什么?

c++ compiler-optimization
2个回答
4
投票

生活在只读内存中的

static const int val = 1;
是不安全的。无条件存储版本将在尝试写入只读内存时出现段错误。

首先检查的版本可以安全地调用 C++ 抽象机中的该对象(通过

const_cast
),因此优化器必须考虑到任何未写入的对象最初是
const
并且处于只读状态的可能性记忆。


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

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

但总的来说,发明非原子加载+存储相同的值在实践中一直是编译器的线程安全问题(例如,我似乎记得读过 IA-64 GCC 对刚好超过数组末尾的字节执行此操作对于奇数长度的

memcpy
或位域或其他东西,当它位于
uint8_t lock
旁边的结构中时这是个坏消息。)因此编译器开发人员有理由不愿意发明存储。


如果许多线程在同一个对象上运行这段代码,无条件写入在普通 CPU 架构上是安全的,但速度要慢得多(争用 MESI 对缓存行的独占所有权,而不是共享。)

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

(而且安全只是因为它们都存储相同的值。即使一个线程存储不同的值,如果它恰好不是修改顺序中的最后一个(由 CPU 获得所有权的顺序确定),它可能会覆盖该存储缓存行提交他们的商店。)

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


2
投票

这是否构成优化取决于

x
非 1 的频率,这是 C++ 编译器事先不知道的。如果
x
几乎总是1,那么
if( x != 1 ) return
可能会比
x = 1
更快。

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

© www.soinside.com 2019 - 2024. All rights reserved.