web-dev-qa-db-ja.com

C配列がstd :: arrayよりもはるかに高速なのはなぜですか?

現在、多くの大きな行列やベクトルで動作するパフォーマンスクリティカルなコードをC++で記述しています。私たちの調査に関しては、std::arrayと標準のC配列の間に大きなパフォーマンスの違いはないはずです( この質問 または this を参照)。ただし、テスト中に、std::arrayよりもC配列を使用することで、パフォーマンスが大幅に向上しました。これは私たちのデモコードです:

#include <iostream>
#include <array>
#include <sys/time.h>

#define ROWS 784
#define COLS 100
#define RUNS 50

using std::array;

void DotPComplex(array<double, ROWS> &result, array<double, ROWS> &vec1, array<double, ROWS> &vec2){
  for(int i = 0; i < ROWS; i++){
    result[i] = vec1[i] * vec2[i];
  }
}

void DotPSimple(double result[ROWS], double vec1[ROWS], double vec2[ROWS]){
  for(int i = 0; i < ROWS; i++){
    result[i] = vec1[i] * vec2[i];
  }
}

void MatMultComplex(array<double, ROWS> &result, array<array<double, COLS>, ROWS> &mat, array<double, ROWS> &vec){
  for (int i = 0; i < COLS; ++i) {
      for (int j = 0; j < ROWS; ++j) {
        result[i] += mat[i][j] * vec[j];
      }
  }
}

void MatMultSimple(double result[ROWS], double mat[ROWS][COLS], double vec[ROWS]){
  for (int i = 0; i < COLS; ++i) {
      for (int j = 0; j < ROWS; ++j) {
        result[i] += mat[i][j] * vec[j];
      }
  }
}

double getTime(){
    struct timeval currentTime;
    gettimeofday(&currentTime, NULL);
    double tmp = (double)currentTime.tv_sec * 1000.0 + (double)currentTime.tv_usec/1000.0;
    return tmp;
}

array<double, ROWS> inputVectorComplex = {{ 0 }};
array<double, ROWS> resultVectorComplex = {{ 0 }};
double inputVectorSimple[ROWS] = { 0 };
double resultVectorSimple[ROWS] = { 0 };

array<array<double, COLS>, ROWS> inputMatrixComplex = {{0}};
double inputMatrixSimple[ROWS][COLS] = { 0 };

int main(){
  double start;
  std::cout << "DotP test with C array: " << std::endl;
  start = getTime();
  for(int i = 0; i < RUNS; i++){
    DotPSimple(resultVectorSimple, inputVectorSimple, inputVectorSimple);
  }
  std::cout << "Duration: " << getTime() - start << std::endl;

  std::cout << "DotP test with C++ array: " << std::endl;
  start = getTime();
  for(int i = 0; i < RUNS; i++){
    DotPComplex(resultVectorComplex, inputVectorComplex, inputVectorComplex);
  }
  std::cout << "Duration: " << getTime() - start << std::endl;

  std::cout << "MatMult test with C array : " << std::endl;
  start = getTime();
  for(int i = 0; i < RUNS; i++){
    MatMultSimple(resultVectorSimple, inputMatrixSimple, inputVectorSimple);
  }
  std::cout << "Duration: " << getTime() - start << std::endl;

  std::cout << "MatMult test with C++ array: " << std::endl;
  start = getTime();
  for(int i = 0; i < RUNS; i++){
    MatMultComplex(resultVectorComplex, inputMatrixComplex, inputVectorComplex);
  }
  std::cout << "Duration: " << getTime() - start << std::endl;
}

コンパイル:icpc demo.cpp -std=c++11 -O0これは結果です:

DotP test with C array: 
Duration: 0.289795 ms
DotP test with C++ array: 
Duration: 1.98413 ms
MatMult test with C array : 
Duration: 28.3459 ms
MatMult test with C++ array: 
Duration: 175.15 ms

-O3フラグ付き:

DotP test with C array: 
Duration: 0.0280762 ms
DotP test with C++ array: 
Duration: 0.0288086 ms
MatMult test with C array : 
Duration: 1.78296 ms
MatMult test with C++ array: 
Duration: 4.90991 ms

