Cでのシフトと乗算の時間の違いをテストすると、違いはありません。どうして?


28

バイナリのシフトは、2 ^ kを乗算するよりもはるかに効率的であると教えられました。だから私は実験したかったので、次のコードを使用してこれをテストしました。

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

int main() {
    clock_t launch = clock();
    int test = 0x01;
    int runs;

    //simple loop that oscillates between int 1 and int 2
    for (runs = 0; runs < 100000000; runs++) {


    // I first compiled + ran it a few times with this:
    test *= 2;

    // then I recompiled + ran it a few times with:
    test <<= 1;

    // set back to 1 each time
    test >>= 1;
    }

    clock_t done = clock();
    double diff = (done - launch);
    printf("%f\n",diff);
}

どちらのバージョンでも、印刷は約440000で、10000を与えたり受けたりしました。2つのバージョンの出力の間に(視覚的には)有意な差はありませんでした。だから私の質問は、私の方法論に何か問題がありますか?視覚的な違いもありますか?これは、コンピューターのアーキテクチャー、コンパイラー、または他の何かと関係がありますか?


47
明らかに間違っているとあなたに教えた人。1970年代以降、一般的に使用されるアーキテクチャで一般的に使用されるコンパイラーについては、その信念は真実ではありませんでした。この申し立てをテストするのに適しています。JavaScriptについて天国のためになされたこの無意味な主張を聞いたことがあります。
エリックリッパー14

21
このような質問に答える最良の方法は、コンパイラが生成しているアセンブリコードを調べることです。コンパイラには通常、生成するアセンブリ言語のコピーを作成するオプションがあります。GNU GCCコンパイラーの場合、これは「-S」です。
チャールズE.グラント14

8
を使ってこれを調べた後gcc -S、for test *= 2でコードが実際にコンパイルされ、ループがないshll $1, %eax 場合に呼び出されることを指摘する必要gcc -O3 -Sがあります。2つのクロック呼び出しが離れラインです:callq _clock movq %rax, %rbx callq _clock

6
「バイナリでシフトすることは、2 ^ kで乗算するよりもはるかに効率的であると教えられました」。間違っている(または少なくとも時代遅れである)ことが判明した多くのことを教えられます。賢いコンパイラーは、両方に同じシフト操作を使用します。
ジョンボード14

9
この種の最適化に取り組むときは、常に、生成されたアセンブリコードをチェックして、測定していると思われるものを測定していることを確認してください。SOに関する膨大な数の「なぜ私はこれらの時間を見ているのか」という質問は、結果が使用されないために完全に操作を排除するコンパイラーに沸騰します。
ラッセルボロゴーブ14

回答:


44

他の答えで述べたように、ほとんどのコンパイラーはビットシフトで行われる乗算を自動的に最適化します。

これは最適化の際の非常に一般的なルールです。ほとんどの「最適化」は、実際にあなたが本当に意味することについてコンパイルを誤って導き、パフォーマンスを低下させることさえあります。

パフォーマンスの問題に気付き、問題が何であるかを測定した場合にのみ最適化します。(そして、私たちが書くほとんどのコードはそれほど頻繁に実行されないので、気にする必要はありません)

最適化の大きな欠点は、「最適化された」コードが読みにくい場合が多いことです。したがって、あなたの場合、あなたが乗算しようとしているときは常に乗算に行きます。また、ビットを移動する場合はビットシフトを使用します。


20
常に意味的に正しい操作を使用してください。ビットマスクを操作したり、小さな整数を大きな整数内に配置したりする場合は、シフトが適切な操作です。
ddyer

2
高レベルのソフトウェアアプリケーションでシフト演算子への乗算を最適化する必要が(実際に言えば)あるでしょうか?コンパイラはすでに最適化されているので、この知識を得ることが有用なのは、非常に低いレベル(少なくとも、コンパイラより下)でプログラミングするときだけです。
NicholasFolk 14

11
@NicholasFolkいや。理解するのが最も簡単なことをしてください。アセンブリを直接記述している場合は便利です...または最適化コンパイラを記述している場合は、再び有用です。しかし、これらの2つのケース以外では、あなたがしていることをあいまいにし、次のプログラマー(あなたが住んでいる場所を知っているa 殺人者)にあなたの名前を呪い、趣味を始めることを考えさせるトリックです。

