2のべき乗を使用している場合、ビットを左右にシフトすることは、ほとんどのCPU、おそらくすべてのCPUで乗算および除算演算よりも明らかに高速です。ただし、一部のリーダーおよび一部のアルゴリズムのコードの明瞭さは低下します。パフォーマンスのためにビットシフトは本当に必要ですか、それともコンパイラーまたはVMが大文字小文字を認識して最適化することを期待できますか(特に、2のべき乗がリテラルの場合)?I主にJavaおよび.NETの動作に興味がありますが、他の言語の実装への洞察も歓迎します。
今日のほとんどのコンパイラは、シフト演算に2のべき乗で乗算または除算を変換する以上のことを行います。最適化する場合、多くのコンパイラーは、2の累乗ではない場合でも、コンパイル時定数を使用して乗算または除算を最適化できます。乗算または除算は、一連のシフトおよび加算に分解できます。乗算または除算よりも、コンパイラはそれを使用します。
定数による除算の場合、コンパイラは多くの場合、演算を「マジックナンバー」とそれに続くシフトによる乗算に変換できます。多くの場合、乗算は除算演算よりもはるかに高速であるため、これはクロックサイクルを大幅に節約できます。
Henry Warrenの本、Hacker's Delight、 には、このトピックに関する豊富な情報があります。これについては、コンパニオンWebサイトでも詳しく説明されています。
次のディスカッションも参照してください(1つまたは2つのリンク)。
とにかく、これらすべては、コンパイラーが極小の最適化の退屈な詳細の面倒を見ることを可能にすることに要約されます。独自のシフトを行うとコンパイラーを凌outしてから数年が経ちました。
その価値のあるほぼすべての環境は、あなたのためにこれを最適化します。そうでない場合は、揚げる魚が大きくなります。真剣に、これについてもう少し考えて無駄にしないでください。パフォーマンスの問題が発生したときにわかります。そして、プロファイラーを実行した後、それが何を引き起こしているかがわかります。それを修正します。
「私のアプリケーションは遅すぎたので、ランダムにx * 2
with x << 1
とすべてが修正されました! "パフォーマンスの問題は、通常、同じ作業を1%高速に行う方法を見つけることではなく、作業量を1桁少なくする方法を見つけることで解決されます。
これらの場合、人間は間違っています。
99%は、最新の(およびすべての将来の)コンパイラーを2番目に推測しようとしています。
最新の(および将来の)JITを同時に2番目に推測しようとすると99.9%。
99.999%は、最新(および将来の)CPU最適化を2番目に推測しようとしています。
方法ではなく、達成したいことを正確に記述する方法でプログラムします。 JIT、VM、コンパイラ、およびCPUの将来のバージョンは、すべて独立して改善および最適化できます。非常に小さく具体的なものを指定すると、将来のすべての最適化の利点が失われます。
ほとんど確実に、シフト演算に対するリテラルの2のべき乗の最適化に依存できます。これは、コンパイラ構築の学生が学ぶ最初の最適化の1つです。 :)
ただし、これについて保証はないと思います。ソースコードはintentを反映する必要があります。オプティマイザーに何をすべきかを伝えるのではなく。数量を増やす場合は、乗算を使用します。ビットフィールドをある場所から別の場所に移動する場合(RGBカラー操作を考える)、シフト操作を使用します。いずれにしても、ソースコードは実際に行っていることを反映します。
シフトダウンと除算は、(Javaでは確かに)負の奇数に対して異なる結果を与えることに注意してください。
int a = -7;
System.out.println("Shift: "+(a >> 1));
System.out.println("Div: "+(a / 2));
プリント:
Shift: -4
Div: -3
Javaには符号なしの数値がないため、Javaコンパイラがこれを最適化することは実際には不可能です。
私がテストしたコンピューターでは、整数の除算は他の操作よりも4から10倍遅いです。
コンパイラーが除算を2の倍数で置き換えても違いがない場合、2の倍数ではない除算は大幅に遅くなります。
たとえば、255個の多くの除算を持つ(グラフィック)プログラムがあります。実際、私の計算は次のとおりです。
r = (((top.R - bottom.R) * alpha + (bottom.R * 255)) * 0x8081) >> 23;
以前の計算よりもはるかに高速であることを確認できます。
r = ((top.R - bottom.R) * alpha + (bottom.R * 255)) / 255;
いいえ、コンパイラは最適化のすべてのトリックを実行できません。
私は「それが重要だとあなたは何をしていますか?」と尋ねます。最初に、読みやすさと保守性のためにコードを設計します。ビットシフトを行うことと標準の乗算を行うことでパフォーマンスの違いが生じる可能性は非常に小さいです。
ハードウェアに依存します。マイクロコントローラーまたはi386について話している場合、シフトはより高速になる可能性がありますが、いくつかの答えが示すように、コンパイラーは通常最適化を行います。
最新の(Pentium Pro以降の)ハードウェアでは、パイプライン処理によりこれはまったく無関係になり、beatられたパスから外れると、通常、獲得できるよりも多くの最適化が失われます。
マイクロ最適化は時間の浪費であるだけでなく、正しく行うことも非常に困難です。
このマイクロベンチマーク の結果によると、シフトは分割の2倍の速さです(Oracle Java 1.7.0_72)。
コンパイラー(コンパイル時定数)またはJIT(実行時定数)が除数または被乗数が2のべき乗であり、整数演算が実行されていることを知っている場合、それをシフトに変換します。
このコードを書いたばかりで、1つずつシフトすることは2を乗算するよりも実際に遅いことに気付いたのでst然としました!
(編集:Michael Myersの提案の後、オーバーフローを停止するようにコードを変更しましたが、結果は同じです!ここで何が間違っていますか?)
import Java.util.Date;
public class Test {
public static void main(String[] args) {
Date before = new Date();
for (int j = 1; j < 50000000; j++) {
int a = 1 ;
for (int i = 0; i< 10; i++){
a *=2;
}
}
Date after = new Date();
System.out.println("Multiplying " + (after.getTime()-before.getTime()) + " milliseconds");
before = new Date();
for (int j = 1; j < 50000000; j++) {
int a = 1 ;
for (int i = 0; i< 10; i++){
a = a << 1;
}
}
after = new Date();
System.out.println("Shifting " + (after.getTime()-before.getTime()) + " milliseconds");
}
}
結果は次のとおりです。
639ミリ秒の乗算
718ミリ秒のシフト
ほとんどのコンパイラは、必要に応じて乗算と除算をビットシフトに変換します。これは最も簡単な最適化の1つです。そのため、特定のタスクに対してより読みやすく適切なことを行う必要があります。