これは、質問に対する Mysticial による素晴らしい回答を読んでいるときに思い浮かんだ質問です: なぜ速いのですかソートされていない配列よりもソートされた配列を処理するには ?
関係するタイプのコンテキスト:
const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
彼の回答では、Intel Compiler(ICC)がこれを最適化することを説明しています。
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += data[c];
...これと同等のものに:
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
オプティマイザはこれらが同等であることを認識しているため、 ループを交換し 、内部ループの外側に分岐を移動します。非常に賢い!
しかし、なぜそうしないのですか?
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
うまくいけば、Mysticial(または他の誰か)が同様に素晴らしい答えを与えることができます。私は以前に他の質問で議論された最適化について学んだことがないので、これに本当に感謝しています。
コンパイラは一般的に変換できません
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
に
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
後者は前者では発生しない符号付き整数のオーバーフローを引き起こす可能性があるためです。符号付き2の補数整数のオーバーフローに対するラップアラウンド動作が保証されていても、結果が変更されます(data[c]
が30000の場合、製品は通常の32ビットint
sに対して-1294967296
になりますラップアラウンドで、100000回sum
に30000を追加すると、オーバーフローしない場合は、sum
を3000000000増やします)。符号なしの量についても同じことが当てはまり、異なる数で、100000 * data[c]
のオーバーフローは通常、最終結果に現れてはならない2^32
を法とするリダクションを導入します。
に変換できます
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000LL * data[c]; // resp. 100000ull
ただし、通常どおり、long long
がint
よりも十分に大きい場合。
なぜそれができないのか、私にはわかりませんが、それは Mysticialが述べた 、「明らかに、ループ交換後にループ崩壊パスを実行しません」と推測します。
ループ交換自体は一般に有効ではないことに注意してください(符号付き整数の場合)。
for (int c = 0; c < arraySize; ++c)
if (condition(data[c]))
for (int i = 0; i < 100000; ++i)
sum += data[c];
どこでオーバーフローにつながる可能性があります
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (condition(data[c]))
sum += data[c];
しません。条件により、追加されるすべてのdata[c]
が同じ符号を持つことが保証されるため、ここで問題はありません。
ただし、コンパイラがそれを考慮したかどうかはあまりわかりません(@Mysticial、data[c] & 0x80
などの条件で試してみてください。正負の値に当てはまるのでしょうか?)。コンパイラに無効な最適化を行わせました(たとえば、数年前、ICC(11.0、iirc)で1.0/n
where n
でsigned-32-bit-int-to-double変換を使用しましたはunsigned int
でした。gccの出力の約2倍の速さでした。しかし、間違って、多くの値が2^31
よりも大きかったです。
この回答は、リンクされた特定のケースには適用されませんが、質問のタイトルには適用され、将来の読者にとって興味深い場合があります。
精度が有限であるため、繰り返し浮動小数点加算は乗算と同等ではありません。考慮してください:
float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;
float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;
float result2 = init;
result2 += step * count;
cout << (result1 - result2);
コンパイラには、最適化を行うさまざまなパスが含まれています。通常、各パスでステートメントの最適化またはループの最適化が行われます。現在、ループヘッダーに基づいてループ本体の最適化を行うモデルはありません。これは検出が難しく、あまり一般的ではありません。
行われた最適化は、ループ不変コードの動きでした。これは、一連の手法を使用して実行できます。
まあ、私たちは整数演算について話していると仮定して、いくつかのコンパイラがこの種の最適化を行うかもしれないと思います。
同時に、一部のコンパイラーは、繰り返し加算を乗算に置き換えるとコードのオーバーフロー動作が変わる可能性があるため、それを拒否する場合があります。 unsigned
整数型の場合、オーバーフロー動作は言語によって完全に指定されるため、違いはありません。しかし、署名されたものについては(おそらく2の補数プラットフォームではないかもしれません)。符号付きオーバーフローが実際にCで未定義の動作を引き起こすことは事実です。つまり、オーバーフローのセマンティクスを完全に無視しても問題ありません。すべてのコンパイラーがそれを行うのに十分な勇気があるわけではありません。多くの場合、「Cは単なる上位レベルのアセンブリ言語です」という群衆から多くの批判が寄せられています。 (GCCがストリクトエイリアスセマンティクスに基づく最適化を導入したときに何が起こったのか覚えていますか?)
歴史的に、GCCはそのような抜本的な措置を講じるのに必要なものを備えたコンパイラとしての地位を示してきましたが、他のコンパイラは、言語によって定義されていない場合でも、「ユーザーが意図した」認識された動作に固執することを好む場合があります。
この種の最適化には概念的な障壁があります。コンパイラの作成者は、 強度の削減 -に多くの労力を費やし、乗算を加算とシフトに置き換えます。彼らは、乗算が悪いと考えることに慣れます。そのため、一方を逆方向に移動する必要がある場合は、驚くほど直感に反します。だから誰もそれを実装しようとは思わない。
コンパイラーを開発および保守する人々は、作業に費やす時間とエネルギーが限られているため、一般的に、ユーザーが最も重視していることに焦点を当てたいと考えています。彼らは愚かなコードを高速コードに変える方法を見つけようとして時間を費やしたくありません。それがコードレビューの目的です。高レベル言語では、重要なアイデアを表現する「愚かな」コードが存在する場合があり、開発者がそれを速くする価値があります。たとえば、特定の種類のレイジーに構築されたHaskellプログラムメモリを割り当てないタイトループにコンパイルされるデータ構造を生成しました。しかし、そのようなインセンティブは、単純にループ加算を乗算に変換することに適用されません。高速にしたい場合は、乗算で書きます。