2
@NicholasFolk:このレベルでの最適化は、ほとんどの場合CPUアーキテクチャによって不明瞭にされるか、意味がありません。メモリーから引数を取得して、それらを書き戻すのに100サイクル以上かかる場合、50サイクル節約するとどうなりますか?このようなマイクロ最適化は、メモリがCPUの速度(またはそれに近い速度)で実行されたときに意味がありましたが、今日ではそれほどではありません。
TMN 14

2
私はその引用の10%を見るのにうんざりしており、それがここで頭に釘を打っているからです:「効率の杯が悪用につながることは間違いありません。プログラムの重要ではない部分の速度、およびこれらの効率化の試みは、デバッグと保守を考慮すると、実際に大きなマイナスの影響を及ぼします。たとえば、約97%の小さな効率性忘れてください。すべての悪....
cHao 14

25

コンパイラは定数を認識し、必要に応じて乗算をシフトに変換します。


コンパイラは、2の累乗である定数を認識し、シフトに変換します。すべての定数をシフトに変更できるわけではありません。
すぐに14

4
@quickly_now:これらは、シフトと加算/減算の組み合わせに変換できます。
Mehrdad 14

2
古典的なコンパイラオプティマイザーのバグは、除算を右シフトに変換することです。これは、正の配当に対しては機能しますが、負に対しては1オフです。
ddyer

1
@quickly_now「適切な」という用語は、一部の定数をシフトとして書き換えることができないという考えをカバーすると考えています。
ファラプ14

21

シフトよりも乗算が速いかどうかは、CPUのアーキテクチャに依存します。Pentium以前の時代では、被乗数の1ビットの数に応じて、シフトは乗算よりも高速でした。たとえば、被乗数が320だった場合、101000000、2ビットです。

a *= 320;               // Slower
a = (a<<7) + (a<<9);    // Faster

しかし、2ビット以上持っていたら...

a *= 324;                        // About same speed
a = (a<<2) + (a<<7) + (a<<9);    // About same speed

a *= 340;                                 // Faster
a = (a<<2) + (a<<4) + (a<<7) + (a<<9);    // Slower

PIC18のようなシングルサイクル乗算でバレルシフターを使用しない小さなマイクロコントローラーでは、1ビット以上シフトする場合、乗算が高速になります。

a  *= 2;   // Exactly the same speed
a <<= 1;   // Exactly the same speed

a  *= 4;   // Faster
a <<= 2;   // Slower

これは、古いIntel CPUで真実だったものの反対であることに注意してください。

しかし、それはまだそれほど単純ではありません。私の記憶が正しければ、そのスーパースカラーアーキテクチャにより、Pentiumは1つの乗算命令または2つのシフト命令を同時に処理できました(相互に依存していない限り)。これは、2つの変数を2のべき乗で乗算する場合、シフトの方が良いことを意味します。

a  *= 4;   // 
b  *= 4;   // 

a <<= 2;   // Both lines execute in a single cycle
b <<= 2;   // 

5
+1「乗算よりもシフトが速いかどうかは、CPUのアーキテクチャに依存します。」実際に少し歴史に触れて、ほとんどのコンピューター神話が実際に論理的な根拠を持っていることを示してくれてありがとう。
ファラプ14

11

テストプログラムにいくつかの問題があります。

まず、実際にはの値を使用していませんtest。C標準内では、値がtest重要であるという方法はありません。オプティマイザーは完全に無料で削除できます。それを削除すると、ループは実際には空になります。唯一目に見える効果がセットになりruns = 100000000ますが、runsまた、使用されていません。そのため、オプティマイザーはループ全体を削除できます(そしてすべきです!)。簡単な修正:計算された値も出力します。十分に決定されたオプティマイザは、ループを最適化する可能性があることに注意してください(コンパイル時に既知の定数に完全に依存しています)。

