为什么C++编译器不能将有条件的布尔赋值优化为无条件的赋值?

117

考虑以下函数:

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

我认为,如果标志具有有效的布尔值,这将等同于无条件将其设置为 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可能以某种非truefalse的值存在而不会产生未定义的行为,但我不确定这是否可能。


8
你有没有任何证据表明这是一种“优化”? - David Schwartz
1
@200_success 我认为在标题中放置一行带有不起作用标记的代码并不是一个好主意。如果你想要一个更具体的标题,可以选择一个英文句子,但请尽量避免在其中使用代码(例如“为什么编译器不能将条件写入无条件写入进行优化,当它们可以证明它们是等效的?”或类似的问题)。此外,由于反引号不会被渲染,请不要在标题中使用它们,即使您使用代码。 - Bakuriu
2
@Ruslan,虽然它似乎没有针对函数本身进行此优化,但当它可以内联代码时,它似乎确实会对内联版本进行优化。通常只会导致编译时常量1被使用。https://godbolt.org/g/swe0tc - Evan Teran
我将此问题关闭为较新的问题(为什么主要编译器都不优化这个检查值是否已经设置的条件存储?)的重复,因为我认为那里的答案涵盖了这里回答中提出的相同观点(以及他们没有提到的其他事情),但更详细地引用了参考文献,表明去除const是不会导致未定义行为的,因此编译器必须尊重这种可能性。也许应该合并这些问题。 - Peter Cordes
3个回答

104
这可能会对程序的性能产生负面影响,因为需要考虑缓存一致性。每次调用func()时写入flag将污染包含缓存行。即使所写入的值与写入前目标地址处找到的位完全匹配,这种情况仍将发生。

编辑

hvd提供了另一个好理由来阻止这样的优化。这是一个更有说服力的反对所提出的优化的论据,因为它可能导致未定义的行为,而我的(原始)答案只涉及性能方面。

经过更深入的思考,我可以提出一个更多的例子,说明编译器应该被严格禁止——除非它们能够证明在特定的上下文中转换是安全的——从而引入无条件写入。考虑以下代码:

const bool foo = true;

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

func()中进行无条件写操作肯定会触发未定义的行为(即使写入的效果本来是一个无操作,写入只读内存也将终止程序)。

10
如果你去掉一个分支,这可能对性能产生积极影响。因此,我认为在没有特定系统的情况下讨论这个特定案例是没有意义的。 - Lundin
3
@Yakk所说的行为定义性不受目标平台影响。说它会终止程序是不正确的,但是UB本身可能会产生很远的后果,包括鼻妖。 - John Dvorak
18
这取决于对“只读内存”一词的理解。它不在ROM芯片中,而是通常位于一个加载到无法进行写入访问权限的页面的部分,如果你尝试写入它,则会收到例如SIGSEGV信号或STATUS_ACCESS_VIOLATION异常的错误提示。 - Random832
6
“this definitely triggers undefined behavior”。 不是这样的。未定义行为是抽象机器的属性。决定 UB 是否存在取决于代码本身。编译器不能引起它(尽管如果有缺陷,编译器可以导致程序行为不正确)。 - Eric M Schmidt
8
const 强制转换为非 const 以传递给可能会修改数据的函数是未定义行为的来源,而不是无条件写入。医生,当我这样做时感觉很痛... - Spencer
显示剩余13条评论

49

除了Leon的性能答案之外:

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


10
我认为这是应用[intro.races]/21的结果。 - Griwes
10
非常有趣。那么我理解为:编译器绝不允许在抽象机器没有写操作的情况下进行“优化插入”写操作。 - Martin Ba
3
大多数情况是如此。但如果编译器可以证明这并不重要,例如因为它可以证明没有其他线程可能访问到那个特定的变量,那么就可以了。 - user743382
14
只有当编译器所指向的系统使其不安全时,它才是不安全的。我从未在写入0x01到原本已经是0x01的字节时会引起“不安全”行为的系统上进行过开发。对于具有字或双字内存访问的系统来说会存在问题,但优化器应该意识到这一点。在现代PC或手机操作系统上,不会出现问题。因此,这不是一个有效的理由。 - Yakk - Adam Nevraumont
4
@Yakk 实际上,再仔细思考之后,我认为这样做是正确的,即使对于普通的处理器也是如此。当 CPU 可以直接写入内存时,我觉得你是对的,但假设 flag 在一个写时复制页面中。现在,在 CPU 层面上,行为可能已经被定义了(页面错误,让操作系统处理它),但在操作系统层面上,它仍然可能是未定义的,对吗? - user743382
显示剩余18条评论

14
我不确定C++在这里的行为,但在C中,如果内存包含非零值且不为1,则在检查之后会保持不变,但会在检查过程中更改为1,因此内存可能会被更改。
但是由于我对C++不是很熟悉,所以我不知道这种情况是否可能发生。

关于 _Bool,这个说法仍然正确吗? - Ruslan
6
在C语言中,如果内存包含一个ABI没有规定为其类型有效的值,则它是一个陷阱表示,读取一个陷阱表示是未定义行为。在C++中,只有在读取未初始化的对象时才会出现这种情况,并且读取未初始化的对象是未定义行为。但是,如果您可以找到一个ABI,它说任何非零值对于类型bool/_Bool都是有效的并且表示为true,那么在该特定ABI中,您可能是正确的。 - user743382
1
@Ruslan 对于使用Itanium ABI编译器和ARM处理器的情况,C _Bool 和 C++ bool 要么是相同类型,要么是遵循相同规则的兼容类型。在MSVC中,它们具有相同的大小和对齐方式,但没有官方声明它们是否使用相同的规则。 - Justin Time - Reinstate Monica
1
@JustinTime:C语言中的<stdbool.h>包括了一个typedef _Bool bool;。而且,在x86架构中(至少在System V ABI中),bool/_Bool的值必须是0或1,字节的高位需要清零。我认为这个解释不太合理。 - Peter Cordes
1
@JustinTime:没错,我应该只是指出在所有的x86版本的System V ABI中它确实具有相同的语义,这也是这个问题的关键所在。(我可以看出func的第一个参数是通过RDI传递的,而Windows则会使用RDX)。 - Peter Cordes
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接