113

私は C++ でいくつかのパフォーマンスが重要な作業を行っています。現在、本質的に浮動小数点である問題に対して整数計算を使用しています。これにより、多くの厄介な問題が発生し、多くの厄介なコードが追加されます。

さて、浮動小数点の計算がおよそ 386 日ほど遅かったことについて読んだことを覚えています。私は (IIRC)、オプションのコプロセッサがあったと考えています。しかし、最近では指数関数的に複雑で強力な CPU が使用されているため、浮動小数点または整数の計算を行っても「速度」に違いはありませんか? 特に、実際の計算時間は、パイプラインのストールを引き起こしたり、メイン メモリから何かをフェッチしたりするようなものに比べて小さいので?

正しい答えはターゲット ハードウェアでベンチマークすることだと思いますが、これをテストするにはどのような方法がよいでしょうか? 私は 2 つの小さな C++ プログラムを作成し、それらの実行時間を Linux での「時間」と比較しましたが、実際の実行時間は変動しすぎています (仮想サーバーで実行している場合は役に立ちません)。何百ものベンチマークを実行したり、グラフを作成したりするのに 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 を使用してはならない」という一般化に反対しました。この一般化された仮定を反証する方法を (これが真実である場合でも) 探しています。すべての作業を行って後でプロファイリングしない限り、正確な結果を予測することは不可能であることは理解しています。

とにかく、すべての優れた回答と助けに感謝します。他に何でも自由に追加してください:)。

4

11 に答える 11

56

たとえば (数値が小さいほど高速です)、

64 ビット インテル 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]

Dan が指摘したように、クロック周波数を正規化しても (パイプライン化された設計ではそれ自体が誤解を招く可能性があります)、結果は CPU アーキテクチャ(個々のALU / FPUパフォーマンスおよび使用可能な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;
}
于 2010-03-31T06:11:25.497 に答える
39

残念ながら、「場合による」という回答しかできません...

私の経験から、パフォーマンスには非常に多くの変数があります...特に整数と浮動小数点演算の間には。プロセッサによって「パイプライン」の長さが異なるため、(x86 などの同じファミリ内であっても) プロセッサごとに大きく異なります。また、一部の操作は一般に非常に単純で (加算など)、プロセッサを介して加速された経路を持ちますが、他の操作 (除算など) ははるかに長い時間がかかります。

もう 1 つの大きな変数は、データが存在する場所です。追加する値がわずかしかない場合は、すべてのデータをキャッシュに常駐させ、そこですばやく CPU に送信できます。すでにキャッシュにデータがある非常に遅い浮動小数点演算は、システム メモリから整数をコピーする必要がある整数演算よりも何倍も高速です。

パフォーマンスが重要なアプリケーションに取り組んでいるため、この質問をしていると思います。x86 アーキテクチャ用に開発していて、追加のパフォーマンスが必要な場合は、SSE 拡張機能の使用を検討することをお勧めします。これにより、単精度浮動小数点演算が大幅に高速化されます。これは、複数のデータに対して同じ操作を一度に実行できることに加えて、SSE 操作用のレジスタの別の*バンクがあるためです。(2番目の例では、「double」の代わりに「float」を使用していることに気づきました。これにより、単精度の数学を使用していると思われます)。

*注意: 古い MMX 命令を使用すると、実際にはプログラムの速度が低下します。これらの古い命令は実際には FPU と同じレジスタを使用し、FPU と MMX の両方を同時に使用することが不可能になるからです。

于 2010-03-31T04:05:24.897 に答える
31

固定小数点演算と浮動小数点演算の間には実際の速度に大きな違いがある可能性がありますが、ALU と FPU の理論上のベストケースのスループットはまったく関係ありません。代わりに、アーキテクチャ上の整数レジスタと浮動小数点レジスタ (レジスタ名ではなく実際のレジスタ) の数で、それ以外の場合は計算 (ループ制御など) で使用されないもので、キャッシュ ラインに収まる各タイプの要素の数です。 、整数演算と浮動小数点演算のセマンティクスが異なることを考慮して可能な最適化 - これらの効果が支配的です。ここでは、アルゴリズムのデータ依存関係が重要な役割を果たすため、一般的な比較では問題のパフォーマンス ギャップを予測することはできません。