次に、互いにキャンセルする2つの操作を実行します。オプティマイザーはこれに気づき、キャンセルすることができます。再び空のループを残して、削除されます。これを修正するのは実に難しいです。に切り替えることができますunsigned int(したがって、オーバーフローは未定義の動作ではありません)が、もちろん0になります。そして、単純なもの(たとえばtest += 1)は、オプティマイザーが理解するのに十分簡単であり、実行します。

最後に、test *= 2実際に乗算にコンパイルされると仮定します。これは非常に簡単な最適化です。ビットシフトが速い場合、オプティマイザーは代わりにそれを使用します。これを回避するには、実装固有のアセンブリのようなものをインラインで使用する必要があります。

または、マイクロプロセッサのデータシートをチェックして、どちらが速いかを確認してください。

gcc -S -O3バージョン4.9 を使用してプログラムをコンパイルするアセンブリ出力を確認したとき、オプティマイザーは上記の簡単なバリエーションすべてを実際に確認しました。すべての場合で、ループを削除し(定数をに割り当てtest)、残っているのは、への呼び出しclock()、変換/減算、およびprintf


1
また、オプティマイザーは、sqrt c#vs sqrt c ++で示されているように、値を実際の合計と合計するループを置き換えることができるように、定数の操作を(ループでも)最適化できることに注意してください。この最適化を無効にするには、実行時に決定されたもの(コマンドライン引数など)を使用する必要があります。

@MichaelTうん。それは、「十分に決定されたオプティマイザがループを最適化することができることに注意してください(コンパイル時に既知の定数に完全に依存しています)」。
デロバート14

あなたの言っていることは承知していますが、コンパイラがループ全体を削除しているとは思いません。繰り返し回数を増やすだけで、この理論を簡単にテストできます。反復回数を増やすと、プログラムの時間が長くなることがわかります。ループが完全に削除された場合、これは当てはまりません。
DollarAkshay

@AkshayLAradhya私はあなたのコンパイラが何をしいるのか言うことはできませんがgcc -O3、ループを完全に削除することを確認しました(現在7.3で)。(必要に応じてintではなくlongに切り替えてください。そうしないと、オーバーフローのために無限ループに最適化されます)。
デロバート

8

私は質問といくつかの回答やコメントに未検討の仮定がいくつかあるので、質問者がより差別化された答えを持っている方が役立つと思います。

結果として生じるシフトと乗算の相対的な実行時間は、Cとは何の関係もありません。私がCと言うとき、私はそのようなGCCのバージョンなどの特定の実装のインスタンスではなく、言語を意味します。このような馬鹿げた話をするつもりはありませんが、説明のために極端な例を使用します:完全に標準に準拠したCコンパイラを実装し、乗算に1時間かかり、シフトにミリ秒かかります-またはその逆です。CやC ++でのこのようなパフォーマンス制限については知りません。

あなたは議論の中でこの技術を気にかけないかもしれません。あなたの意図はおそらくシフトと乗算の相対的なパフォーマンスをテストすることであり、Cを選択しました。これは一般に低レベルのプログラミング言語として認識されているため、ソースコードが対応する命令に直接変換されることを期待する可能性があるためです このような質問は非常に一般的であり、Cでもソースコードは特定のインスタンスで考えられるほど直接命令に変換されないことを指摘すべきです。以下にいくつかの可能なコンパイル結果を示しました。

ここで、実世界のソフトウェアでこの同等性を代用することの有用性を疑問視するコメントが出てきます。EricLippertのコメントのように、あなたの質問へのコメントの一部を見ることができます。これは、このような最適化に対する一般的な経験豊富なエンジニアからの反応と一致しています。プロダクションコードでバイナリシフトを掛け算と除算の包括的な手段として使用すると、人々はおそらくあなたのコードにうんざりし、ある程度の感情的な反応(「天国のためにJavaScriptについて行われたこの無意味な主張」を聞いたことがあります)それらの反応の理由をよりよく理解しない限り、初心者プログラマーにとって意味がないかもしれません。

