私が主張する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つの関数のパフォーマンスに大きな違いはありません。
これは、オプティマイザの欠陥であり、場合によっては完全なバグであると思われます。
検討してください:
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 で試すことができます。
正確な理由すべきコードを最適化できるのか?機能するすべての変換が行われると想定しています。それはオプティマイザがどのように機能するかではありません。それらは人工知能ではありません。それらは、既知のパターンをパラメトリックに置き換えるだけで機能します。例えば。 「Common Subexpression Elimination」は、式をスキャンして一般的な部分式を探し、副作用が変わらない場合はそれらを前に移動します。
(ところで、CSEは、オプティマイザが副作用の可能性がある場合に許可されるコードの移動をすでに十分に認識していることを示しています。&&
に注意する必要があることを彼らは知っています。expr && expr
をCSE-にできるかどうか最適化されているかどうかは、expr
の副作用に依存します。)
要約すると、ここでどのパターンが当てはまると思いますか?
これは コードの見え方 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
私が取り組んだ最後のコンパイラは、この種の最適化を行いませんでした。バイナリ演算子と論理演算子の組み合わせに関連する最適化を利用するオプティマイザを作成しても、アプリケーションの速度は向上しません。これの主な理由は、人々がそのような二項演算子をあまり使用しないことです。多くの人々は、バイナリ演算子に慣れておらず、通常、最適化が必要な無駄な操作を記述しません。
書面のトラブルに行ったら
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)
違いのないものを最適化するよりも、コンパイラの開発者の時間を使うほうが良いです。コンパイラの最適化には、大きな魚がたくさんいます。
この最適化はすべてのマシンで有効であるとは限らないことに注意してください。特に、負の数の1の補数表現を使用するマシンで実行する場合は、次のようになります。
-0 & 4242 == true
-0 && ( -0 & 4242 ) == false
GCCはそのような表現をサポートしたことがありませんが、C標準では許可されています。
Cでは、符号付き整数型よりも符号なし整数型の動作に対する制限が少なくなっています。特に負の値は、ビット演算で奇妙なことを合法的に行う可能性があります。ビット操作の引数に法的に制約のない動作がある場合、コンパイラーはそれらを削除できません。
たとえば、「x/y == 1またはtrue」は、ゼロで除算するとプログラムがクラッシュする可能性があるため、コンパイラは除算の評価を無視できません。負の符号付き値とビット演算は、一般的なシステムでは実際にそのようなことをすることはありませんが、言語の定義によって除外されているかどうかはわかりません。
符号なし整数でコードを試し、それが役立つかどうかを確認する必要があります。もしそうなら、それは式ではなく型の問題であることを知るでしょう。