为什么GCC不能优化"x &&(x&4242)"到"x&4242"中的逻辑按位AND对?

Joh*_*nck 66 c++ optimization gcc compiler-optimization

以下是我声称完成同样事情的两个功能:

bool fast(int x)
{
  return x & 4242;
}

bool slow(int x)
{
  return x && (x & 4242);
}
Run Code Online (Sandbox Code Playgroud)

从逻辑上讲,他们做同样的事情,只是为了100%肯定我写了一个测试,通过他们两个运行所有40亿个可能的输入,并且他们匹配.但汇编代码是一个不同的故事:

fast:
    andl    $4242, %edi
    setne   %al
    ret

slow:
    xorl    %eax, %eax
    testl   %edi, %edi
    je      .L3
    andl    $4242, %edi
    setne   %al
.L3:
    rep
    ret
Run Code Online (Sandbox Code Playgroud)

令我感到惊讶的是,GCC无法实现消除冗余测试的逻辑上的飞跃.我用-O2,-O3和-Os尝试了g ++ 4.4.3和4.7.2,所有这些都生成了相同的代码.该平台是Linux x86_64.

有人可以解释为什么GCC不够聪明,不能在两种情况下生成相同的代码吗?我还想知道其他编译器是否可以做得更好.

编辑以添加测试工具:

#include <cstdlib>
#include <vector>
using namespace std;

int main(int argc, char* argv[])
{
    // make vector filled with numbers starting from argv[1]
    int seed = atoi(argv[1]);
    vector<int> v(100000);
    for (int j = 0; j < 100000; ++j)
        v[j] = j + seed;

    // count how many times the function returns true
    int result = 0;
    for (int j = 0; j < 100000; ++j)
        for (int i : v)
            result += slow(i); // or fast(i), try both

    return result;
}
Run Code Online (Sandbox Code Playgroud)

我在Mac OS上用-O3测试了上面的clang 5.1.使用时间为2.9秒,使用时间为fast()3.8秒slow().如果我改为使用全零的向量,则两个函数之间的性能没有显着差异.

MSa*_*ers 50

究竟为什么它能够对代码进行优化?您假设任何有效的转换都将完成.这根本不是优化者的工作方式.他们不是人工智能.他们只是通过参数化替换已知模式来工作.例如,"Common Subexpression Elimination"扫描表达式以查找常见的子表达式,如果不改变副作用,则向前移动它们.

(顺便说一句,CSE表明优化者已经非常清楚可能存在副作用的代码运动.他们知道你必须要小心&&.是否expr && expr可以进行CSE优化取决于副作用expr. )

总而言之:您认为哪种模式适用于此?

  • 我们知道GCC有很多种方法可以建立等价的算术表达式和表达式之间的关系,如果不是之前的话,它会在发出代码时使用.人们可能会天真地假设这样的模式:"给定无副作用的A && B`,如果`(bool)B`是假的,只要`(bool)A`为假,转换为'B`".但是,当"A"评价比"B"更快时,这会产生性能影响.这些影响甚至可能是问题的答案,我只是不知道. (9认同)
  • @SteveJessop:特殊的形式`A && B`,其中`B`意味着'A`并不是很少见; 在计算昂贵的"B"之前首先计算一个快速的"A"表达式是一种常见的(人类)优化.例如,在创建`regex`之前检查`!string :: empty()`,即使该正则表达式在空输入上做正确的事情.因此,作为优化编写者,我会单独留下那些"A && B".这确实可能是答案. (7认同)
  • 是的.它可能不是高优先级,但我认为,对于算术表达式,编译器是否应该对"A"和"B"的性能进行自己的评估仍然存在一个问题,忽略了一些愚蠢的内脏对内脏的看法.学科.这是我想要的编译器;-)正如您所指出的,模板生成代码,其中特定类型的情况"明显地"写错了,但我不想专门针对性能. (4认同)
  • 或者不要制作真相表.SMT求解器可以轻松解决这个问题.显然,并非所有问题都可以解决这个问题. (2认同)

Nem*_*emo 33

你是正确的,这在优化器中似乎是一个缺陷,可能是一个彻头彻尾的错误.

考虑:

bool slow(int x)
{
  return x && (x & 4242);
}

bool slow2(int x)
{
  return (x & 4242) && x;
}
Run Code Online (Sandbox Code Playgroud)

GCC 4.8.1(-O3)发布的装配:

slow:
    xorl    %eax, %eax
    testl   %edi, %edi
    je      .L2
    andl    $4242, %edi
    setne   %al
.L2:
    rep ret

slow2:
    andl    $4242, %edi
    setne   %al
    ret
Run Code Online (Sandbox Code Playgroud)

换句话说,slow2是错误的名字.

我只是偶尔向GCC提供补丁,所以我的观点是否有任何重要性值得商榷:-).但在我看来,GCC优化其中一个而不是另一个当然是奇怪的.我建议提交一份错误报告.

[更新]

