最近のハードウェアでの乗算は非常に最適化されているため、実際には加算と同じ速度であるとよく言われます。本当?
正式な確認を取得することはできません。私自身の研究は質問を追加するだけです。速度テストは通常、私を混乱させるデータを示します。以下に例を示します。
#include <stdio.h>
#include <sys/time.h>
unsigned int time1000() {
timeval val;
gettimeofday(&val, 0);
val.tv_sec &= 0xffff;
return val.tv_sec * 1000 + val.tv_usec / 1000;
}
int main() {
unsigned int sum = 1, T = time1000();
for (int i = 1; i < 100000000; i++) {
sum += i + (i+1); sum++;
}
printf("%u %u\n", time1000() - T, sum);
sum = 1;
T = time1000();
for (int i = 1; i < 100000000; i++) {
sum += i * (i+1); sum++;
}
printf("%u %u\n", time1000() - T, sum);
}
上記のコードは、乗算が高速であることを示しています。
clang++ benchmark.cpp -o benchmark
./benchmark
746 1974919423
708 3830355456
しかし、他のコンパイラー、他のコンパイラー引数、異なる方法で作成された内部ループでは、結果が異なる可能性があり、近似値を取得することさえできません。
実際には、2つのn-ビット数の乗算を行うことができますO(log n)回路の深さで、加算と同じです。
O(log n)での加算は、数値を半分に分割し、(再帰的に)parallelの2つの部分を追加します。 bothの場合、「0キャリー」と「1キャリー」の場合。下半分が追加されると、キャリーが検査され、その値が0キャリーと1キャリーのケースの選択に使用されます。
O(log n)の深さの乗算はalsoによって行われますparallelization、ここで、3つの数字のすべての合計が並列に2つの数字の合計に削減され、合計は上記のような方法で行われます。
ここでは説明しませんが、 "carry-lookahead"を調べることで、速い加算と乗算の読み物を見つけることができますおよび "carry-save"の追加。
理論的な観点からは、回路は明らかにソフトウェアとは異なり本質的に並列であるため、乗算が漸近的に遅くなる唯一の理由は、漸近的な複雑さではなく、前の定数因子です。
いいえ、同じ速度ではありません。誰があなたにそれを言ったの?
Agner Fogの命令テーブル 32ビット整数レジスタを使用する場合、HaswellのADD/SUBには0.25–1サイクル(命令のパイプライン処理の程度によります)がかかりますが、MULには2–4サイクルかかります。浮動小数点は他の方法です:ADDSS/SUBSSは1〜3サイクルかかり、MULSSは0.5〜5サイクルかかります。
これは、単に乗算と加算を行うよりもさらに複雑な答えです。実際には、答えは決して「はい」ではないでしょう。電子的に乗算は、はるかに複雑な回路です。理由のほとんどは、乗算が乗算ステップに続く加算ステップの動作であるため、電卓を使用する前に小数を乗算するのがどのようなものだったかを覚えています。
覚えておくべきもう1つのことは、実行するプロセッサのアーキテクチャに応じて、乗算にかかる時間が長くなるか短くなることです。これは、会社固有のものである場合とそうでない場合があります。 AMDはおそらくIntelとは異なりますが、Intel i7でもコア2(同じ世代内)とは異なる場合があり、世代間(特に先に行くほど)確かに異なります。
すべての技術において、乗算があなたがしている唯一のことである場合(ループやカウントなどなしで)、乗算は2から(PPCアーキテクチャで見られるように)35倍遅くなります。これは、アーキテクチャと電子機器を理解するための演習です。
さらに:乗算を含むすべての操作が単一のクロックを使用するプロセッサを構築できることに注意してください。このプロセッサがしなければならないことは、すべてのパイプライン処理を取り除き、クロックを遅くして、OP回路のHWレイテンシがクロックタイミングで提供されるレイテンシ以下になるようにすることです。
これを行うと、プロセッサにパイプラインを追加したときに得られる固有のパフォーマンスの向上がなくなります。パイプライン化とは、タスクを取得して、より迅速に実行できる小さなサブタスクに分割するという考え方です。サブタスク間で各サブタスクの結果を保存および転送することにより、サブタスク全体の遅延ではなく、サブタスクの最長のレイテンシを可能にするだけの高速クロックレートを実行できるようになりました。
乗算による時間の図:
| ------------------------------------------------- パイプラインなし
|-ステップ1-- |-ステップ2-- |-ステップ3-- |-ステップ4-- |-ステップ5-- |パイプライン化
上の図では、パイプライン化されていない回路には50単位の時間がかかります。パイプラインバージョンでは、50ユニットを5ステップに分割し、各ステップは10ユニットの時間を要し、その間にストアステップがあります。パイプライン化された例では、各ステップが単独で完全に並行して動作できることに注意することが非常に重要です。操作を完了するには、5つのステップすべてを順番に移動する必要がありますが、オペランドを含む同じ操作の別の1つは、ステップ1、3、4、および5にあるようにステップ2にあります。
以上のことをすべて説明すると、このパイプライン化されたアプローチにより、各クロックサイクルでオペレーターを継続的に満たすことができ、各クロックサイクルで結果を得ることができます。切り替え前に1つの操作をすべて実行できるように操作をオーダーできる場合タイミングヒットとして取得するのは、パイプラインから最初の操作を取得するために必要な元のクロック数だけです。
Mysticalにはもう1つの良い点があります。また、より多くのシステムの観点からアーキテクチャを検討することも重要です。プロセッサ内の浮動小数点乗算のパフォーマンスを向上させるために、新しいHaswellアーキテクチャが構築されたことは事実です。このため、システムレベルとして、複数の乗算がシステムクロックごとに1回のみ発生する追加に対して、同時に発生できるように設計されました。
---(これはすべて次のように要約できます:
Haswellが持っているのでIntel
add
4 /クロックスループット、1サイクルレイテンシのパフォーマンス。 (任意のオペランドサイズ)imul
1 /クロックスループット、3サイクルレイテンシのパフォーマンス。 (任意のオペランドサイズ)Ryzenも同様です。ブルドーザーファミリの整数スループットは非常に低く、パイプラインが完全に乗算されていません。64ビットのオペランドサイズの乗算では非常に低速です。 https://agner.org/optimize/ および https://stackoverflow.com/tags/x86/info のその他のリンクを参照してください
しかし、優れたコンパイラーはループを自動ベクトル化できます。 (SIMD整数乗算スループットとレイテンシは、両方ともSIMD整数加算よりも劣ります)。または、単にそれらを定数伝播して、答えを印刷するだけです! Clangはsum(i=0..n)
の閉じた形式のガウスの式を本当に知っており、それを行ういくつかのループを認識することができます。
最適化を有効にするのを忘れたため、両方のループがALUのボトルネック+store/reload latencysum += independent stuff
とsum++
のそれぞれの間でsum
をメモリに保持します。 なぜこのclangが-O0で非効率的なasmを生成するのか(この単純な浮動小数点の合計について)? を参照してください。 clang++
のデフォルトは-O0
(デバッグモード:デバッガーがC++ステートメント間で変数を変更できる場所に変数を保持します)。
Sandybridgeファミリー(HaswellやSkylakeを含む)のような最新のx86でのストア転送遅延は、リロードのタイミングに応じて約3〜5サイクルです。したがって、1サイクルのレイテンシALU add
もそこにあるため、このループのクリティカルパスにある約2つの6サイクルのレイテンシステップを見ています。 (i
に基づくすべてのストア/リロードと計算、およびループカウンターの更新を非表示にするために十分です)。
最適化なしでコンパイルされた場合、冗長な割り当てを追加するとコードが高速化されますその1つでは、ループ内でより独立した作業を行うことにより、ストア転送の遅延が実際に短縮され、リロードの試行が遅延します。
最新のx86 CPUには1 /クロック乗算スループットがあるため、最適化を行ってもスループットのボトルネックは発生しません。または、2クロックあたり1スループットで完全にパイプライン化されていないブルドーザーファミリ。
サイクルごとにすべての作業を発行するというフロントエンド作業のボトルネックになる可能性が高くなります。
lea
は非常に効率的なコピーアンドアドを許可しますが、1つの命令でi + i + 1
を実行します。本当に優れたコンパイラーは、ループが2*i
のみを使用し、2ずつ増加するように最適化することを確認します。
そしてもちろん、最適化により、余分なsum++
は、stuff
に既に定数が含まれているsum += stuff
に折りたたむことができます。乗算ではそうではありません。
乗算では、少なくとも同じサイズの数を加算する最終ステップが必要です。そのため、追加よりも時間がかかります。 10進数で:
123
112
----
+246 ----
123 | matrix generation
123 ----
-----
13776 <---------------- Addition
同じことがバイナリに適用され、より複雑な行列の削減が行われます。
とはいえ、同じ時間がかかる理由:
もちろん、そうではないより複雑なアーキテクチャがあり、まったく異なる値を取得する場合があります。また、相互に依存していないときに複数の命令を並行して実行するアーキテクチャもあり、コンパイラーとオペレーティングシステムに少しばかり依存しています。
このテストを厳密に実行する唯一の方法は、オペレーティングシステムなしでアセンブリで実行する必要があることです。そうしないと、変数が多すぎます。
たとえそうであったとしても、それはほとんどの場合、クロックがハードウェアにどのような制限を課すかを教えてくれます。 heat(?)のためにクロックを高くすることはできませんが、クロック中に信号が通過できるADD命令ゲートの数は非常に多くなる可能性がありますが、単一のADD命令はそのうちの1つだけを使用します。そのため、ある時点では同じくらい多くのクロックサイクルを要する場合がありますが、信号の伝播時間のすべてが利用されるわけではありません。
クロックを高くすることができれば、defができます。 ADDをおそらく数桁速くします。
これは実際にマシンに依存します。もちろん、整数の乗算は加算に比べて非常に複雑ですが、かなりの数のAMD CPUが乗算を実行できます1サイクルで。それは追加と同じくらい速いです。
他のCPUは乗算を行うのに3サイクルまたは4サイクルかかります。これは加算よりも少し遅くなります。しかし、10年前に受けなければならなかったパフォーマンスの低下に近いところはありません(当時、32ビットの乗算は一部のCPUで30サイクルかかることがありました)。
そのため、今日、乗算は同じ速度クラスにありますが、いや、すべてのCPUでの加算ほど正確ではありません。
いいえ、そうではありません。実際、著しく遅いです(これは、実行中の特定の実際のプログラムで15%のパフォーマンスヒットに変換されます)。
ほんの数日前からこの質問をするとき、私は自分でこれに気付きました here 。