C++ [[可能]]和[[不可能]]影响程序汇编的简单示例?

C++ [[可能]]和[[不可能]]影响程序汇编的简单示例?,c++,assembly,gcc,c++20,C++,Assembly,Gcc,C++20,C++20在语言中引入了属性[[可能]]和[[不可能]]],可用于允许编译器针对一条执行路径比其他路径更可能或更不可能的情况进行优化 考虑到错误分支预测的代价,这似乎是一个在代码的性能关键部分可能非常有用的特性,但我不知道它实际上会导致编译器做什么 是否有一段简单的代码,添加[[可能]]和[[不可能]]]属性会更改编译器的程序集输出?也许更重要的是,这些更改会做些什么 为了我自己的理解,我创建了一个简单的示例,以查看组件中是否存在任何差异,但这个示例似乎太简单,无法实际显示对组件的任何更改: v

C++20在语言中引入了属性
[[可能]]
[[不可能]]]
,可用于允许编译器针对一条执行路径比其他路径更可能或更不可能的情况进行优化

考虑到错误分支预测的代价,这似乎是一个在代码的性能关键部分可能非常有用的特性,但我不知道它实际上会导致编译器做什么

是否有一段简单的代码,添加
[[可能]]
[[不可能]]]
属性会更改编译器的程序集输出?
也许更重要的是,这些更改会做些什么

为了我自己的理解,我创建了一个简单的示例,以查看组件中是否存在任何差异,但这个示例似乎太简单,无法实际显示对组件的任何更改:

void true_path();
void false_path();

void foo(int i) {
    if(i) {
        true_path();
    } else {
        false_path();
    }
}
void bar(int i) {
    if(i) [[likely]] {
        true_path();
    } else [[unlikely]] {
        false_path();
    }
}

看起来,gcc中有一个bug。如果您有两个相同的函数,除了属性之外,gcc会错误地折叠它们

但是,如果只使用一个函数,并在
[[可能]]
/
[[不可能]]]]
之间切换,则程序集将发生更改

所以,这个函数:

void bar(int i) {
    if(i) [[unlikely]] {
        true_path();
    } else [[likely]] {
        false_path();
    }
}
汇编至:

bar(int):
        test    edi, edi
        jne     .L4
        jmp     false_path()
.L4:
        jmp     true_path()
bar(int):
        test    edi, edi
        je      .L2
        jmp     true_path()
.L2:
        jmp     false_path()
这是:

void bar(int i) {
    if(i) [[likely]] {
        true_path();
    } else [[unlikely]] {
        false_path();
    }
}
汇编至:

bar(int):
        test    edi, edi
        jne     .L4
        jmp     false_path()
.L4:
        jmp     true_path()
bar(int):
        test    edi, edi
        je      .L2
        jmp     true_path()
.L2:
        jmp     false_path()
请注意,条件已更改:如果
i
为非零,则第一个版本将跳转;如果
i
为零,则第二个版本将跳转


这与属性一致:gcc生成代码,其中条件跳转发生在不太可能的路径上。

旁白:属性是一个严重的误称,它们不用于优化更可能的代码路径。事实上,分支预测器已经做到了这一点,不需要属性或内部函数。相反,这些属性用于告诉分支预测器忽略可能性较大的分支,而可能性较小的分支的性能更为重要。我建议观看以下视频:。@KonradRudolph:通常意义上的“分支预测”是一个运行时事件。这些提示是编译时对编译器的提示,说明编译器可能希望如何布置分支,或者决定是否转换为无分支的东西,如
cmov
。只有当编译器使用基于实际运行时数据的概要文件引导优化时,它才重写任何内容。(在这种情况下IIRC gcc使用PGO数据,忽略提示)。@geza:heh,这很有趣。GCC的相同代码折叠过程间分析优化(ipa icf)发生在最终asm生成之前,可能是在框架级别。我记得在不同的函数中,发现了一个带有不同内联ASM约束的bug,它们实际上是一个正确的问题。@康拉德-鲁道夫:我假设它们是相同的,并且将代码< > [[[可能] ] <代码> >代码> [[不可能] ] //COD>添加到ISO C++中,以便携方式公开现有的编译器行为。是的,我猜GCC可能会忽略PGO数据可用时的任何一种写入方式。您是否知道为什么这里使用
jne
代替
je
,以及效果如何?@J.AntonioPerez:编译器会尝试优化,因此CPU不必进行分支。我认为它确实做到了,因为正确预测和未执行的跳转比正确预测和执行的分支便宜。或者它是这样做的,因为缓存的使用:冷路径放在函数的末尾,所以热路径有一个连续的路径,所以缓存的利用率更好一些。但我不是这方面的专家,如果你好奇的话,也许你想问这个特定的问题,贴上标签。@J.AntonioPerez:已经有一些信息了:,请阅读答案下面的评论(特别是Peter Cordes和BeeOnRope的评论)@J.AntonioPerez:geza引用的两个因素都是使预期的快速路径顺利通过的优势。对于更大的代码块,让一行I-cache保持冷状态更合理。还要注意的是,传统上(在IT-TAGE分支预测器之前),无法找到预测的分支被静态预测为不向前跳跃,向后跳跃。(IT-TAGE(Haswell和更高版本)始终为动态预测编制索引,因此没有静态预测。索引预测可能主要针对不同的分支,但这是与TAGE的折衷)