私はC++でパフォーマンスの重要な作業を行っていますが、現在は「高速」であるために本質的に浮動小数点である問題に対して整数計算を使用しています。これは非常に多くの迷惑な問題を引き起こし、多くの迷惑なコードを追加します。
さて、私は浮動小数点の計算が386日前後で非常に遅かったのを読んだことを覚えています。私は(IIRC)、オプションのコプロセッサがあったと信じています。しかし、確かに今日、指数関数的により複雑で強力なCPUを使用する場合、浮動小数点または整数の計算を行っても「速度」に違いはありませんか?特に、実際の計算時間は、パイプラインのストールを引き起こしたり、メインメモリから何かをフェッチしたりするようなものに比べて小さいのですか?
正しい答えは、ターゲットハードウェアでベンチマークを行うことです。これをテストする良い方法は何でしょうか。 Linuxで2つの小さなC++プログラムを作成し、それらの実行時間を「時間」と比較しましたが、実際の実行時間は変動しすぎます(仮想サーバーで実行するのに役立ちません)。数百のベンチマークを実行したり、グラフを作成したりするのに1日を費やすのではなく、相対速度の妥当なテストを行うためにできることはありますか?アイデアや考えはありますか?私は完全に間違っていますか?
私が次のように使用したプログラムは、決して同一ではありません:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
int accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += Rand( ) % 365;
}
std::cout << accum << std::endl;
return 0;
}
プログラム2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
float accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( Rand( ) % 365 );
}
std::cout << accum << std::endl;
return 0;
}
前もって感謝します!
編集:私が気にするプラットフォームは、デスクトップLinuxおよびWindowsマシンで実行される通常のx86またはx86-64です。
編集2(以下のコメントから貼り付け):現在、広範なコードベースがあります。本当に「整数計算が速いので、floatを使わないでください」という一般化に出くわしました-そして、この一般化された仮定を反証する方法を探しています(これが本当であるなら)。すべての作業を行い、その後プロファイリングする以外に、私たちにとって正確な結果を予測することは不可能だと思います。
とにかく、すばらしい回答と助けをありがとう。他のものを自由に追加してください:)。
悲しいかな、私はあなたに「依存する」答えを与えることができます...
私の経験から、パフォーマンスには多くの多くの変数があります。特に整数と浮動小数点の計算の間です。プロセッサーごとに「パイプライン」の長さが異なるため、プロセッサーごとに大きく異なります(x86などの同じファミリー内であっても)。また、一部の操作は一般的に非常に単純(加算など)であり、プロセッサを介した高速化されたルートを持ち、他の操作(除算など)ははるかに長い時間がかかります。
もう1つの大きな変数は、データが存在する場所です。追加する値が数個しかない場合は、すべてのデータをキャッシュに保存して、すぐにCPUに送信できます。既にキャッシュにデータがある非常に遅い浮動小数点演算は、整数をシステムメモリからコピーする必要がある整数演算よりも何倍も高速になります。
パフォーマンスが重要なアプリケーションで作業しているため、この質問をしていると思います。 x86アーキテクチャ用に開発していて、追加のパフォーマンスが必要な場合は、SSE拡張機能を使用することを検討してください。これにより、単精度浮動小数点演算を大幅に高速化できます。操作は一度に複数のデータに対して実行できます。さらに、SSE操作用のレジスタの別個のバンクがあります。(2番目の例では、「double」の代わりに「float 、単精度の数学を使用していると思わせます)。
*注:古いMMX命令を使用すると、実際にはプログラムが遅くなります。これは、これらの古い命令が実際にFPUと同じレジスタを使用し、FPUとMMXの両方を同時に使用できないためです。
たとえば(小さい数字ほど速い)、
64ビットIntel Xeon X5550 @ 2.67GHz、gcc 4.1.2 -O3
short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]
32ビットデュアルコアAMD Opteron(tm)プロセッサー265 @ 1.81GHz、gcc 3.4.6 -O3
short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]
ダンが指摘した のように、クロック周波数を正規化しても(パイプライン設計では誤解を招く可能性があります)、結果はCPUアーキテクチャによって大きく異なります(個別[〜#〜] alu [〜#〜] / [〜#〜] fpu [〜# 〜] performance、および実際のALU/FPUの数スーパースカラー 設計でコアごとに利用可能 独立した操作が並列に実行できる -後者の要因は、すべての操作として以下のコードによって実行されません以下は順番に依存しています。)
貧乏人のFPU/ALU操作ベンチマーク:
#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>
double
mygettime(void) {
# ifdef _WIN32
struct _timeb tb;
_ftime(&tb);
return (double)tb.time + (0.001 * (double)tb.millitm);
# else
struct timeval tv;
if(gettimeofday(&tv, 0) < 0) {
perror("oops");
}
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}
template< typename Type >
void my_test(const char* name) {
Type v = 0;
// Do not use constants or repeating values
// to avoid loop unroll optimizations.
// All values >0 to avoid division by 0
// Perform ten ops/iteration to reduce
// impact of ++i below on measurements
Type v0 = (Type)(Rand() % 256)/16 + 1;
Type v1 = (Type)(Rand() % 256)/16 + 1;
Type v2 = (Type)(Rand() % 256)/16 + 1;
Type v3 = (Type)(Rand() % 256)/16 + 1;
Type v4 = (Type)(Rand() % 256)/16 + 1;
Type v5 = (Type)(Rand() % 256)/16 + 1;
Type v6 = (Type)(Rand() % 256)/16 + 1;
Type v7 = (Type)(Rand() % 256)/16 + 1;
Type v8 = (Type)(Rand() % 256)/16 + 1;
Type v9 = (Type)(Rand() % 256)/16 + 1;
double t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v += v0;
v -= v1;
v += v2;
v -= v3;
v += v4;
v -= v5;
v += v6;
v -= v7;
v += v8;
v -= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
// the loop completely
printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v /= v0;
v *= v1;
v /= v2;
v *= v3;
v /= v4;
v *= v5;
v /= v6;
v *= v7;
v /= v8;
v *= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
// the loop completely
printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}
int main() {
my_test< short >("short");
my_test< long >("long");
my_test< long long >("long long");
my_test< float >("float");
my_test< double >("double");
return 0;
}
固定小数点演算と浮動小数点演算の間には、実際の速度に大きな違いがある可能性がありますが、ALU対FPUの理論上のベストケーススループットは完全に無関係です。代わりに、計算(ループ制御など)で使用されないアーキテクチャ上の整数および浮動小数点レジスタ(レジスタ名ではなく実際のレジスタ)の数、キャッシュラインに収まる各タイプの要素の数、整数演算と浮動小数点演算の異なるセマンティクスを考慮した最適化が可能です。これらの効果が支配的です。ここでは、アルゴリズムのデータ依存性が重要な役割を果たすため、一般的な比較では問題のパフォーマンスギャップを予測することはできません。
たとえば、整数の加算は可換であるため、コンパイラがベンチマークに使用したようなループを見つけた場合(ランダムデータが事前に準備されているため結果が不明瞭にならない場合)、ループを展開し、依存関係がない場合は、ループの終了時にそれらを追加します。ただし、浮動小数点の場合、コンパイラは要求した順序と同じ順序で操作を行う必要があります(シーケンスポイントがあるため、コンパイラは同じ結果を保証する必要があるため、並べ替えができません)。前のものの結果。
同時に、より多くの整数オペランドをキャッシュに収める可能性があります。そのため、固定小数点バージョンは、FPUが理論的に高いスループットを持っているマシン上でも、浮動小数点バージョンよりも桁違いに優れている場合があります。
加算はRand
よりもはるかに速いため、プログラムは(特に)役に立たない。
パフォーマンスのホットスポットを特定し、プログラムを段階的に変更する必要があります。開発環境に問題があり、最初に解決する必要があるようです。小さな問題セットのためにPCでプログラムを実行することは不可能ですか?
一般的に、整数演算を使用したFPジョブの試行は低速のレシピです。
TILこれはさまざまです(かなり)。ここにgnuコンパイラーを使用した結果を示します(マシンでコンパイルすることで確認しましたが、xenialのgnu g ++ 5.4は、正確にlinaroの4.6.3よりもはるかに高速です)
Intel i7 4700MQ xenial
short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173
Intel i3 2370Mにも同様の結果があります
short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484
Intel(R)Celeron(R)2955U(xenialを実行しているAcer C720 Chromebook)
short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226
DigitalOcean 1GB Droplet Intel(R)Xeon(R)CPU E5-2630L v2(running trusty)
short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649
AMD Opteron(tm)プロセッサー4122(正確)
short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536
これは http://Pastebin.com/Kx8WGUfg as benchmark-pc.c
g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c
複数のパスを実行しましたが、これは一般的な数値が同じ場合のようです。
注目すべき例外の1つは、ALU mul対FPU mulです。加算と減算は些細な違いのように見えます。
上記はチャート形式です(クリックするとフルサイズになり、小さいほど高速で望ましい):
https://Gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
short add: 0.773049
short sub: 0.789793
short mul: 0.960152
short div: 3.273668
int add: 0.837695
int sub: 0.804066
int mul: 0.960840
int div: 3.281113
long add: 0.829946
long sub: 0.829168
long mul: 0.960717
long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
float add: 1.081649
float sub: 1.080351
float mul: 1.323401
float div: 1.984582
double add: 1.081079
double sub: 1.082572
double mul: 1.323857
double div: 1.968488
short add: 1.235603
short sub: 1.235017
short mul: 1.280661
short div: 5.535520
int add: 1.233110
int sub: 1.232561
int mul: 1.280593
int div: 5.350998
long add: 1.281022
long sub: 1.251045
long mul: 1.834241
long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
float add: 2.307852
float sub: 2.305122
float mul: 2.298346
float div: 4.833562
double add: 2.305454
double sub: 2.307195
double mul: 2.302797
double div: 5.485736
short add: 1.040745
short sub: 0.998255
short mul: 1.240751
short div: 3.900671
int add: 1.054430
int sub: 1.000328
int mul: 1.250496
int div: 3.904415
long add: 0.995786
long sub: 1.021743
long mul: 1.335557
long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
float add: 1.572640
float sub: 1.532714
float mul: 1.864489
float div: 2.825330
double add: 1.535827
double sub: 1.535055
double mul: 1.881584
double div: 2.777245
考慮すべき2つのポイント-
最新のハードウェアは、命令をオーバーラップさせ、それらを並行して実行し、ハードウェアを最大限に活用するためにそれらを並べ替えることができます。また、重要な浮動小数点プログラムは、配列、ループカウンターなどへのインデックスを計算するだけでも、重要な整数の働きをする可能性が高いため、遅い浮動小数点命令を使用している場合でも、ハードウェアの別のビットで実行される可能性があります整数の仕事のいくつかと重なった。私のポイントは、浮動小数点命令が整数命令よりも遅い場合でも、より多くのハードウェアを使用できるため、プログラム全体が高速に実行される可能性があるということです。
いつものように、確実にする唯一の方法は、実際のプログラムをプロファイルすることです。
2番目のポイントは、最近のほとんどのCPUには、複数の浮動小数点値をすべて同時に処理できる浮動小数点用のSIMD命令があるということです。たとえば、4つのフロートを1つのSSEレジスタにロードし、それらに対して4つの乗算をすべて並行して実行できます。 SSE命令を使用するようにコードの一部を書き換えることができる場合、整数バージョンよりも高速になる可能性があります。 Visual c ++は、これを行うコンパイラ組み込み関数を提供します。詳細については、 http://msdn.Microsoft.com/en-us/library/x5c07e2a(v = VS.80).aspx を参照してください。
1秒間に数百万回呼び出されるコード(たとえば、グラフィックアプリケーションで画面に線を引くなど)を書いているのでない限り、整数演算と浮動小数点演算がボトルネックになることはめったにありません。
効率の問題に対する通常の最初のステップは、コードをプロファイルして、ランタイムが実際に費やされている場所を確認することです。このためのlinuxコマンドはgprof
です。
編集:
整数と浮動小数点数を使用して線描画アルゴリズムをいつでも実装でき、何度も呼び出して、違いが生じるかどうかを確認できると思います。
現在、整数演算は通常、浮動小数点演算よりも少し高速です。したがって、整数と浮動小数点で同じ演算を使用して計算できる場合は、整数を使用します。しかし、「これは非常に多くの迷惑な問題を引き起こし、多くの迷惑なコードを追加します」と言っています。浮動小数点ではなく整数演算を使用しているため、より多くの演算が必要なようです。その場合、浮動小数点はより速く実行されます。
より多くの整数演算が必要になるとすぐに、おそらくもっと多くの整数演算が必要になるので、わずかな速度の利点は、追加の演算によって食い尽くされるだけではありません
浮動小数点コードはより単純です。つまり、コードの記述が高速になります。つまり、速度が重要な場合は、コードの最適化により多くの時間を費やすことができます。
剰余演算がない場合、浮動小数点バージョンははるかに遅くなります。すべての加算はシーケンシャルであるため、CPUは合計を並列化できません。待ち時間は非常に重要です。 FPUの追加レイテンシは通常3サイクルですが、整数の追加レイテンシは1サイクルです。ただし、剰余演算子の分周器は、現代のCPUでは完全にパイプライン化されていないため、おそらく重要な部分になります。したがって、除算/剰余命令が時間の大部分を消費すると仮定すると、追加レイテンシによる差は小さくなります。
Rand()の代わりに数字に1を追加したテストを実行しました。結果(x86-64上)は次のとおりです。