C配列の実装は、コンパイラーの最適化なしではるかに高速です。どうして?コンパイラの最適化を使用すると、内積も同様に高速になります。ただし、行列の乗算では、C配列を使用すると大幅に高速化されます。 std::arrayを使用するときに同等のパフォーマンスを達成する方法はありますか?


更新:

使用したコンパイラ:icpc 17.0.0

gcc 4.8.5を使用すると、最適化レベルを使用するインテル®コンパイラーよりもコードの実行速度が大幅に低下します。したがって、主にインテル®コンパイラーの動作に関心があります。

Jonas が示唆するように、RUNS 50.000を調整して次の結果を出しました(インテルコンパイラー)。

-O0フラグ付き:

DotP test with C array: 
Duration: 201.764 ms
DotP test with C++ array: 
Duration: 1020.67 ms
MatMult test with C array : 
Duration: 15069.2 ms
MatMult test with C++ array: 
Duration: 123826 ms

-O3フラグ付き:

DotP test with C array: 
Duration: 16.583 ms
DotP test with C++ array: 
Duration: 15.635 ms
MatMult test with C array : 
Duration: 980.582 ms
MatMult test with C++ array: 
Duration: 2344.46 ms
16
The Floe

まず、使用している実行の量が少なすぎます。個人的には、(コードを実行する前に)「期間」の測定値がミリ秒であることを認識していませんでした。

RUNSDotPSimpleDotPComplexを5,000,000に増やすと、タイミングは次のようになります。

Cアレイを使用したDotPテスト:

期間:1074.89

C++配列を使用したDotPテスト:

期間:1085.34

つまり、それらは同等に高速であることに非常に近いです。実際、ベンチマークの確率的性質により、これまでで最も速いものはテストごとに異なりました。 MatMultSimpleMatMultComplexについても同じことが言えますが、必要なのは50,000回だけです。

本当に測定して詳細を知りたい場合は、このベンチマークの確率的性質を受け入れ、「期間」測定の分布を概算する必要があります。関数のランダムな順序を含めて、順序の偏りを取り除きます。

編集: アセンブリコード (user2079303の回答から)は、最適化を有効にしても違いがないことを完全に証明しています。したがって、ゼロコストの抽象化は、実際には最適化が有効になっているゼロコストであり、これは妥当な要件です。

更新:

私が使用したコンパイラ:

g++ (Debian 6.3.0-6) 6.3.0 20170205

次のコマンドを使用します。

g++ -Wall -Wextra -pedantic -O3 test.cpp

このプロセッサの使用:

Intel(R) Core(TM) i5-4300U CPU @ 1.90GHz
20
Jonas

なぜ...コンパイラの最適化なしではるかに高速です。どうして?

何らかの理由でコンパイラが選択します。コンパイラーを最適化させない場合、2つの異なるコードが同じ動作をしていても、同様のパフォーマンスを期待することはできません。最適化が有効になっている場合、コンパイラーは抽象化されたコードを効率的なコードに変換できる可能性があり、パフォーマンスは同等である必要があります。

std::arrayの使用には、ポインターの使用が含まない関数呼び出しが含まれます。たとえば、std::array::operator[]は関数ですが、ポインタの添え字演算子は関数ではありません。関数呼び出しを行うことは、関数呼び出しを行わないことよりも潜在的に遅くなります。これらの関数呼び出しはすべて離れて最適化できます(インライン展開)が、最適化を有効にしないことを選択した場合、関数呼び出しは残ります。

ただし、行列の乗算では、C配列を使用すると大幅に高速化されます。

おそらくあなたのベンチマーク、またはコンパイラの癖。 ここ 両方の関数は同じアセンブリを持っているので、同じパフォーマンスを持っています

編集:私はジョナスの答えに同意します。ベンチマークの反復回数が少なすぎます。また、ベンチマークを繰り返し、偏差を分析しないと、2つの測定値の差が有意であるかどうかを判断することはできません。


結論は次のとおりです。

  • 最適化が有効になっている場合、C配列はstd::arrayよりも高速ではありません。リンクに示されているように、少なくともclang3.9.1でコンパイルする場合はそうではありません。おそらくあなたのコンパイラは異なるアセンブリを生成しますが、そうすべき理由はわかりません。

  • C++のゼロコスト抽象化は、最適化後にのみゼロコストになります。

  • 意味のあるマイクロベンチマークを書くことは簡単ではありません。

10
eerorika