Cのシフト演算子を使用した乗算と除算は実際には高速ですか?


288

乗算と除算は、ビット演算子を使用して実現できます。

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

等々。

直接(i<<3)+(i<<1)使用i*10するよりも、sayを使用して10を乗算する方が実際に高速ですか?この方法で乗算または除算できない入力の種類はありますか?


8
実際には、2の累乗以外の定数による安価な除算が可能ですが、質問の "/ Division…/ divided"で正義を実行していないトリッキーなサブジェット。たとえばhackersdelight.org/divcMore.pdfを参照してください(または、できれば「ハッカーの喜び」という本を入手してください)。
Pascal Cuoq 2011年

46
簡単にテストできるもののようです。
juanchopanza 2011年

25
いつものように-それは異なります。むかしむかし私はこれをIntel 8088(IBM PC / XT)のアセンブラーで試してみましたが、乗算には膨大なクロックがかかりました。シフトと加算ははるかに速く実行されるため、良いアイデアのように思えました。ただし、バスユニットを乗算すると、命令キューを埋めることができるため、次の命令をすぐに開始できます。一連のシフトと追加の後、命令キューは空になり、CPUは次の命令がメモリからフェッチされるのを待たなければなりません(一度に1バイトです!)。測定、測定、測定!
Bo Persson、2011年

19
また、右シフトは符号なし整数に対してのみ明確に定義されていることに注意してください。符号付き整数の場合、左から0または最上位ビットが埋め込まれるかどうかは定義されていません。(そして、誰か(自分でも)が1年後にコードを読み取るのにかかる時間を忘れないでください!)
Kerrek SB

29
実際には、優れた最適化コンパイラーは、シフトが速い乗算と除算を実装します。
Peter G.

回答:


487

短い答え:ありそうもない。

長い答え:コンパイラにはオプティマイザがあり、ターゲットプロセッサアーキテクチャで可能な限り速く乗算する方法を知っています。最善の策は、コンパイラに意図を明確に伝え(つまり、i << 1ではなくi * 2)、コンパイラに最速のアセンブリ/マシンコードシーケンスを決定させることです。マイクロコード内のシフトと加算のシーケンスとして、プロセッサ自体が乗算命令を実装している可能性さえあります。

結論-これについて心配するのに多くの時間を費やさないでください。シフトするつもりなら、シフト。乗算するつもりなら、乗算します。意味的に最も明確なことを行います。同僚は後で感謝します。または、可能であれば、後でそうする場合は後で呪います。


31
はい、そうです、ほとんどすべてのアプリケーションの可能な利益は、導入されたあいまいさを完全に上回ります。この種の最適化を時期尚早に心配する必要はありません。セマンティックに明確なものを構築し、ボトルネックを特定して、そこから最適化します...
Dave

4
読みやすさと保守性を考慮して最適化すると、プロファイラーがホットコードパスであると言っていることを実際に最適化するのに費やす時間が増えることに同意します。
doug65536 2013年

5
これらのコメントは、コンパイラーにその仕事のやり方を伝えることから潜在的なパフォーマンスをあきらめているように聞こえます。これはそうではありません。実際には、シフトバージョンよりもx86上でより優れたコードを取得できます。コンパイラ出力をよく見る人(私のasm /最適化の回答の多くを参照)として、私は驚きません。コンパイラーを1つの方法で実行することが役立つ場合がありますが、これはその1つではありません。gccは重要なので、整数演算が得意です。gcc -O3return i*10
Peter Cordes、2016

があるarduinoスケッチをダウンロードしただけmillis() >> 2です。単に割り算を求めるのは多すぎるでしょうか?
Paul Wieland

1
cortex-a9(ハードウェア部門がない)のgccに対してi / 32vs i >> 5i / 4vs i >> 2を最適化-O3でテストしましたが、結果のアセンブリはまったく同じでした。最初に除算を使用するのは好きではありませんでしたが、それは私の意図を説明しており、出力は同じです。
robsn

91

ちょうど具体的な測定ポイント:何年も前に、ハッシュアルゴリズムの2つのバージョンをベンチマークしました。

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

そして

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

