为什么C ++编译器不将此条件布尔分配优化为无条件分配?


117

考虑以下功能:

void func(bool& flag)
{
    if(!flag) flag=true;
}

在我看来,如果flag具有有效的布尔值,则相当于将其无条件设置为true,如下所示:

void func(bool& flag)
{
    flag=true;
}

然而,gcc和clang都不以这种方式对其进行优化-两者都在-O3优化级别上生成以下内容:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

我的问题是:只是代码太特殊而无法优化,还是有充分的理由说明为什么这种优化是不必要的(如果flag没有引用的话)volatile?似乎唯一的原因可能是在读取时flag可能以某种方式具有非true-或- false值而没有未定义的行为,但是我不确定这是否可能。


8
您是否有证据表明这是“优化”?
David Schwartz

1
@ 200_success我认为以非工作标记作为标题的代码行不是一件好事。如果你想更具体的标题,精细选择一个英语句子,并尽量避免在它的代码(例如,为什么不编译器优化条件写入无条件写入时,他们可以证明他们是等价?或类似)。另外,由于未显示反引号,即使您使用代码也不要在标题中使用反引号。
巴库里

2
@Ruslan,虽然似乎没有对函数本身进行优化,但当它可以内联代码时,似乎确实对内联版本进行了优化。通常只会导致使用编译时间常数1godbolt.org/g/swe0tc
埃文·

Answers:


102

由于缓存一致性的考虑,这可能会对程序的性能产生负面影响。flag每次写入func()都会导致包含的缓存行变脏。无论写入的值与写入前在目标地址上找到的位完全匹配的事实,都会发生这种情况。


编辑

HV提供了另一个阻止此类优化的充分理由。对于提议的优化,这是一个更有说服力的论据,因为它可能导致不确定的行为,而我的(原始)回答仅涉及性能方面。

经过一番思考之后,我可以再举一个示例,说明为什么应严格禁止编译器-除非他们可以证明该转换对于特定上下文是安全的-否则引入无条件写入。考虑以下代码:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

如果使用无条件写入,func()则肯定会触发未定义的行为(对只读存储器的写入将终止程序,即使该写入的效果否则为空操作)。


7
由于您摆脱了分支机构,因此它也可能对性能产生积极影响。因此,我认为在没有非常具体的系统的情况下讨论这个特殊案例是没有意义的。
伦丁

3
@Yakk行为定义不受目标平台的影响。说它将终止程序是不正确的,但是UB本身可能会产生深远的后果,包括鼻恶魔。
约翰·德沃夏克

16
@Yakk这取决于“只读内存”的含义。不,它不在ROM芯片中,但通常是在装入未启用写访问权的页面的部分中,当您尝试对其进行写操作时,您将收到SIGSEGV信号或STATUS_ACCESS_VIOLATION异常。
Random832 '16

5
“这肯定会触发未定义的行为”。否。未定义行为是抽象机的属性。代码说的就是确定UB是否存在。编译器无法引起这种情况(尽管如果出现故障,编译器可能导致程序无法正常运行)。
Eric M Schmidt

7
const传递给可以修改作为未定义行为(而非无条件写入)来源的数据的函数一种抛弃。医生,当我这样做时会很痛
Spencer

48

除了莱昂对性能的回答外:

假设 flagtrue。假设有两个线程在不断调用func(flag)。在这种情况下,编写的函数不会在中存储任何内容flag,因此这应该是线程安全的。两个线程确实访问相同的内存,但只能读取它。无条件设置flagtrue意味着两个不同的线程将写入同一内​​存。这是不安全的,即使正在写入的数据与已经存在的数据相同,也不安全。


9
认为这是应用的结果[intro.races]/21
Griwes

10
很有意思。所以,我看这是:编译器永远不会允许“优化”的写操作,其中抽象的机器不会有一个。
马丁·巴

3
@MartinBa通常是这样。但是,如果编译器可以证明没关系,例如,因为可以证明没有其他线程可以访问该特定变量,那么它就可以了。

13
当编译器针对的系统使其不安全时,这才是不安全的。我从未开发过这样的系统,在该系统上写入0x01已经0x01导致“不安全”行为的字节。在具有字或双字内存访问的系统上,它将;但是优化程序应该意识到这一点。在现代PC或电话OS上,不会发生任何问题。因此,这不是正当理由。
Yakk-Adam Nevraumont

4
@Yakk实际上,我想得更多,即使对于普通处理器,我也认为这是正确的。我认为当CPU可以直接写入内存时您是正确的,但是flag应该在写时复制页面中。现在,可以在CPU级别上定义行为(页面错误,让OS处理),但是在OS级别上,可能仍未定义,对吗?

13

我不确定这里的C ++的行为,但是在C中,内存可能会更改,因为如果内存中包含非1以外的非零值,则它在检查时将保持不变,但在检查时变为1。

但是由于我不太熟练使用C ++,所以我不知道这种情况是否可能。


这仍然是正确的_Bool吗?
Ruslan

5
在C语言中,如果内存中包含一个ABI并未说对它的类型有效的值,则它是一个陷阱表示,并且读取陷阱表示是未定义的行为。在C ++中,只有在读取未初始化的对象并且正在读取UB的未初始化的对象时,才会发生这种情况。但是,如果您找到一个说任何非零值对于类型bool/ _Bool和mean 有效的trueABI,那么在该特定ABI中,您可能是对的。

1
@Ruslan对于使用Itanium ABI的编译器,在ARM处理器上,C _Bool和C ++ bool是相同类型,或者是遵循相同规则的兼容类型。对于MSVC,它们具有相同的大小和对齐方式,但是没有官方声明它们是否使用相同的规则。
贾斯汀时间-恢复莫妮卡

1
@JustinTime:C <stdbool.h>包含一个typedef _Bool bool; 。是的,在x86上(至少在System V ABI中),bool/ _Bool必须为0或1,并清除字节的高位。我认为这种解释是不合理的。
彼得·科德斯

1
@JustinTime:是的,我应该指出,在System V ABI的所有x86版本中,它确实具有相同的语义,这就是这个问题。(我可以知道,因为第一个arg func是在RDI中传递的,而Windows将使用RDX)。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.