我可以通过给出整数范围来提示优化器吗?

rol*_*vax 171 c++ optimization integer range compiler-optimization

我正在使用一种int类型来存储一个值.根据程序的语义,值总是在很小的范围内变化(0 - 36),并且仅使用int(不是a char)因为CPU效率.

似乎可以在如此小的整数范围内执行许多特殊的算术优化.可以将对这些整数的许多函数调用优化为一小组"神奇"操作,并且甚至可以将某些函数优化为表查找.

那么,是否有可能告诉编译器这int总是在那么小的范围内,并且编译器是否可以进行这些优化?

小智 227

对的,这是可能的.例如,gcc您可以使用__builtin_unreachable告诉编译器有关不可能的条件,如下所示:

if (value < 0 || value > 36) __builtin_unreachable();
Run Code Online (Sandbox Code Playgroud)

我们可以在宏中包装上面的条件:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

assume(x >= 0 && x <= 10);
Run Code Online (Sandbox Code Playgroud)

如您所见,gcc根据以下信息执行优化:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}
Run Code Online (Sandbox Code Playgroud)

生产:

func(int):
    mov     eax, 17
    ret
Run Code Online (Sandbox Code Playgroud)

然而,一个缺点是,如果您的代码打破了这样的假设,那么您将得到未定义的行为.

即使在调试版本中,它也不会在发生这种情况时通知您.要更容易地使用假设来调试/测试/捕获错误,您可以使用混合的假设/断言宏(@David Z的信用),如下所示:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif
Run Code Online (Sandbox Code Playgroud)

在调试版本(具有NDEBUG 定义),它的工作原理像一个普通的assert,印刷错误消息和abort"ING程序,并在发布版本它利用一个假设的,产生优化代码.