これらの理由は主に、相対的なパフォーマンスの比較ですでにわかっているように、そのような最適化の可読性の低下と無益さの組み合わせです。ただし、乗算の代わりにシフトを使用することがこのような最適化の唯一の例である場合、人々がそれほど強い反応を示すとは思いません。あなたのような質問は、さまざまな形でさまざまなコンテキストで頻繁に出てきます。より多くの上級エンジニアが実際に非常に強く反応するのは、少なくとも私が時々持っていると思うのは、人々がそのようなマイクロ最適化をコードベース全体で自由に採用すると、はるかに広い範囲の害の可能性があるということです。Microsoftのような会社で大規模なコードベースで働いている場合、他のエンジニアのソースコードを読んだり、特定のコードを見つけようとすることに多くの時間を費やすことになります。数年のうちに、特にポケットベルでの呼び出しを受けた後の本番システムの停止を修正する必要がある場合など、最も不適切な時期に意味をなそうとしているのは、独自のコードですらあります。金曜日の夜の義務で、友人との楽しい夜に出かけようとしています…コードを読むことに多くの時間を費やしているなら、それが可能な限り読みやすいことに感謝します。お気に入りの小説を読んでいると想像してみてください。しかし、出版社はabbrvを使用する新しいエディションをリリースすることを決定しました。すべてのovr th plc bcs thy thnk svs spc。これは、他のエンジニアがそのような最適化を振りかけた場合、あなたのコードに対する反応と似ています。他の答えが指摘したように、あなたが何を意味するかを明確に述べる方が良いです、

ただし、そのような環境であっても、これまたは他の同等性を知っていることが期待されるインタビューの質問を解くことがあります。それらを知ることは悪くなく、優れたエンジニアはバイナリシフトの算術効果を知っているでしょう。これは良いエンジニアになるとは言わなかったが、私の意見では、良いエンジニアは知っているだろうことに注意してください。特に、通常はインタビューループの終わり頃に、このスマートエンジニアリングの「トリック」をコーディングの質問で明らかにし、彼/彼女を証明する喜びを期待してあなたに広く笑うマネージャーを見つけることができます。 、また、以前は熟練したエンジニアの1人であり、「単なる」マネージャーではありません。そのような状況では、感銘を受けたように見えるようにし、啓発的なインタビューに感謝します。

Cで速度の違いが見られなかったのはなぜですか?最もありそうな答えは、どちらも同じアセンブリコードになったということです。

int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }

両方にコンパイルできます

shift(int):
    lea eax, [0+rdi*4]
    ret

最適化を行わないGCC、つまり「-O0」フラグを使用すると、次のようになります:

shift(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    sal eax, 2
    pop rbp
    ret
multiply(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    add eax, eax
    pop rbp
    ret

ご覧のとおり、GCCに "-O0"を渡しても、生成されるコードの種類が多少賢くないという意味ではありません。特に、この場合でもコンパイラーは乗算命令の使用を避けていることに注意してください。同じ実験を、他の数によるシフトや、2のべき乗ではない数による乗算でも繰り返すことができます。プラットフォームでは、シフトと加算の組み合わせが表示されますが、乗算は表示されない可能性があります。乗算とシフトのコストが実際に同じ場合、コンパイラーがすべてのケースで乗算の使用を避けることは明らかに偶然のように思えますか?しかし、私は証明のために仮定を提供するつもりはないので、先に進みましょう。

上記のコードを使用してテストを再実行し、速度の違いに気づいたかどうかを確認できます。それでも、乗算がないことからわかるように、シフトと乗算のテストは行っていませんが、特定のインスタンスでのシフトと乗算のC操作に対してGCCが特定のフラグセットで生成したコードです。そのため、別のテストでは、アセンブリコードを手動で編集し、代わりに「乗算」メソッドのコードで「imul」命令を使用できます。

コンパイラーのこれらのスマートのいくつかを無効にしたい場合は、より一般的なシフトおよび乗算メソッドを定義でき、次のような結果になります。

int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }

次のアセンブリコードが生成される場合があります。

shift(int, int):
    mov eax, edi
    mov ecx, esi
    sal eax, cl
    ret
multiply(int, int):
    mov eax, edi
    imul    eax, esi
    ret

ここで最終的に、GCC 4.9の最高の最適化レベルであっても、テストに最初に着手したときに予想されるアセンブリ命令の式があります。パフォーマンスの最適化において、それ自体が重要な教訓になると思います。コンパイラが適用できるスマートの観点から、コード内の具体的な定数を変数に置き換えることで生じた違いを見ることができます。shift-multiply置換のようなマイクロ最適化は、コンパイラが通常単独で簡単に実行できる非常に低レベルの最適化です。パフォーマンスにはるかに影響を与える他の最適化には、コードの意図を理解する必要があります多くの場合、コンパイラはアクセスできないか、何らかの発見的手法によってのみ推測できます。ソフトウェアエンジニアとしてあなたが入る場所であり、通常、乗算をシフトに置き換える必要はありません。これには、I / Oを生成し、プロセスをブロックする可能性のあるサービスへの冗長な呼び出しを回避するなどの要素が含まれます。ハードディスクに移動する場合、または既にメモリに保存されているデータから派生した余分なデータをリモートデータベースに移動する場合、待機時間は100万命令の実行を上回ります。今、私たちはあなたの元の質問から少し離れていると思いますが、特にコードの翻訳と実行を把握し始めたばかりの人がいる場合、これを質問者に指摘すると思います。

それで、どれがより速くなりますか?パフォーマンスの違いを実際にテストするために選択した良い方法だと思います。一般に、いくつかのコード変更の実行時パフォーマンスに驚くのは簡単です。現代のプロセッサが採用している多くの技術があり、ソフトウェア間の相互作用も複雑になる可能性があります。ある状況で特定の変更に対して有益なパフォーマンス結果が得られたとしても、このタイプの変更が常にパフォーマンス上のメリットをもたらすと結論付けるのは危険だと思います。そのようなテストを1回実行するのは危険だと思います。「わかりました。今はどちらが速いかわかりました!」そして、測定を繰り返さずに、その同じ最適化を実稼働コードに無差別に適用します。

では、シフトが乗算より速い場合はどうでしょうか?なぜそうなるのかは確かに示されています。上記でわかるように、GCCは、(最適化を行わなくても)他の命令を優先して直接乗算を行わないことをお勧めします。インテル64およびIA-32アーキテクチャー最適化リファレンス・マニュアルは、あなたのCPU命令の相対的なコストのアイデアを与えるだろう。命令のレイテンシとスループットに重点を置いた別のリソースは、http: //www.agner.org/optimize/instruction_tables.pdfです。これらは絶対実行時間の適切な述語ではなく、相互に相対的な命令のパフォーマンスの述語であることに注意してください。タイトループでは、テストがシミュレートしているため、「スループット」のメトリックが最も重要になります。これは、特定の命令を実行するときに実行ユニットが通常拘束されるサイクル数です。

では、シフトが乗算より速くない場合はどうでしょうか?上で述べたように、現代のアーキテクチャは非常に複雑になる可能性があり、分岐予測、キャッシュ、パイプライン化、並列実行ユニットなどによって、論理的に等価な2つのコードの相対的なパフォーマンスを予測することが困難になる場合があります。私は本当にこれを強調したいと思います。なぜなら、こういう質問に対するほとんどの答えと、シフトが乗算より速いということは(もう)真実ではないと言っている人々のキャンプに満足していないからです。

いいえ、私が知っている限りでは、1970年代に、または乗算ユニットとビットシフターのコストの違いを突然無効にするときは、秘密のエンジニアリングソースを発明しませんでした。論理ゲートの点で、そして確かに論理演算の点での一般的な乗算は、多くのシナリオで、多くのアーキテクチャのバレルシフターを使用したシフトよりもさらに複雑です。これがデスクトップコンピュータの全体的なランタイムにどのように変換されるかは、少し不透明かもしれません。特定のプロセッサにどのように実装されているのかはわかりませんが、乗算の説明は次のとおりです。整数乗算は、最新のCPUでの加算と本当に同じ速度ですか

ここにバレルシフターの説明がありますが。前の段落で参照したドキュメントは、CPU命令のプロキシによる操作の相対コストに関する別の見解を示しています。Intelのエンジニアは、よく似た質問をしているようです。Intel開発者ゾーンフォーラムは、コア2デュオプロセッサでの整数の乗算と加算のクロックサイクルです。

はい、ほとんどの実際のシナリオで、そしてほぼ確実にJavaScriptで、パフォーマンスのためにこの同等性を悪用しようとすることは、おそらく無駄な仕事です。ただし、乗算命令の使用を強制し、ランタイムに違いが見られない場合でも、それは使用したコストメトリックスの性質によるものであり、正確には、コストの違いがないためではありません。エンドツーエンドランタイムは1つのメトリックであり、それが私たちが関心を持っている唯一のメトリックである場合、すべてが順調です。しかし、それは乗算とシフトの間のすべてのコストの違いが単に消えたという意味ではありません。そして、その考えを質問者に伝えることは確かに良い考えではないと思います。質問者は暗黙的または別の方法で、明らかに現代のコードの実行時間とコストに関与する要因の考えを得始めています。エンジニアリングは常にトレードオフに関係しています。ユーザーが最終的に見る実行時間を表示するために現代のプロセッサーがどのようなトレードオフを行ったかについての問い合わせと説明は、より差別化された答えをもたらす可能性があります。そして、「最適化」の性質をより一般的に理解する必要があるため、マイクロ最適化されたコードを読みやすくするより少ないエンジニアをチェックしたい場合は、「これはもはや真実ではありません」よりもより差別化された答えが保証されると思います特定のインスタンスを単に古くなったものとして参照するよりも、さまざまな多様な化身を見つけます。


6

あなたが見るものは、オプティマイザーの効果です。

optimisersの仕事は、結果としてコンパイルされたコードを小さくするか、高速にすることです(ただし、両方を同時に実行することはめったにありません...しかし、多くのことが好きです...コードの内容に依存します)。

原則として、乗算ライブラリの呼び出し、または頻繁にハードウェア乗算器の使用でさえ、ビット単位のシフトを行うよりも遅くなります。

そのため、単純なコンパイラーが操作* 2のライブラリーへの呼び出しを生成した場合、もちろん、ビット単位のシフト*よりも実行速度が遅くなります。

しかし、オプティマイザーはパターンを検出し、コードをより小さく/より速く/どのようにするかを見つけるためにあります。そしてあなたが見たのは、コンパイラが* 2がシフトと同じであることを検出していることです。

ちょうど興味のある問題として、私は今日* 5のようないくつかの操作のために生成されたアセンブラーを見ていた...実際にはそれではなく他のものを見て、途中でコンパイラーが* 5に変わったことに気づいた:

  • シフト
  • シフト
  • 元の番号を追加

そのため、私のコンパイラのオプティマイザーは、(少なくとも特定の小さな定数に対して)汎用的な乗算ライブラリの呼び出しの代わりにインラインシフトと追加を生成するのに十分なほどスマートでした。

コンパイラーオプティマイザーの技術はまったく別のテーマであり、魔法に満ちており、地球全体で約6人が本当に適切に理解しています:)


