MSVC的可能/不可能等效项

61

GCC编译器支持__builtin_expect语句,用于定义可能和不可能的宏。

例如:

#define likely(expr)    (__builtin_expect(!!(expr), 1))
#define unlikely(expr)  (__builtin_expect(!!(expr), 0))

是否有适用于Microsoft Visual C编译器的等效语句或类似语句?


3
VS 反馈 网站上投票以添加此功能! - rustyx
2
请注意,微软已经声明他们不喜欢这种优化方式在此在此。未来似乎不太可能添加这种优化方式(无恶意)。我们希望人们使用基于配置文件的优化而不是手动注释代码。有关更多信息,请参见此博客文章。配置文件计数不会撒谎(或者说,它们撒谎的比用户少得多)。 - jrh
请参阅BOOST_LIKELYBOOST_UNLIKELY - phuclv
@jrh 所有这些链接现在都失效了。另一个答案:*不实现此功能的论点是它是非标准的。MSVC正在推动实现标准特性,而不是以与其他编译器不兼容的方式扩展语言。(我们在过去做得太多了。)有一个标准提案引入这样的属性。当它被标准化后,我们将实现它:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0627r0.pdf - phuclv
@phuclv很不幸,我不知道有哪个归档可以连接页面。在连接页面上,一位微软员工说机器更擅长优化,我猜我们现在得到了一个稍微不同的故事(“它不符合标准”)。 - jrh
1
@phuclv 请参阅关于连接链接的此元帖 - jrh
8个回答

25

2
有没有可能出现类似 GCC 的 __builtin_expect(又称 likely/unlikely)的东西?不实现此功能的论点是它是非标准的。MSVC正在推动实现标准特性,而不是以与其他编译器不兼容的方式扩展语言。(我们在过去做了太多这样的事情。)已经有一个标准提案引入了这样的属性。当它被标准化后,我们将实现它:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0627r0.pdf - phuclv
1
现在已经在MSVC、GCC和Clang中实现了。请注意,例如无法与三元运算符一起使用。 - Alex Shpilkin

20
根据http://www.akkadia.org/drepper/cpumemory.pdf(第57页)的说法,即使CPU动态预测正确,使用静态分支预测仍然有意义。
原因是如果静态预测做得正确,L1i缓存将被更有效地利用。

17

我建议直接删除

没有什么可以替代它。虽然有__assume(),但不要使用它,因为它是一种不同类型的优化器指令。

实际上,gnu内置函数被包装在宏中只是为了让你可以自动删除它,如果未定义 __GNUC__ 。这些宏并没有什么必要性,我敢打赌你不会注意到运行时差异。

总结

在非GNU环境下,只需删除(将* likely设置为空)即可。你不会感觉到它的缺失。


17
我有一个硬件设备,每次调用函数后我需要执行类似 safeCall(mDevice.doit()) 这样的检查,我的 safeCall 函数被内联了,这可以提高性能,但只有在我有可能/不可能分支时才能使用。我想说这些宏可能很有用。 - Mikhail
4
通常情况下,你应该更倾向于使用实际的配置文件反馈来进行[分支预测](-fprofile-arcs),因为程序员在预测他们的程序实际表现方面往往表现不佳。 - holgac
2
我确实错过了 unlikely 内置函数。没有 PGO(这是个大烦恼),愚蠢的 MSVC 几乎总是把指令排序错了。 - rustyx
9
尽管您建议不要这样做,但在此上下文中将 _assume 命名为“close”有点不太合适,因为它根本不相似。不了解的读者可能会错误地解释“可以获得错误的代码”。它的含义等同于 __builtin_unreachable。将其用于分支提示不仅是危险的,而且在任何情况下都是不正确的。 - Damon
4
在GCC中,“_assume(cond)”的语义与“if (!(cond)) __builtin_unreachable();”在很大程度上是等价的。这意味着将其用作分支提示可以使编译器删除整个“不可达”的分支以及条件(如果没有副作用的话)。 - Arne Vogel
显示剩余2条评论

9

我知道这个问题是关于Visual Studio的,但我会尝试为尽可能多的编译器(包括Visual Studio)回答……