たとえば、整数の加算は交換可能であるため、コンパイラがベンチマークに使用したようなループを検出した場合 (結果が不明瞭にならないようにランダム データが事前に準備されていると仮定)、ループを展開して部分和を計算できます。依存関係がない場合は、ループが終了したときにそれらを追加します。しかし、浮動小数点を使用すると、コンパイラは要求されたのと同じ順序で操作を実行する必要があります (そこにシーケンス ポイントがあるため、コンパイラは同じ結果を保証する必要があり、並べ替えができません)。前回の結果。

一度にキャッシュに格納できる整数オペランドの数も増える可能性があります。そのため、固定小数点バージョンは、FPU のスループットが理論的により高いマシンでも、浮動小数点バージョンよりも桁違いに優れている可能性があります。

于 2010-03-31T05:20:48.473 に答える
20

加算は よりもはるかに高速でrandあるため、プログラムは (特に) 役に立ちません。

パフォーマンスのホットスポットを特定し、プログラムを段階的に修正する必要があります。最初に解決する必要がある開発環境に問題があるようです。小さな問題セットの場合、プログラムを PC で実行することは不可能ですか?

一般に、整数演算で FP ジョブを試みると、処理が遅くなります。

于 2010-03-31T03:24:05.740 に答える
8

考慮すべき2つのポイント -

最新のハードウェアは、ハードウェアを最大限に活用するために、命令をオーバーラップさせ、並行して実行し、順序を変更することができます。また、重要な浮動小数点プログラムは、配列やループカウンターなどのインデックスを計算するだけであっても、重要な整数作業を行う可能性が高いため、遅い浮動小数点命令を使用している場合でも、ハードウェアの別のビットで実行されている可能性があります。整数作業の一部と重複しています。私のポイントは、浮動小数点命令が整数命令よりも遅い場合でも、より多くのハードウェアを利用できるため、プログラム全体がより高速に実行される可能性があるということです。

いつものように、確実にする唯一の方法は、実際のプログラムをプロファイリングすることです。

2 つ目のポイントは、最近のほとんどの CPU には、複数の浮動小数点値を同時に操作できる浮動小数点用の SIMD 命令があることです。たとえば、4 つの float を 1 つの SSE レジスタにロードし、4 つの乗算をすべて並行して実行できます。コードの一部を SSE 命令を使用するように書き直すことができれば、整数バージョンよりも高速になる可能性があります。Visual C++ には、これを行うためのコンパイラ組み込み関数が用意されています。詳細については、http://msdn.microsoft.com/en-us/library/x5c07e2a( v=VS.80 ).aspx を参照してください。

于 2010-03-31T08:11:44.720 に答える
4

1 秒間に何百万回も呼び出されるコード (たとえば、グラフィック アプリケーションで画面に線を描画するなど) を作成している場合を除き、整数演算と浮動小数点演算がボトルネックになることはめったにありません。

効率の問題に対する通常の最初のステップは、コードをプロファイリングして、実行時間が実際に費やされている場所を確認することです。このための Linux コマンドはgprof.

編集:

整数と浮動小数点数を使用して線描画アルゴリズムをいつでも実装できると思いますが、それを何度も呼び出して、違いがあるかどうかを確認してください。

http://en.wikipedia.org/wiki/Bresenham's_algorithm

于 2010-03-31T03:21:49.843 に答える
3

rand() の代わりに数値に 1 を追加するだけのテストを実行しました。結果 (x86-64 上) は次のとおりです。

  • 短い: 4.260 秒
  • 整数: 4.020 秒
  • 長い長い: 3.350 秒
  • フロート: 7.330 秒
  • ダブル: 7.210 秒
于 2010-03-31T04:47:24.860 に答える
0

なんとも頼もしい「聞いた話」によると、昔は整数計算の方が浮動小数点より20倍から50倍速かったのですが、最近は2倍以下です。

于 2010-03-31T03:24:18.390 に答える