3

次のタイミングで試してください:

for (runs = 0; runs < 100000000; runs++) {
      ;
}

コンパイラはtest、ループの各反復後に値が変更されておらず、最終値testが使用されていないことを認識し、ループを完全に削除する必要があります。


2

乗算は、シフトと加算の組み合わせです。

あなたが言及した場合、コンパイラがそれを最適化するかどうかは重要ではないと思います-「x2で乗算」は次のいずれかとして実装できます。

  • x1桁のビットを左にシフトします。
  • に追加xxます。

これらはそれぞれ基本的なアトミック操作です。一方は他方より速くありません。

これを「x4で乗算」(またはany 2^k, k>1)に変更すると、少し異なります。

  • x2桁のビットを左にシフトします。
  • に追加xxて呼び出しy、に追加yyます。

基本的なアーキテクチャでは、シフトがより効率的であることが簡単にわかります。1対2の操作を行うのは、何がわかるかまで追加できないyためです。yy

後者(またはany 2^k, k>1)を試して、適切なオプションを使用して、実装で同じものになるように最適化しないようにします。でO(1)繰り返し追加する場合に比べて、シフトが速いことに気付くはずO(k)です。

明らかに、被乗数が2のべき乗ではない場合、シフトと加算の組み合わせ(それぞれの数が0でない場合)が必要です。