但请注意,它不能代替常规assert- cond仍然在发布版本中,所以你不应该做类似的事情assume(VeryExpensiveComputation()).

  • @ user3528438,`__ builtin_expect`是一个非严格的提示.`__builtin_expect(e,c)`应该读作"`e`最有可能评估为'c`",并且可以用于优化分支预测,但它不会将`e`限制为始终为`c` ,所以不允许优化器丢弃其他情况.[看看如何在大会中组织分支](https://godbolt.org/g/Kv5zdM). (19认同)
  • 除非有一些怪癖但我不知道这是一个坏主意,将它与`assert`结合起来可能是有意义的,例如,当`NDEBUG'未定义时,将`assume`定义为`assert`,以及`__builtin_unreachable ()`定义`NDEBUG`时.这样您就可以从生产代码中获得假设,但在调试版本中,您仍然可以进行明确的检查.当然,你必须做足够的测试,以确保自己的假设_will_在野外得到满足. (14认同)
  • 但是,似乎gcc [不能](https://godbolt.org/g/9rZqE2)优化函数到魔术操作或表查找OP预期. (6认同)
  • 理论上,任何无条件地导致未定义行为的代码都可以用来代替`__builtin_unreachable()`. (6认同)
  • @Xofo,没有得到它,在我的例子中已经发生了这种情况,因为编译器从代码中删除了`return 2`分支. (5认同)
  • 这真的很酷; 我不知道存在这样的事情. (4认同)
  • @CodesInChaos,理论上 - 是的,在实践中 - 坚持编译器批准的产生未定义行为的方法.gcc处理UB的各种情况[非常不同](https://godbolt.org/g/DYd96v). (4认同)
  • @CodesInChaos任何无条件地在抽象机器中导致未定义行为的代码都不够好,因为编译器扩展可能会定义行为.例如,有符号整数溢出可能会根据编译器选项被定义为中止程序,需要特殊代码才能确保程序真正中止. (4认同)
  • @wingleader,通常的做法是确保宏调用后的分号不会引入错误:http://stackoverflow.com/questions/154136/why-use-apparently-meaningless-do-while-and-if-else-声明式-CC-宏 (3认同)
  • @DavidZ,好点,但不要急于替换你的断言.`assert`的最大优点是你可以'断言(VeryExpensiveCheck())`并且仍然不会减慢释放构建的速度.你提出的`断言/假设'的东西比普通的`__builtin_unreachable`更好,但它的用法仍局限于非常具体的情况. (3认同)
  • 这对gcc和clang非常有用,但是ICC实际上进行了检查,并且分支到函数的`ret`指令之外!(https://godbolt.org/g/cyX1oU).然后不利用范围信息.所以我想``ifdef'围绕`assume()`宏更好地检查和排除ICC,而不仅仅是`__GNUC__> = what`. (3认同)
  • 一个简单的`断言'不是这样做的吗?我希望`assert`可以完成这个答案的'假设'宏所做的一切,同时还可以在调试模式下提供更多有用的输出. (2认同)
  • 为什么`assume`宏被定义为`do while`循环? (2认同)
  • @deniss哦,当然。我只是将其作为“assume”的稍微改进版本,用于您已经决定要使用它的情况,而不是作为“assert”语句的一般替代品。 (2认同)
  • 发布版本的问题似乎是if检查可能会留在代码中并导致性能问题? (2认同)
  • @2501 是的。虽然编译器知道“if”的结果,但它仍然必须执行条件才能获得任何副作用。例如,假设编译器知道“f”将在“if (f()) ...”中返回“false”。它仍然必须执行足够的“f”,因为“f”可能是“printf("hi"); 返回假;`。因此,知道某件事会产生特定的结果并不意味着您不必执行任何该事情。 (2认同)
  • icc 非常擅长自动矢量化,但有时在其他方面比 gcc 或 clang 差。我之前肯定见过来自 icc 的整数位摆弄之类的脑死亡代码(尽管大部分来自 icc13,因为 Matt Godbolt 最近才用 ICC16 和 ICC17 更新了他的网站)。但即使 ICC17 在 bsr/bsf/tzcnt 方面也表现不佳[在这种情况下](http://stackoverflow.com/questions/40456481/gcc-compiles-leading-zero-count-poorly-unless-haswell-specified) `__builtin_ctzll()` (2认同)
  • MSVC具有[`__assume`](https://msdn.microsoft.com/zh-cn/library/1b3fsfxw.aspx),在此答案中可以代替#define假定(x)。 (2认同)

Lun*_*din 60

有标准的支持.你应该做的是包括stdint.h(cstdint)然后使用类型uint_fast8_t.

这告诉编译器您只使用0到255之间的数字,但是如果它提供更快的代码则可以使用更大的类型.类似地,编译器可以假设变量永远不会有大于255的值,然后相应地进行优化.

  • 这些类型的使用几乎没有应有的程度(我个人倾向于忘记它们的存在).它们提供既快速又便携的代码,非常出色.它们自1999年以来一直存在. (2认同)
  • 编译器只在系统上获取 0-255 范围信息,其中 `uint_fast8_t` 实际上是一个 8 位类型(例如 `unsigned char`),就像在 x86/ARM/MIPS/PPC 上一样(https://godbolt.org/ g/KNyc31)。在 [21164A 之前的早期 DEC Alpha](http://www.tldp.org/HOWTO/Alpha-HOWTO-8.html) 上,不支持字节加载/存储,因此任何合理的实现都将使用 `typedef uint32_t uint_fast8_t` . AFAIK,对于大多数编译器(如 gcc),没有一种机制可以让类型具有额外的范围限制,所以我很确定 `uint_fast8_t` 的行为与 `unsigned int` 或在这种情况下的任何内容完全相同。 (2认同)

Meh*_*dad 8

当您确切知道范围是什么时,当前答案是有利的,但如果您仍然希望在值超出预期范围时仍然需要正确的行为,那么它将无效.

对于这种情况,我发现这种技术可以工作:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}
Run Code Online (Sandbox Code Playgroud)

这个想法是代码数据权衡:你将1位数据(无论是x == c)移动到控制逻辑中.
这暗示优化器x实际上是一个已知常量c,鼓励它内联和优化第一次调用foo与其余调用分开,可能相当大.

确保实际将代码分解为单个子例程foo,但不要复制代码.

例:

要使这种技术起作用,你需要有点幸运 - 有些情况下编译器决定不静态地评估它们,而且它们有点武断.但是当它工作时,它运作良好:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}
Run Code Online (Sandbox Code Playgroud)

只需使用-O3并注意预评估常数0x200x30e汇编输出.


Sto*_*ica 6

我只想说,如果你想要一个更标准的C++解决方案,你可以使用该[[noreturn]]属性编写自己的unreachable.

因此,我将重新设计deniss的优秀示例来证明:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}
Run Code Online (Sandbox Code Playgroud)

其中你可以看到,结果几乎相同的代码:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret
Run Code Online (Sandbox Code Playgroud)

当然,缺点是你得到一个[[noreturn]]功能确实会返回的警告.