【问题标题】:Why don't C++ compilers optimize this conditional boolean assignment as an unconditional assignment?为什么 C++ 编译器不将此条件布尔赋值优化为无条件赋值?
【发布时间】:2017-03-11 05:37:16
【问题描述】:

考虑以下函数:

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 值而在阅读时没有未定义的行为,但我不确定这是否可能。

【问题讨论】:

  • 你有任何证据表明这是一种“优化”吗?
  • @200_success 我不认为将带有非工作标记的代码行作为标题是一件好事。如果你想要一个更具体的标题,可以但是选择一个英文句子,并尽量避免其中的代码(例如为什么编译器不优化条件写入到无条件写入何时可以证明它们是等价的? 或类似)。此外,由于不呈现反引号,即使您使用代码,也不要在标题中使用它们。
  • @Ruslan,虽然它似乎没有对函数本身进行这种优化,但当它可以内联代码时,它似乎确实对内联版本这样做了。通常只会导致使用 1 的编译时间常数。 godbolt.org/g/swe0tc

标签: c++ optimization


【解决方案1】:

由于cache coherence 的考虑,这可能会对程序的性能产生负面影响。每次调用func() 时写入flag 会弄脏包含缓存行。无论写入的值是否与写入前在目标地址找到的位完全匹配,都会发生这种情况。


编辑

hvd 提供了另一个 good reason 来阻止这种优化。这是反对建议优化的更有说服力的论据,因为它可能会导致未定义的行为,而我的(原始)答案仅涉及性能方面。

经过一点思考,我可以再举一个例子,为什么编译器应该被强烈禁止——除非他们可以证明转换对于特定上下文是安全的——从引入无条件写入。考虑这段代码:

const bool foo = true;

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

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

【讨论】:

  • 它也可能对性能产生积极影响,因为你摆脱了一个分支。因此,如果没有考虑到非常具体的系统,我认为讨论这个特殊案例没有意义。
  • @Yakk 行为定义不受目标平台的影响。说它会终止程序是不正确的,但 UB 本身可能会产生深远的影响,包括鼻恶魔。
  • @Yakk 这取决于“只读内存”的含义。不,它不在 ROM 芯片中,但它通常位于加载到没有启用写访问权限的页面的部分中,你会得到例如尝试写入时出现 SIGSEGV 信号或 STATUS_ACCESS_VIOLATION 异常。
  • “这肯定会触发未定义的行为”。不,未定义的行为是抽象机器的属性。代码所说的内容决定了 UB 是否存在。编译器不会导致它(尽管如果有错误,编译器可能会导致程序行为不正确)。
  • const 传递给一个函数可能会修改数据,这是未定义行为的来源,而不是无条件写入。医生,我这样做的时候很痛......
【解决方案2】:

除了 Leon 关于性能的回答:

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

【讨论】:

  • 认为这是应用[intro.races]/21的结果。
  • 非常有趣。所以我将其解读为:编译器从不允许“优化”抽象机器没有的写操作。
  • @MartinBa 基本上是这样。但是,如果编译器可以证明它无关紧要,例如因为它可以证明没有其他线程可能访问该特定变量,那么它可能没问题。
  • 这只是不安全的如果编译器的目标系统使其不安全。我从未在将0x01 写入已经是0x01 的字节导致“不安全”行为的系统上进行开发。在具有 word 或 dword 内存访问的系统上,它会;但是优化器应该意识到这一点。在现代 PC 或手机操作系统上,不会出现任何问题。所以这不是一个正当的理由。
  • @Yakk 其实再想一想,我觉得这毕竟是对的,即使对于普通处理器也是如此。我认为当 CPU 可以直接写入内存时你是对的,但假设 flag 位于写时复制页面中。现在,在 CPU 级别,可能定义了行为(页面错误,让操作系统处理它),但在操作系统级别,它可能仍然未定义,对吧?
【解决方案3】:

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

但由于我对 C++ 不是很流利,我不知道这种情况是否可能发生。

【讨论】:

  • _Bool 仍然是这样吗?
  • 在 C 语言中,如果内存包含 ABI 没有说对其类型有效的值,则它是陷阱表示,读取陷阱表示是未定义的行为。在 C++ 中,这只会在读取未初始化的对象时发生,并且它正在读取未初始化的 UB 对象。但是,如果您能找到一个 ABI 表明任何非零值都对类型 bool/_Bool 有效并且意味着 true,那么在该特定 ABI 中,您可能是对的。
  • @Ruslan 对于使用 Itanium ABI 的编译器和在 ARM 处理器上,C _Bool 和 C++ bool 要么是相同类型,要么是遵循相同规则的兼容类型。使用 MSVC,它们具有相同的大小和对齐方式,但没有官方声明它们是否使用相同的规则。
  • @JustinTime: C 的 &lt;stdbool.h&gt; 包括一个 typedef _Bool bool; 是的,在 x86 上(至少在 System V ABI 中),bool/_Bool 必须是 0 或 1 , 字节的高位被清除。我不认为这种解释是合理的。
  • @JustinTime:是的,我应该指出它在 System V ABI 的所有 x86 风格中确实具有相同的语义,这就是这个问题的意义所在。 (我可以说是因为func 的第一个参数是在 RDI 中传递的,而 Windows 会使用 RDX)。
猜你喜欢
  • 1970-01-01
  • 2011-03-14
  • 1970-01-01
  • 2010-09-14
  • 1970-01-01
  • 1970-01-01
  • 2011-01-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多