web-dev-qa-db-ja.com

GCCが「x &&(x&4242)」の論理ビット単位ANDペアを「x&4242」に最適化できないのはなぜですか?

私が主張する2つの関数は、まったく同じことです。

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

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

論理的にはそれらは同じことを行い、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
_

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;
}
_

-O3を搭載したMac OSのclang 5.1で上記をテストしました。 fast()を使用すると2.9秒、slow()を使用すると3.8秒かかりました。代わりにすべて0のベクトルを使用する場合、2つの関数のパフォーマンスに大きな違いはありません。

66
John Zwinck

これは、オプティマイザの欠陥であり、場合によっては完全なバグであると思われます。

検討してください:

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

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

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

言い換えると、 slow2の名前が間違っています。

私は時々GCCにパッチを提供しただけなので、私の見解が重要であるかどうかは議論の余地があります:-)。しかし、私の見解では、GCCがこれらの一方を最適化し、他方を最適化するのは確かに奇妙です。 バグレポートの提出 をお勧めします。

[更新]

驚くほど小さな変化が大きな違いを生むように見えます。例えば:

bool slow3(int x)
{
  int y = x & 4242;
  return y && x;
}

...再び「遅い」コードを生成します。私にはこの行動についての仮説はありません。

これらすべてを複数のコンパイラ here で試すことができます。

33
Nemo

正確な理由すべきコードを最適化できるのか?機能するすべての変換が行われると想定しています。それはオプティマイザがどのように機能するかではありません。それらは人工知能ではありません。それらは、既知のパターンをパラメトリックに置き換えるだけで機能します。例えば。 「Common Subexpression Elimination」は、式をスキャンして一般的な部分式を探し、副作用が変わらない場合はそれらを前に移動します。

(ところで、CSEは、オプティマイザが副作用の可能性がある場合に許可されるコードの移動をすでに十分に認識していることを示しています。&&に注意する必要があることを彼らは知っています。expr && exprをCSE-にできるかどうか最適化されているかどうかは、exprの副作用に依存します。)

要約すると、ここでどのパターンが当てはまると思いますか?

50
MSalters

これは コードの見え方 in ARMこれにより、入力が0のときにslowがより速く実行されるはずです。

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

しかし、とにかくそのような些細な関数を使い始めると、GCCは非常にうまく最適化します。

bool foo() {
    return fast(4242) && slow(42);
}

なる

foo():
    mov r0, #1
    bx  lr

私のポイントは、時々そのようなコードはさらに最適化するためにより多くのコンテキストを必要とするので、なぜオプティマイザの実装者(改善者!)は煩わしいのでしょうか?

もう一つの例:

bool bar(int c) {
  if (fast(c))
    return slow(c);
}

なる

bar(int):
    movw    r3, #4242
    and r3, r0, r3
    cmp r3, #0
    movne   r0, #1
    bxne    lr
    bx  lr
13
auselen

私が取り組んだ最後のコンパイラは、この種の最適化を行いませんでした。バイナリ演算子と論理演算子の組み合わせに関連する最適化を利用するオプティマイザを作成しても、アプリケーションの速度は向上しません。これの主な理由は、人々がそのような二項演算子をあまり使用しないことです。多くの人々は、バイナリ演算子に慣れておらず、通常、最適化が必要な無駄な操作を記述しません。

書面のトラブルに行ったら

return (x & 4242)

そして、それがどうして私がなぜ追加のステップに悩むのかを理解しています。同じ理由で、この次善のコードは書きません

if (x==0) return false;
if (x==1) return true;
if (x==0xFFFEFD6) return false;
if (x==4242) return true;
return (x & 4242)

違いのないものを最適化するよりも、コンパイラの開発者の時間を使うほうが良いです。コンパイラの最適化には、大きな魚がたくさんいます。

6
Marc

この最適化はすべてのマシンで有効であるとは限らないことに注意してください。特に、負の数の1の補数表現を使用するマシンで実行する場合は、次のようになります。

-0 & 4242 == true
-0 && ( -0 & 4242 ) == false

GCCはそのような表現をサポートしたことがありませんが、C標準では許可されています。

5
andrew.punnett

Cでは、符号付き整数型よりも符号なし整数型の動作に対する制限が少なくなっています。特に負の値は、ビット演算で奇妙なことを合法的に行う可能性があります。ビット操作の引数に法的に制約のない動作がある場合、コンパイラーはそれらを削除できません。

たとえば、「x/y == 1またはtrue」は、ゼロで除算するとプログラムがクラッシュする可能性があるため、コンパイラは除算の評価を無視できません。負の符号付き値とビット演算は、一般的なシステムでは実際にそのようなことをすることはありませんが、言語の定義によって除外されているかどうかはわかりません。

符号なし整数でコードを試し、それが役立つかどうかを確認する必要があります。もしそうなら、それは式ではなく型の問題であることを知るでしょう。

4
eggcrook