私がベンチマークしたすべてのマシンで、最初のマシンは少なくとも2番目のマシンと同じくらい高速でした。やや驚いたことに、それは時々より高速でした(例えば、Sun Sparc)。ハードウェアが高速な乗算をサポートしていなかった場合(そしてそのほとんどが当時はサポートされていませんでした)、コンパイラーは乗算をシフトと加算/減算の適切な組み合わせに変換します。また、最終的な目標を知っているため、シフトと追加/サブを明示的に記述した場合よりも少ない指示で実行できる場合があります。

これは15年前のようなものでした。うまくいけば、コンパイラーはそれ以来良くなっているだけなので、コンパイラーが正しいことを実行することにかなりの数を期待できます。(また、コードがC'ishのように見える理由は、15年以上前だったからです。std::string今日は明らかにイテレータを使用します。)


5
次のブログ投稿に興味があるかもしれませんが、著者は、現代の最適化コンパイラが、プログラマーがそれらをより効率的に数学的形式に考えるために使用する一般的なパターンをリバースエンジニアリングして、最も効率的な命令シーケンスを実際に生成するように見えると述べています。shape-of-code.coding-guidelines.com/2009/06/30/...
パスカルCuoq

@PascalCuoqこれについて本当に新しいことは何もありません。20年近く前のSun CCでもほとんど同じことを発見しました。
James Kanze、2012年

67

ここでの他のすべての良い答えに加えて、除算または乗算を意味するときにシフトを使用しない別の理由を指摘させてください。乗算と加算の相対的な優先順位を忘れて誰かがバグを導入するのを見たことがありません。メンテナンスプログラマがシフトを介した「乗算」は論理的には乗算であることを構文的にではなく、構文的には乗算と同じ優先順位ではないことを忘れたときに導入されたバグを見ました。x * 2 + zx << 1 + zはかなり異なります!

数値を扱う場合は、などの算術演算子を使用します+ - * / %。ビットの配列で作業している場合は、などのビット操作演算子を使用します& ^ | >>。それらを混ぜないでください。少しいじると算術の両方を持つ式は、発生するのを待っているバグです。


5
単純な括弧で回避できますか?
Joel B

21
@ジョエル:確かに。あなたがそれらを必要とすることを覚えているなら。私のポイントは、あなたがすることを忘れがちであるということです。「x << 1」を「x * 2」であるかのように読むという精神的習慣に入る人は、<<は乗算と同じ優先順位であると考える精神的習慣に入りますが、そうではありません。
Eric Lippert、2011年

1
ええと、「(hi << 256)+ lo」よりも「(hi << 8)+ lo」という表現の方がより意図的であることがわかります。おそらくそれは好みの問題ですが、ビットいじりを書く方がより明確な場合もあります。ほとんどの場合、私はあなたの主張に完全に同意します。
Ivan Danilov、2011年

32
@Ivan:そして、「(hi << 8)| lo」はさらに明確です。ビット配列の下位ビットの設定は、整数の加算ではありません。それはビットを設定するように設定ビットそのコードを書きます。
Eric Lippert、2011年

1
ワオ。以前はこのように考えていませんでした。ありがとう。
Ivan Danilov

50

これはプロセッサとコンパイラに依存します。一部のコンパイラはすでにこの方法でコードを最適化していますが、そうでないコンパイラもあります。したがって、この方法でコードを最適化する必要があるたびに確認する必要があります。

必死に最適化する必要がない限り、アセンブリ命令またはプロセッササイクルを保存するためだけにソースコードをスクランブルすることはありません。


3
概算を追加するだけです。通常の16ビットプロセッサ(80C166)では、2つの整数を追加すると1-2サイクル、乗算では10サイクル、除算では20サイクルになります。さらに、i * 10を複数の操作に最適化した場合のいくつかの移動操作(各mov別の+1サイクル)。最も一般的なコンパイラ(Keil社/タスキング)が2の累乗による乗算/部門用に最適化されない限りはありません
イェンス

55
そして一般的に、コンパイラはあなたよりもコードを最適化します。
user703016

「量」を乗算する場合、乗算演算子の方が一般に優れていますが、符号付きの値を2の累乗で除算すると、>>演算子はより高速で/あり、符号付きの値が負になる可能性がある場合は、意味的に同様に優れていることがよくあります。x>>4生成される値が必要な場合x < 0 ? -((-1-x)/16)-1 : x/16;、それはよりも明確であり、コンパイラーが後者の式をどのように素晴らしいものに最適化できるか想像できません。
スーパーキャット2013年

38

i * 10を直接使用するよりも、say(i << 3)+(i << 1)を使用して10を乗算する方が実際に高速ですか?

それはあなたのマシン上にあるかもしれないし、そうでないかもしれません-もし気にしたら、あなたの実際の使用法で測定してください。

ケーススタディ-486からコアi7まで

ベンチマークを有意義に行うことは非常に困難ですが、いくつかの事実を見ることができます。http://www.penguin.cz/~literakl/intel/s.html#SALおよび http://www.penguin.cz/~literakl/intel/i.html#IMULから、x86クロックサイクルの概念がわかります算術シフトと乗算に必要です。"486"(最新のものはリストされています)、32ビットのレジスタとイミディエイトに固執するとします。IMULは13〜42サイクル、IDIV 44です。各SALは2を取り、1を加えます。勝者のように。

最近、コアi7で:

http://software.intel.com/en-us/forums/showthread.php?t=61481から)

レイテンシは、整数の加算で1サイクル、整数の乗算では3サイクルです。レイテンシとスループットは、http://www.intel.com/products/processor/manuals/にある「インテル®64およびIA-32アーキテクチャー最適化リファレンスマニュアル」の付録Cにあります

(一部のIntelの宣伝文から)

Core i7はSSEを使用して、加算と乗算の同時命令を発行できるため、クロックサイクルあたりのピークレートは8浮動小数点演算(FLOP)になります。

これは、物事がどこまで進んだかについてのアイデアを与えます。ビットシフトと同様*に、90年代にまで真剣に取られた最適化のトリビアは、現在はもう使われていません。ビットシフトはさらに高速ですが、2の累乗ではないmul / divの場合、すべてのシフトを実行して結果を追加するまでに、速度は再び遅くなります。次に、より多くの命令はより多くのキャッシュ障害、より多くの潜在的なパイプラインの問題、より多くの一時レジスターの使用はスタックからのレジスター内容のより多くの保存と復元を意味するかもしれません...それはすべての影響を明確に定量化するにはあまりに複雑になりすぎますが、主に否定的です。

ソースコードの機能と実装

より一般的には、質問にはCおよびC ++のタグが付けられます。これらは第3世代言語として、基になるCPU命令セットの詳細を隠すように特別に設計されています。言語標準を満たすには、基盤となるハードウェアがサポートしていない場合でも、乗算およびシフト演算(およびその他の多くの演算)をサポートする必要があります。このような場合、他の多くの命令を使用して必要な結果を合成する必要があります。同様に、CPUに浮動小数点演算がなく、FPUがない場合は、浮動小数点演算のソフトウェアサポートを提供する必要があります。最新のCPUはすべてサポートし*<<、これは不合理に理論的かつ歴史的に見えるかもしれませんが、重要なことは、実装を選択する自由は両方の方向に進むということです。CPUが一般的なケースでソースコードで要求された操作を実装する命令を持っている場合でも、コンパイラは自由にコンパイラーが直面している特定のケースに適しているため、他の方法を選択してください。

例(架空のアセンブリ言語を使用)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

排他的または(xor)のような命令はソースコードとは関係ありませんが、それ自体で何かをXORすると、すべてのビットがクリアされるため、何かを0に設定するために使用できます。メモリアドレスを意味するソースコードは、何も使用されない可能性があります。

この種のハッキングは、コンピューターが使用されている限り使用されてきました。3GLの初期の頃は、開発者の取り込みを確保するために、コンパイラの出力は既存のハードコアの手最適化アセンブリ言語開発者を満足させる必要がありました。生成されたコードが遅くなったり、冗長になったり、それ以外に悪くなったりしないコミュニティ。コンパイラーは多くの優れた最適化をすぐに採用しました-個々のアセンブリ言語プログラマーが持つ可能性よりも、集中化された最適化されたストアになりました。誰かがその経験を彼らにフィードバックするまで、コンパイラは彼らが言われたようにちょうどそうする間、それを気を抜いて何か良いものを模索します。

したがって、特定のハードウェアでシフトと追加がさらに速い場合でも、コンパイラー作成者は、安全で有益な場合に正確に機能する可能性があります。

保守性

ハードウェアが変更された場合、再コンパイルしてターゲットCPUを確認し、別の最良の選択を行うことができますが、「最適化」を再確認したり、乗算を使用するコンパイル環境とシフトするコンパイル環境をリストしたりすることはほとんどありません。10年以上前に書かれた2のべき乗以外のすべてのビットシフトされた「最適化」を考えてみてください。現在のプロセッサーで実行すると、コードが遅くなります...!

ありがたいことに、GCCのような優れたコンパイラは通常、最適化が有効(つまり...main(...) { return (argc << 4) + (argc << 2) + argc; }-> imull $21, 8(%ebp), %eax)になっている場合、一連のビットシフトと算術を直接乗算で置き換えることができるため、コードを修正しなくても再コンパイルが役立つ場合がありますが、これは保証されません。

乗算または除算を実装する奇妙なビットシフトコードは、概念的に達成しようとしていたことをはるかに表現しにくいため、他の開発者は混乱し、混乱したプログラマーは、バグを導入したり、正気に見えるようにするために不可欠なものを削除したりする可能性が高くなります。それらが本当に具体的に有益な場合に自明ではないことだけを行い、それからそれらを適切に文書化する(ただし、とにかく直観的な他のものは文書化しない)場合、誰もが幸せになります。

一般的なソリューションと部分的なソリューション

int実際に値を保存するだけであるなどの追加の知識がある場合xyおよびz、それらの値に対して機能するいくつかの命令を実行して、コンパイラーがない場合よりも迅速に結果を得ることができる場合があります。その洞察とすべてのint価値のために働く実装が必要です。たとえば、あなたの質問を考えてみましょう:

乗算と除算はビット演算子を使用して実現できます...

あなたは乗算を説明していますが、除算はどうですか?

int x;
x >> 1;   // divide by 2?

C ++標準5.8によると:

-3- E1 >> E2の値は、E1を右シフトしたE2ビット位置です。E1に符号なしの型がある場合、またはE1に符号付きの型があり、負でない値である場合、結果の値は、E1をE2で累乗した数量2で割った商の整数部です。E1に符号付きの型と負の値がある場合、結果の値は実装定義です。

そのため、xが負の場合、ビットシフトは実装定義の結果になります。異なるマシンでは同じように機能しない可能性があります。しかし、/はるかに予測どおりに機能します。 (マシンごとに負の数の表現が異なり、したがって表現を構成するビット数が同じであっても範囲が異なるため、完全に一貫性がない場合もあります。)

「私は気にしない...それintは従業員の年齢を格納しているので、決して負になることはありません」と言うかもしれません。そのような特別な洞察があれば、はい- >>コードで明示的に行わない限り、安全な最適化はコンパイラーによって渡される可能性があります。しかし、それは危険であり、ほとんどの場合、この種の洞察が得られないのでほとんど役に立ちません。同じコードで作業している他のプログラマーは、あなたがデータのいくつかの異常な期待に家を賭けたことを知らないでしょう処理します...「最適化」のために、それらに対する完全に安全な変更は裏目に出る可能性があります。

この方法で乗算または除算できない入力の種類はありますか?

はい...上で述べたように、負の数は、ビットシフトによって「除算」されたときに、実装で定義された動作をします。


2
とてもいい答えです。Core i7と486の比較は賢明です!
ドリューホール

すべてのありふれたアーキテクチャーでは、時々役立つ方法とはintVal>>1異なるセマンティクスがintVal/2使用されます。ありふれたアーキテクチャがに対してもたらす価値をポータブルな方法で計算する必要がある場合intVal >> 1、式はかなり複雑で読みにくくする必要があり、に対して生成されたコードよりも実質的に劣るコードを生成する可能性がありますintVal >> 1
スーパーキャット

35

これをコンパイルして私のマシンで試してみました:

int a = ...;
int b = a * 10;

分解すると出力が生成されます:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

このバージョンは、手動で最適化されたコードよりも高速で、純粋なシフトと追加が行われます。

コンパイラーが何を考え出すかは本当にわからないので、コンパイラーが最適化できないことを知っている非常に正確な場合を除いて、通常の乗算を単純に記述し、彼が望む方法で最適化できるようにする方が良いでしょう。


1
ベクトルに関する部分をスキップした場合、これに対する大きな賛成票を獲得したでしょう。コンパイラーが乗算を修正できる場合、ベクトルが変更されないことも確認できます。
Bo Persson

コンパイラーは、いくつかの本当に危険な仮定をしなければ、ベクトルサイズが変化しないことをどのようにして知ることができますか?または、同時実行性について聞いたことがない...
Charles Goodwin

1
では、ロックのないグローバルベクトルをループしますか?そして、アドレスが取得されていないローカルベクトルをループし、constメンバー関数のみを呼び出します。少なくとも私のコンパイラは、ベクトルサイズが変化しないことを認識しています。(そしてすぐに誰かがチャットのために私たちにフラグを立てるでしょう:-)。
Bo Persson

1
@BoPersson最後に、結局のところ、コンパイラーが最適化できないという私の声明を削除しましたvector<T>::size()。私のコンパイラはかなり古くからありました!:)
user703016