十年后终于有所进展!截至到Visual Studio 2019,MSVC仍然不支持任何类似的功能(即使它是最受欢迎的内置/内建函数),但正如Pauli Nieminen上面提到的,C++20具有likely/unlikely属性,可以用于创建likely/unlikely宏,而MSVC通常很快就会添加对新的C++标准的支持(不像C语言),因此我期望Visual Studio 2021能够支持它们。

目前(2019-10-14)只有GCC支持这些属性,甚至只应用于标签,但它足以进行一些基本测试。以下是一个快速的实现,您可以在Compiler Explorer上测试

#define LIKELY(expr) \
  ( \
    ([](bool value){ \
      switch (value) { \
        [[likely]] case true: \
          return true; \
        [[unlikely]] case false: \
          return false; \
      } \
    }) \
  (expr))
#define UNLIKELY(expr) \
  ( \
    ([](bool value){ \
      switch (value) { \
        [[unlikely]] case true: \
          return true; \
        [[likely]] case false: \
          return false; \
      } \
    }) \
  (expr))
编辑(2022-05-02):MSVC 2022支持C++20,包括[[likely]]/[[unlikely]],但对此生成的代码非常糟糕(请参见本帖子中的评论)...不要在那里使用它。
您可能需要使用#ifdef来支持无法处理它的编译器,但幸运的是大多数编译器都支持__builtin_expect
  • GCC 3.0
  • clang
  • 自至少13以来的ICC,很可能更长时间。
  • Oracle Development Studio 12.6+,但仅适用于C++模式。
  • ARM 4.1
  • 自至少10.1以来的IBM XL C/C ++,很可能更长时间。
  • TI自6.1起
  • TinyCC自0.9.27起
GCC 9+还支持__builtin_expect_with_probability。虽然它只在这里可用,但希望有一天会…它可以消除尝试确定是否使用likely/unlikely时的很多猜测 - 您只需设置概率,编译器(理论上)即可正确处理。
此外,clang支持__builtin_unpredictable(自3.8以来),但使用__has_builtin(__builtin_unpredictable)进行测试。由于许多编译器现在都是基于clang的,因此它可能也适用于它们。
如果您希望一切都准备就绪,您可能会对我的一个项目Hedley感兴趣。它是一个公共领域的单个C/C++头文件,可以在几乎所有编译器上工作,并包含许多有用的宏,包括HEDLEY_LIKELYHEDLEY_UNLIKELYHEDLEY_UNPREDICTABLEHEDLEY_PREDICTHEDLEY_PREDICT_TRUEHEDLEY_PREDICT_FALSE。虽然它还没有 C++20 版本,但很快就会有...

即使您不想在项目中使用 Hedley,您也可能想检查那里的实现,而不是依赖于上面列出的列表;我可能会忘记更新此答案的新信息,但 Hedley 应该始终是最新的。