1
「基本的な原子操作」とは何ですか?シフトでは、操作はすべてのビットに並行して適用でき、さらに左端のビットは他のビットに依存すると主張することはできませんか?
ベルギ14

2
@Bergi:彼は、shiftとaddの両方が単一の機械命令であることを意味していると推測しています。命令セットのドキュメントを見て、それぞれのサイクルカウントを確認する必要がありますが、はい、追加は通常マルチサイクル操作であるのに対して、シフトは通常1サイクルで実行されます。
TMN 14

はい、そうかもしれませんが、乗算も同様に単一の機械語命令です(もちろん、より多くのサイクルが必要な場合があります)
ベルギ14

@Bergi、それもアーチに依存しています。32ビットの加算(または該当する場合はxビット)よりも少ないサイクルでそのシフトをどのようなアーチで考えていますか?
OJフォード14

私は特定のアーキテクチャーを知りません、いいえ(そして私のコンピューター工学コースは衰退しました)、おそらく両方の命令は1サイクル未満で完了します。私はおそらく、マイクロコードまたは論理ゲートの観点から考えていたでしょう。
ベルギ14

1

2のべき乗による符号付きまたは符号なしの値の乗算は、左シフトと同等であり、ほとんどのコンパイラーが置換を行います。符号なしの値の除算、またはコンパイラーが負ないことを証明できる符号付きの値は、右シフトと同等であり、ほとんどのコンパイラーがその置換を行います(ただし、符号付きの値が負になれないことを証明するほど洗練されていないものもあります) 。

ただし、潜在的に負の符号付き値の除算は、右シフトと同等ではないことに注意してください。のような式(x+8)>>4はと同等ではありません(x+8)/16。前者は、コンパイラの99%で、値を-24から-9から-1、-8から+7から0、および+8から+23から1にマッピングします(ゼロを中心にほぼ対称的に丸めます)。後者は、-39から-24から-1、-23から+7から0、および+8から+23から+1 [全体的に非対称であり、意図したものではない可能性があります]をマップします。値が負であると予想されない場合でも、使用すると、コンパイラが値を負にできないことを証明できない場合>>4よりも高速なコードが生成される可能性が高いことに注意してください/16


0

チェックアウトしたばかりの詳細情報。

x86_64では、MULオペコードのレイテンシは10サイクル、スループットは1/2サイクルです。MOV、ADD、およびSHLのレイテンシは1サイクルで、スループットは2.5、2.5、および1.7サイクルです。

15を掛けるには、最低3つのSHLと3つのADD opが必要で、おそらく2、3のMOVが必要です。

https://gmplib.org/~tege/x86-timing.pdf


0

あなたの方法論には欠陥があります。ループの増分と条件のチェック自体に時間がかかります。

  • 空のループを実行して、時間を測定してください(呼び出しますbase)。
  • 次に、1つのシフト操作を追加し、時間を測定します(呼び出しますs1)。
  • 次に、10個のシフト操作を追加し、時間を測定します(それを呼び出しますs2

すべてが正しく進行している場合base-s2は、の10倍以上でなければなりませんbase-s1。そうでなければ、ここで何か他のものが出てきます。

今、私は実際に自分でこれを試してみました、Ifループが問題を引き起こしているのはなぜそれらを完全に削除しないのですか?だから私は先に行き、これをしました:

int main(){

    int test = 2;
    clock_t launch = clock();

    test << 6;
    test << 6;
    test << 6;
    test << 6;
    //.... 1 million times
    test << 6;

    clock_t done = clock();
    printf("Time taken : %d\n", done - launch);
    return 0;
}

そしてそこに結果があります

1ミリ秒未満で100万のシフト操作?

64で乗算しても同じことを行い、同じ結果を得ました。したがって、おそらくコンパイラは、テストの値が変更されることはないと他の人が述べたように、操作を完全に無視しています。

シフトワイズ演算子の結果

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.