21

通常、シフトは命令レベルで乗算するよりもはるかに高速ですが、時期尚早の最適化を行うために時間を浪費している可能性があります。コンパイラーは、コンパイル時にこれらの最適化を実行します。自分で行うと、読みやすさに影響し、パフォーマンスに影響を与えない可能性があります。プロファイリングを行い、これがボトルネックであるとわかった場合にのみ、このようなことをする価値があります。

実際、「マジックディビジョン」として知られるディビジョントリックは、実際に大きな利益を生み出すことができます。繰り返しますが、最初にプロファイルを作成して、それが必要かどうかを確認する必要があります。しかし、それを使用する場合、同じ除算セマンティクスに必要な命令を理解するのに役立つ便利なプログラムがあります。次に例を示します。http//www.masm32.com/board/index.php?topic = 12421.0

MASM32のOPのスレッドから持ち上げた例:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

生成されます:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@Drewが何らかの理由であなたのコメントで笑ってコーヒーをこぼしてしまった。ありがとう。
asawyer

30
数学を好きになることについてのランダムなフォーラムスレッドはありません。数学が好きな人なら誰でも、真の「ランダム」なフォーラムスレッドを生成するのがいかに難しいかを知っています。
Joel B

1
プロファイリングしてこれがボトルネック であることがわかり、代替案を再度実装してプロファイリングし、少なくとも10倍のパフォーマンス上の利点を得る場合にのみ、このようなことをする価値があります
リーライアン