MSVC 2022支持[[likely]]和[[unlikely]],但使用lambda宏时会生成糟糕的代码。如果直接将[[likely]]属性放入代码中,MSVC生成的代码会好得多。 - Paul Groke
即使启用了优化,也是这样吗?如果我没记错的话,我试过验证它的良好工作,尽管(显然)没有使用2022。无论如何,很高兴知道[[likely]]/[[unlikely]]可以工作,我会尽快更新Hedley。 - nemequ
是的,即使进行了优化。MSVC 2022似乎无法完全优化掉switch-case。请参见https://godbolt.org/z/xhar5WWv5。当使用if-else而不是switch-case时,生成的代码显着改善。但在两种情况下,它都失去了提示。似乎无法将其传播到内联lambda之外。https://godbolt.org/z/fsqd4o5fM - Paul Groke
好的,谢谢!看起来我没有在 Hedley 中包含这个版本(当时只有GCC支持它,并且没有什么好理由不使用__builtin_expect),所以根本不需要更新它。我会在这个回答中加上一个说明,并尝试记得向微软提交问题,但我不指望能得到解决:( - nemequ

7
根据英特尔的分支和循环重组以防止错误预测文档:
为了有效地编写代码以利用这些规则,在编写if-else或switch语句时,请先检查最常见的情况,然后逐步向下处理不太常见的情况。
不幸的是,您不能编写类似以下内容的代码:
#define if_unlikely(cond) if (!(cond)); else 

因为VS10中的MSVC优化器忽略了这样的“提示”。
由于我更喜欢先处理代码中的错误,所以我似乎写的代码效率较低。幸运的是,当CPU第二次遇到该分支时,它将使用其统计数据而不是静态提示。

7
回答Xentrax:你关于MSVC的陈述似乎与我的观察不符。我使用的是VS 2010,当使用普通的if语句时,编译器会生成一个“jne”。但是当使用else代替时,编译器会生成一个“je”,并将else块放在主流程之后。因此,在MSVC中,你的定义似乎是有效的。现在,如果我能找到来自MS的声明,证明这是有意的、受支持的行为,那就好了。 - Ruben
@Ruben:自从 MSVC 2005,我就一直在利用这个代码生成技巧。在此之后的所有版本中,它仍然像这样工作。 - Yakov Galka

6

12
我认为这可能是危险的。根据微软的说法:“因为编译器基于__assume生成代码,如果__assume语句内部的表达式在运行时为假,则该代码可能无法正确运行。” - DigitalRoss
@Digital - 非常正确,MSDN文章中描述了这些陷阱。再次强调,应该避免使用这样的静态提示,如果有可能,尽量使用PGO。 - Michael
5
很抱歉,但 PGO 在任何稍微复杂的库中都很难搞定。我更了解自己代码中可能和不可能发生的情况。 - rustyx

5

现在微软表示他们已经实现了可能/不可能属性

但事实上,使用"可能"或不使用没有任何区别。

我已经编译了这些代码,产生了相同的结果

    int main()
    {
        int i = rand() % 2;
        if (i) [[likely]]
        {
           printf("Hello World!\n");
        }
        else
        {
            printf("Hello World2%d!\n",i);
        }
    }

    int main()
    {
        int i = rand() % 2;
        if (i)
        {
           printf("Hello World!\n");
        }
        else [[likely]]
        {
            printf("Hello World2%d!\n",i);
        }
    }

int pdb._main (int argc, char **argv, char **envp);
0x00401040      push    ebp
0x00401041      mov     ebp, esp
0x00401043      push    ecx
0x00401044      call    dword [rand] ; pdb.__imp__rand
                                   ; 0x4020c4
0x0040104a      and     eax, 0x80000001
0x0040104f      jns     0x401058
0x00401051      dec     eax
0x00401052      or      eax, 0xfffffffe ; 4294967294
0x00401055      add     eax, 1
0x00401058      je      0x40106d
0x0040105a      push    str.Hello_World ; pdb.___C__0O_NFOCKKMG_Hello_5World__CB_6
                                   ; 0x402108 ; const char *format
0x0040105f      call    pdb._printf ; int printf(const char *format)
0x00401064      add     esp, 4
0x00401067      xor     eax, eax
0x00401069      mov     esp, ebp
0x0040106b      pop     ebp
0x0040106c      ret
0x0040106d      push    0
0x0040106f      push    str.Hello_World2_d ; pdb.___C__0BB_DODJFBPJ_Hello_5World2__CFd__CB_6
                                   ; 0x402118 ; const char *format
0x00401074      call    pdb._printf ; int printf(const char *format)
0x00401079      add     esp, 8
0x0040107c      xor     eax, eax
0x0040107e      mov     esp, ebp
0x00401080      pop     ebp
0x00401081      ret

2
这是正确的。[[likely]]和[[unlikely]]都是noops。跟踪此功能被挂钩到优化器的位置在这里:https://developercommunity2.visualstudio.com/t/unlikelylikely-have-no-effect-i/1383350 - Mac

2
由于这个问题已经很老了,那些声称在MSVC中没有[[likely]]/[[unlikely]]或者没有影响的答案已经过时了。
最新的MSVC支持在/std:c++20/std:c++latest模式下使用[[likely]]/[[unlikely]]
请参见Godbolt编译器浏览器上的演示,展示了它们之间的区别。
如上面链接所示,对于if-else语句在x86/x64上的一个明显效果是条件跳转将会是不太可能的分支。在C++20之前并且支持VS版本之前,可以通过将可能的分支放入if部分,将不太可能的分支放入else部分,并根据需要取反条件来实现相同的效果。
请注意,这种优化的效果是微小的。对于在紧密循环中频繁调用的代码,动态分支预测会自动处理正确的事情。

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