令人惊讶的是,微小的变化似乎有很大的不同.例如:

bool slow3(int x)
{
  int y = x & 4242;
  return y && x;
}
Run Code Online (Sandbox Code Playgroud)

...再次生成"慢"代码.我对这种行为没有假设.

您可以在此处在多个编译器上试验所有这些.

  • 逻辑AND是短路的,对吧?这可以解释为什么把它放在左侧那样做. (4认同)
  • @ 2rs2ts:优化器绝对必须知道,例如使CSE成为可能.如果CSE有副作用(每次都应该发生),那是不允许的. (2认同)
  • 顺便说一句,clang 优化了所有这些,但即使是 8 年后的当前 GCC 也没有:https://gcc.godbolt.org/z/7nbxfaE1x。同意“slow3”令人惊讶。 (2认同)

aus*_*len 13

这就是你的代码在ARM中看起来如何slow在输入0时运行得更快.

fast(int):
    movw    r3, #4242
    and r3, r0, r3
    adds    r0, r3, #0
    movne   r0, #1
    bx  lr
slow(int):
    cmp r0, #0
    bxeq    lr
    movw    r3, #4242
    and r3, r0, r3
    adds    r0, r3, #0
    movne   r0, #1
    bx  lr
Run Code Online (Sandbox Code Playgroud)

但是,无论如何,当你开始使用这些简单的函数时,GCC会非常好地进行优化.

bool foo() {
    return fast(4242) && slow(42);
}
Run Code Online (Sandbox Code Playgroud)

foo():
    mov r0, #1
    bx  lr
Run Code Online (Sandbox Code Playgroud)

我的观点是,有时这样的代码需要更多的上下文进一步优化,为什么优化器(改进器!)的实现者应该打扰?

另一个例子:

bool bar(int c) {
  if (fast(c))
    return slow(c);
}
Run Code Online (Sandbox Code Playgroud)

bar(int):
    movw    r3, #4242
    and r3, r0, r3
    cmp r3, #0
    movne   r0, #1
    bxne    lr
    bx  lr
Run Code Online (Sandbox Code Playgroud)

  • 好吧,呃 - 如果你传入常数,GCC可以直接计算结果.对于`constexpr`来说,它具有这种能力. (9认同)
  • 问题在于两个片段对于40亿个可能的输入是相同的,而不仅仅是一个.编译器测试您明确提供的一组参数是合理的,但不能测试所有40亿个可能的参数. (2认同)

Yve*_*ust 8

要执行此优化,需要研究两个不同情况的表达式:x == 0简化到false,并x != 0简化为x & 4242.然后足够聪明地看到第二个表达式的值也会产生正确的值,即使对于x == 0.

让我们假设编译器执行案例研究并找到简化.

如果x != 0,表达式简化为x & 4242.

如果x == 0,表达式简化为false.

简化后,我们得到两个完全不相关的表达式.为了协调它们,编译器应该提出不自然的问题:

如果x != 0,可以false代替使用x & 4242吗?[没有]

如果x == 0,可以x & 4242代替使用false吗?[是]

  • 我不认为当`x`是`&&`*的操作数时,优化器调查`x == 0`作为特殊情况*是一个很大的飞跃.看一下二元选择的两条腿并不是一个不切实际的蛮力!优化器要问的唯一问题是",`(bool)(x&4242)`暗示`(bool)x`?".很容易看出它(无论如何,没有比GCC用算术表达式做出的大量针孔优化更难看到),因此如果认为该问题值得研究,优化器可以看到该分支在逻辑上是多余的. (2认同)

Mar*_*arc 6

我工作的最后一个编译器没有进行这些类型的优化.编写优化器以利用与组合二进制和逻辑运算符相关的优化不会加速应用程序.这样做的主要原因是人们不经常使用这样的二元运算符.许多人对二元运算符感到不舒服,那些通常不会编写需要优化的无用操作的人.

如果我去写作的麻烦

return (x & 4242)
Run Code Online (Sandbox Code Playgroud)

我理解这意味着为什么我会为额外的步骤而烦恼.出于同样的原因,我不会写这个次优的代码

if (x==0) return false;
if (x==1) return true;
if (x==0xFFFEFD6) return false;
if (x==4242) return true;
return (x & 4242)
Run Code Online (Sandbox Code Playgroud)

可以更好地利用编译器开发人员的时间,而不是优化那些没有区别的东西.在编译器优化中有很多更大的鱼可以炒.


and*_*ett 5

值得注意的是,这种优化在所有机器上都无效.特别是如果你在使用负数的补码表示的机器上运行,那么:

-0 & 4242 == true
-0 && ( -0 & 4242 ) == false
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会从未支持此类陈述,但C标准允许这些陈述.

  • 一个有趣的观察,但不是一个"重要的"观察.这个问题是关于特定编译器的行为,因此它已经与平台有关.GCC支持的每个平台 - 事实上,过去40多年来的每个平台 - 都使用了两个补充. (6认同)