12

シフトおよび整数乗算命令は、最近のほとんどのCPUで同様のパフォーマンスを発揮します。整数乗算命令は、1980年代には比較的遅くなりましたが、一般的にはこれは当てはまりません。整数乗算命令はレイテンシが高くなる可能性があるため、シフトの方が望ましい場合もあります。より多くの実行ユニットをビジー状態に保つことができる場合の同上(これは両方の方法を削減できますが)。

ただし、整数の除算はまだ比較的遅いため、2の累乗による除算の代わりにシフトを使用することは依然として有利であり、ほとんどのコンパイラーはこれを最適化として実装します。ただし、この最適化を有効にするには、被除数が符号なしであるか、正であることがわかっている必要があります。負の被除数の場合、シフトと除算は同等ではありません!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

出力:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

したがって、コンパイラを支援する場合は、被除数の変数または式が明示的に符号なしであることを確認してください。


4
整数乗算は、たとえばPlayStation 3のPPUでマイクロコード化され、パイプライン全体を停止させます。一部のプラットフォームでは整数の乗算を回避することをお勧めします:)
Maister

2
多くの符号なし除算は-コンパイラがその方法を知っていると仮定して-符号なし乗算を使用して実装されています。数クロックサイクルで1回または2回乗算すると、それぞれ40サイクル以上で除算と同じ処理を実行できます。
Olof Forshell 2012年

1
@Olof:true、ただし当然のことながらコンパイル時定数による除算にのみ有効
Paul R

4

ターゲットデバイス、言語、目的などに完全に依存します。

ビデオカードドライバーのピクセル処理 はい、そうです!

あなたの部門のための.NETビジネスアプリケーション?調査する理由すら絶対にありません。

モバイルデバイス向けの高性能ゲームの場合、検討する価値があるかもしれませんが、より簡単な最適化が実行された後でのみです。


2

絶対に必要で、コードの意図が乗算/除算ではなくシフトを必要とする場合を除き、実行しないでください。

典型的な日-いくつかのマシンサイクルを節約できる可能性があります(またはコンパイラーが最適化するものを知っているので緩い)が、コストはそれだけの価値はありません。あなたの同僚はあなたをののしります。

高負荷の計算では、これを実行する必要がある場合があります。保存された各サイクルは、実行時間の分を意味します。ただし、一度に1か所を最適化し、毎回パフォーマンステストを行って、本当に高速化したか、コンパイラロジックが壊れたかを確認する必要があります。


1

私の知る限り、一部のマシンでは、乗算に最大16〜32マシンサイクルが必要です。だから、はい、マシン・タイプに応じて、ビットシフト演算子は、より高速な乗算/除算よりも。

ただし、特定のマシンには、乗算/除算用の特別な命令を含む演算プロセッサがあります。


7
これらのマシン用のコンパイラーを作成している人々も、おそらくハッカーの喜びを読み、それに応じて最適化しています。
Bo Persson、2011年

1

ドリュー・ホールのマークされた答えに同意します。答えはいくつかの追加のメモを使用することもできます。

ソフトウェア開発者の大多数にとって、プロセッサとコンパイラはもはやこの問題に関係ありません。私たちのほとんどは8088とMS-DOSをはるかに超えています。それはおそらく、組み込みプロセッサ向けにまだ開発中の人にのみ関連します...

私のソフトウェア会社では、すべての数学にMath(add / sub / mul / div)を使用する必要があります。一方、データタイプ間で変換する場合はShiftを使用する必要があります。u >> は、 n / 256 ではなく n >> 8としてバイトに変換します。


私も同意します。私は無意識のうちに同じガイドラインに従いますが、正式な要件はこれまでありませんでした。
ドリューホール

0

符号付き整数と右シフトvs除算の場合、違いが生じる可能性があります。負の数の場合、シフトは負の無限大に丸め、除算はゼロに丸めます。もちろん、コンパイラーは除算をより安価なものに変更しますが、通常は除算と同じ丸め動作をするものに変更します。これは、変数が負にならないことを証明できないか、単にそうしないためです。ケア。したがって、数値が負にならないことを証明できる場合、またはどの方法で丸めるかを気にしない場合は、違いを生む可能性が高い方法で最適化を実行できます。


または番号をキャストunsigned
Lie Ryan

4
シフト動作は標準化されていますか?負の整数の右シフトは実装定義であるという印象を受けました。
Kerrek SB、2011

1
負の数値を右シフトするための特定の動作に依存するコードはその要件を文書化する必要があることをおそらく言及する必要がありますが、右シフトの利点は、自然に正しい値が得られ、除算演算子がコードを生成して無駄にする場合です。ユーザーコードが不要な値を計算するのにかかる時間は、シフトが最初に与えたであろうものを生み出すために追加の時間調整を無駄にしなければなりません。実際、もし私がドラザーを持っているとしたら、コンパイラーは、符号付き除算を実行しようとするときにコウカンするオプションを持っているでしょう。それは...
supercat

1
...オペランドが正であることを認識しているコードは、除算の前に符号なしにキャストした場合(おそらく、その後に符号付きにキャストし戻す可能性があります)、最適化を改善できます。それらをポジティブであると仮定することもできます)。
スーパーキャット2013年

0

Pythonは、同じ乱数に対して同じ乗算を1億回実行します。

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

したがって、Pythonで2のべき乗による乗算/除算ではなくシフトを実行すると、わずかに改善されます(除算では最大10%、乗算では最大1%)。2のべき乗でない場合は、かなりの速度低下が見られます。

繰り返しになりますが、これらの#はプロセッサ、コンパイラ(またはインタプリタ-単純化のためにPythonで行ったもの)によって異なります。

他の人と同様に、時期尚早に最適化しないでください。非常に読みやすいコードを記述し、十分に高速でない場合はプロファイルを作成してから、遅い部分を最適化してください。覚えておいてください、あなたのコンパイラーはあなたよりも最適化に優れています。


0

コンパイラーが実行できない最適化があります。これらは最適化された入力のセットに対してのみ機能するためです。

以下は、64ビットの「逆数による乗算」を行う高速な除算を実行できるc ++サンプルコードです。分子と分母の両方が特定のしきい値を下回っている必要があります。通常の除算よりも実際に高速になるには、64ビット命令を使用するようにコンパイルする必要があることに注意してください。

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

2つの累乗で乗算または除算したい場合は、コンパイラーがMUL / DIVに変換したとしても、ビットシフト演算子の使用で問題が発生することはないと思います。マクロ)とにかく、それらの場合、特にシフトが1より大きい場合、特にCPUにビットシフト演算子がない場合はMUL / DIVになりますが、CPUがビットシフト演算子を使用すると、マイクロコードの分岐を回避できます。これにより、命令数が少なくなります。

高密度のバイナリツリーで動作しているため、多くの2倍算/半分演算を必要とするコードを今書いています。さらに、加算よりも最適であると思われる演算が1つあります-左(2の累乗) )加算でシフトします。これは左シフトとxorで置き換えることができます。シフトが追加するビット数よりも広い場合、簡単な例は(i << 1)^ 1で、1を2倍の値に追加します。もちろん、これは右シフト(2の累乗の除算)には適用されません。左(リトルエンディアン)シフトのみがギャップをゼロで埋めるためです。

私のコードでは、これらの2の乗算/除算と2の累乗の演算が非常に集中的に使用されており、式はすでに非常に短いため、削除できる各命令はかなりの利益になります。プロセッサがこれらのビットシフト演算子をサポートしていない場合、ゲインは発生しませんが、ロスも発生しません。

また、私が書いているアルゴリズムでは、発生する動きを視覚的に表現しているので、その意味で実際にはより明確です。二分木の左側は大きく、右側は小さくなります。それだけでなく、私のコードでは、奇数と偶数には特別な意味があり、ツリー内のすべての左側の子は奇数であり、すべての右側の子とルートは偶数です。まだ遭遇していないが、実際にはこれさえ考えていなかった場合もあります。x&1は、x%2よりも最適な操作である可能性があります。偶数のx&1はゼロを生成しますが、奇数の場合は1を生成します。

奇数/偶数の識別だけではなく、x&3のゼロが得られた場合、4は数値の係数であり、x%7の場合は8と同じであることがわかります。これらのケースのユーティリティは限られていることを知っていますが、ビットごとの演算はほとんど常に最速であり、コンパイラにとってあいまいになる可能性が最も低いため、モジュラス演算を回避し、代わりにビットごとの論理演算を使用できることを知っておくと便利です。

私はほとんど密な二分木の分野を発明しているので、人々が2のべき乗のみ、または2の乗算/除算のみで因数分解のみを実行することを望んでいないので、人々はこのコメントの価値を理解できないかもしれません。



0

gccコンパイラでx + x、x * 2、x << 1構文の出力を比較すると、x86アセンブリで同じ結果が得られます:https : //godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

したがって、gccは、入力した内容とは関係なく、彼自身の最善の解決策を決定するのに十分ほど賢いと考えることができます。


0

私も家を倒せるかどうか見たかった。これは、任意の数による任意の数の乗算のより一般的なビット単位です。私が作成したマクロは、通常の乗算​​よりも約25%長く、2倍遅くなっています。2の倍数に近いか、2の倍数で構成されている場合に他の人が言うように、勝つかもしれません。(X << 4)+(X << 2)+(X << 1)+ Xで構成されるX * 23のように、(X << 6)+ Xで構成されるX * 65より遅くなります。

#include <stdio.h>
#include <time.h>

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.