2017-09-25 4 views
2

私はEigenを使っていくつかのSSEコードを書こうとしています。SSEのパフォーマンスEigen

#ifndef EIGEN_DONT_VECTORIZE // Not needed with Intel C++ Compiler XE 15.0 
#define EIGEN_VECTORIZE_SSE4_2 
#define EIGEN_VECTORIZE_SSE4_1 
#define EIGEN_VECTORIZE_SSSE3 
#define EIGEN_VECTORIZE_SSE3 
#endif 

#include "stdafx.h" 
#include <iostream> 
#include <unsupported/Eigen/AlignedVector3> 
#include <Eigen/StdVector> 
#include <chrono> 

int _tmain(int argc, _TCHAR* argv[]) { 
    static const int SIZE = 4000000; 
    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE(1, 1, 1); 
    //EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    //std::vector<Eigen::AlignedVector3<float>> C_SSE(SIZE, Eigen::AlignedVector3<float>(0,0,0)); 


    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE1(1, 1, 1); 
    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE2(1, 1, 1); 
    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE3(1, 1, 1); 
    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE4(1, 1, 1); 

    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE_increment_unroll(16, 16, 16); 

    A_SSE2 += B_SSE; 
    A_SSE3 = A_SSE2 + B_SSE; 
    A_SSE4 = A_SSE3 + B_SSE; 


    std::vector<Eigen::AlignedVector3<float>> C_SSE(SIZE, Eigen::AlignedVector3<float>(0, 0, 0)); 

    auto start2 = std::chrono::system_clock::now(); 

    // no unroll 
    for (int iteration = 0; iteration < SIZE; ++iteration) { 
     A_SSE += B_SSE; 
     C_SSE[iteration] = A_SSE; 
    } 

    //// own unroll 
    //for (int iteration = 0; iteration < SIZE/8; ++iteration){ 
    // A_SSE1 += B_SSE_increment_unroll; 
    // A_SSE2 += B_SSE_increment_unroll; 
    // A_SSE3 += B_SSE_increment_unroll; 
    // A_SSE4 += B_SSE_increment_unroll; 

    // C_SSE[iteration * 2] = A_SSE1; 
    // C_SSE[iteration * 2 + 1] = A_SSE2; 
    // C_SSE[iteration * 2 + 2] = A_SSE3; 
    // C_SSE[iteration * 2 + 3] = A_SSE4; 

    //} 

    auto end2 = std::chrono::system_clock::now(); 
    auto elapsed2 = end2 - start2; 
    std::cout << "Eigen aligned vector " << elapsed2.count() << '\n'; 

    Eigen::Matrix3Xf A = Eigen::Matrix3Xf::Zero(3, SIZE); 
    Eigen::Vector3f B(3, 3, 3); 
    Eigen::Vector3f C(2, 2, 2); 

    auto start1 = std::chrono::system_clock::now(); 

    for (int iteration = 0; iteration < SIZE; ++iteration) { 
     B += C; 
     A.col(iteration) = B; 
    } 
    auto end1 = std::chrono::system_clock::now(); 
    auto elapsed1 = end1 - start1; 
    std::cout << "Eigen matrix " << elapsed1.count() << '\n'; 


    float *pResult = (float*)_aligned_malloc(SIZE * sizeof(float) * 4, 16); // align to 16-byte for SSE 
    auto start3 = std::chrono::system_clock::now(); 

    __m128 x; 
    __m128 xDelta = _mm_set1_ps(2.0f);  // Set the xDelta to (4,4,4,4) 
    __m128 *pResultSSE = (__m128*) pResult; 

    x = _mm_set_ps(1.0f, 1.0f, 1.0f, 1.0f); // Set the initial values of x to (4,3,2,1) 

    for (int iteration = 0; iteration < SIZE; ++iteration) 
    { 
     x = _mm_add_ps(x, xDelta); 
     pResultSSE[iteration] = x; 
    } 

    auto end3 = std::chrono::system_clock::now(); 
    auto elapsed3 = end3 - start3; 
    std::cout << "Own sse " << elapsed3.count() << '\n'; 

} 

タイミングが

  • 固有値、配向ベクトルをほどき私のPC上で、奇妙に思える:20057
  • 固有合わせベクトルなしアンロール:〜120320
  • 固有マトリックス:コードを考えると

    〜120207(アンロールなしと同じ)

  • 自分のSSE:160784

私がアセンブリを調べると、アライメントされたバージョンとOwn SSEはaddps movapsを使用しますが、ループを手動でアンロールするまではパフォーマンスは向上しません。実行しても(50%どんなブーストも得られない。バージョン固有の行列はsseを使用せず、同じパフォーマンスを達成し、インラインアセンブリは16回の反復で展開を示します。手動のアンローリングはそれほど大きな影響を与えますか?私たちは手動でSSEのためにそれを行うべきですか?CPUのプロパティでオンになっているかどうかは、それが依存していますか?

編集: 合計すると。 SSE命令は、展開されていないループが展開されていない場合と同じ結果を保持することを証明できないため、パフォーマンスが向上しないため、メモリ記憶のレイテンシを隠すことができません。しかし、アセンブリコードでは、 "単一"命令は1つのレジスタしか使用せず、展開されていないループでそれをインクリメントします。 SSE中毒が垂直方向に行われた場合(整列したベクトル内の単一の浮動小数点は同じ量の加算演算を蓄積する)、コンパイラはアンローリングの平等性を証明できるはずです。 SSE操作は、デフォルトでコンパイラによって最適化されていませんか?アンロールループが実行順序を保持しているので、非連想計算が維持されるため、自動アンローリングが可能になるはずですが、どうしてそれが起こらないのでしょうか?

EDIT:固有値から としては、私がテストを実行する提案が、ベンチユニットは、Visual Studio 2017の下では動作しませんので、それは

#include <iostream> 
#include <vector> 
#include <unsupported/Eigen/AlignedVector3> 
#include <chrono> 
#include <numeric> 

EIGEN_DONT_INLINE 
void vector_no_unroll(std::vector<Eigen::AlignedVector3<float>>& out) 
{ 
    Eigen::AlignedVector3<float> A_SSE(1, 1, 1); 
    Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    for (auto &x : out) 
    { 
     A_SSE += B_SSE; 
     x = A_SSE; 
    } 
} 

EIGEN_DONT_INLINE 
void vector_unrolled(std::vector<Eigen::AlignedVector3<float>>& out) 
{ 
    Eigen::AlignedVector3<float> A_SSE1(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE2(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE3(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE4(1, 1, 1); 

    Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    Eigen::AlignedVector3<float> B_SSE_increment_unroll(16, 16, 16); 

    A_SSE2 += B_SSE; 
    A_SSE3 = A_SSE2 + B_SSE; 
    A_SSE4 = A_SSE3 + B_SSE; 
    for (size_t i = 0; i<out.size(); i += 4) 
    { 
     A_SSE1 += B_SSE_increment_unroll; 
     A_SSE2 += B_SSE_increment_unroll; 
     A_SSE3 += B_SSE_increment_unroll; 
     A_SSE4 += B_SSE_increment_unroll; 
     out[i + 0] = A_SSE1; 
     out[i + 1] = A_SSE2; 
     out[i + 2] = A_SSE3; 
     out[i + 3] = A_SSE4; 
    } 
} 

EIGEN_DONT_INLINE 
void eigen_matrix(Eigen::Matrix3Xf& out) 
{ 
    Eigen::Vector3f B(1, 1, 1); 
    Eigen::Vector3f C(2, 2, 2); 

    for (int i = 0; i < out.cols(); ++i) { 
     B += C; 
     out.col(i) = B; 
    } 
} 

template<int unrolling> EIGEN_DONT_INLINE 
void eigen_matrix_unrolled(Eigen::Matrix3Xf& out) 
{ 
    Eigen::Matrix<float, 3, unrolling> B = Eigen::Matrix<float, 1, unrolling>::LinSpaced(3.f, 1 + 2 * unrolling).template replicate<3, 1>(); 

    for (int i = 0; i < out.cols(); i += unrolling) { 
     out.middleCols<unrolling>(i) = B; 
     B.array() += float(2 * unrolling); 
    } 
} 

int main() { 
    static const int SIZE = 4000000; 

    int tries = 30; 
    int rep = 10; 


    std::vector<int> Timings(tries, 0); 
    { 
     Eigen::Matrix3Xf A(3, SIZE); 
#pragma loop(1) 
     for (int iter = 0; iter < tries; ++iter) 
     { 
      auto start1 = std::chrono::system_clock::now(); 
      eigen_matrix(A); 
      Timings[iter] = (std::chrono::system_clock::now() - start1).count(); 
     } 
    } 
    std::cout << "eigen matrix Min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n"; 
    std::cout << "eigen matrix Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n"; 

    { 
     Eigen::Matrix3Xf A(3, SIZE); 
#pragma loop(1) 
     for (int iter = 0; iter < tries; ++iter) 
     { 
      auto start1 = std::chrono::system_clock::now(); 
      eigen_matrix_unrolled<4>(A); 
      Timings[iter] = (std::chrono::system_clock::now() - start1).count(); 
     } 
    } 
    std::cout << "eigen matrix unrolled 4 min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n"; 
    std::cout << "eigen matrix unrolled 4 Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n"; 

    { 
     Eigen::Matrix3Xf A(3, SIZE); 
#pragma loop(1) 
     for (int iter = 0; iter < tries; ++iter) 
     { 
      auto start1 = std::chrono::system_clock::now(); 
      eigen_matrix_unrolled<8>(A); 
      Timings[iter] = (std::chrono::system_clock::now() - start1).count(); 
     } 
    } 
    std::cout << "eigen matrix unrolled 8 min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n"; 
    std::cout << "eigen matrix unrolled 8 Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n"; 

    { 
     std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0)); 
#pragma loop(1) 
     for (int iter = 0; iter < tries; ++iter) 
     { 
      auto start1 = std::chrono::system_clock::now(); 
      vector_no_unroll(A); 
      Timings[iter] = (std::chrono::system_clock::now() - start1).count(); 
     } 
    } 
    std::cout << "eigen vector min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n"; 
    std::cout << "eigen vector Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n"; 

    { 
     std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0)); 
#pragma loop(1) 
     for (int iter = 0; iter < tries; ++iter) 
     { 
      auto start1 = std::chrono::system_clock::now(); 
      vector_unrolled(A); 
      Timings[iter] = (std::chrono::system_clock::now() - start1).count(); 
     } 
    } 
    std::cout << "eigen vector unrolled min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n"; 
    std::cout << "eigen vector unrolled Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n"; 

} 

によって置き換えられ、8台の切り抜いたマシン(すべてのウィンドウ)に結果を確認したと以下の結果

固有行列分を取得:110477ミリ秒

固有の行列が平均:131691ミリ秒

40099のMS

固有行列展開4平均:54812のMS

固有行列展開8分:40001のMS

固有行列展開8平均:51482のMS

固有ベクトル固有行列は4分を広げ分:100270のMS

固有ベクトルの意味:117316のMS

固有ベクトル展開分:59966のMS

固有ベクトル平均:65847ms

私がテストしたすべてのマシンで、最も古いものが除外されました。新しいマシンのように見えますが、小さなアンローリングは非常に有益です(結果は、1.5倍から3.5倍のスピードアップが4倍になり、アンローリングが8,16,32、または256倍であってもインクリメントしません)。

+0

*すべての*最適化はCPUごとに*する必要があります。それはすべて根本的な質問に戻ってきます。それは遅すぎますか?答えが* yes *の場合、早すぎる最適化は避けています。これは時期尚早な最適化と思われますか? Intel SSEまたはC++ SSEですか? – Sebivor

+0

これらのルチンを最適化するのではなく、一般的にsseを使用します。 CPUごとの最適化では、たとえばxmmのレジスタをカウントし、すべてを使用するためにアンロールしますか? – CzakCzan

+0

ループオーバーヘッド上のフロントエンドボトルネックを回避するために、ADDPSまたはFMAのレイテンシを隠すために展開します。例えばドットプロダクトの場合:https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction。 –

答えて

1

あなたのタイミングは非常に不正確です(あなたのコードを何度も実行すると、私は多くのバリエーションを得ています)。再現性を向上させるには、各バリアントを複数回実行し、最小限の時間を取る必要があります。私は一緒に固有の一部であるBenchUtilsを使用してベンチマークを置く:

#include <iostream> 
#include <unsupported/Eigen/AlignedVector3> 
#include <bench/BenchUtil.h> 

EIGEN_DONT_INLINE 
void vector_no_unroll(std::vector<Eigen::AlignedVector3<float>>& out) 
{ 
    Eigen::AlignedVector3<float> A_SSE(1, 1, 1); 
    Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    for(auto &x : out) 
    { 
     A_SSE += B_SSE; 
     x = A_SSE; 
    } 
} 

EIGEN_DONT_INLINE 
void vector_unrolled(std::vector<Eigen::AlignedVector3<float>>& out) 
{ 
    Eigen::AlignedVector3<float> A_SSE1(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE2(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE3(1, 1, 1); 
    Eigen::AlignedVector3<float> A_SSE4(1, 1, 1); 

    Eigen::AlignedVector3<float> B_SSE(2, 2, 2); 
    Eigen::AlignedVector3<float> B_SSE_increment_unroll(16, 16, 16); 

    A_SSE2 += B_SSE; 
    A_SSE3 = A_SSE2 + B_SSE; 
    A_SSE4 = A_SSE3 + B_SSE; 
    for(size_t i=0; i<out.size(); i+=4) 
    { 
     A_SSE1 += B_SSE_increment_unroll; 
     A_SSE2 += B_SSE_increment_unroll; 
     A_SSE3 += B_SSE_increment_unroll; 
     A_SSE4 += B_SSE_increment_unroll; 
     out[i + 0] = A_SSE1; 
     out[i + 1] = A_SSE2; 
     out[i + 2] = A_SSE3; 
     out[i + 3] = A_SSE4; 
    } 
} 

EIGEN_DONT_INLINE 
void eigen_matrix(Eigen::Matrix3Xf& out) 
{ 
    Eigen::Vector3f B(1, 1, 1); 
    Eigen::Vector3f C(2, 2, 2); 

    for (int i = 0; i < out.cols(); ++i) { 
     B += C; 
     out.col(i) = B; 
    } 
} 

template<int unrolling> EIGEN_DONT_INLINE 
void eigen_matrix_unrolled(Eigen::Matrix3Xf& out) 
{ 
    Eigen::Matrix<float,3,unrolling> B = Eigen::Matrix<float, 1, unrolling>::LinSpaced(3.f, 1+2*unrolling).template replicate<3,1>(); 

    for (int i = 0; i < out.cols(); i+=unrolling) { 
     out.middleCols<unrolling>(i) = B; 
     B.array() += float(2*unrolling); 
    } 
} 

int main() { 
    static const int SIZE = 4000000; 

    int tries = 10; 
    int rep = 10; 
    BenchTimer t; 

    std::cout.precision(4); 
    { 
     std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0)); 
     BENCH(t, tries, rep, vector_no_unroll(A)); 
     std::cout << "no unroll: " << 1e3*t.best(CPU_TIMER) << "ms\n"; 
    } 
    { 
     std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0)); 
     BENCH(t, tries, rep, vector_unrolled(A)); 
     std::cout << "unrolled:  " << 1e3*t.best(CPU_TIMER) << "ms\n"; 
    } 
    { 
     Eigen::Matrix3Xf A(3, SIZE); 
     BENCH(t, tries, rep, eigen_matrix(A)); 
     std::cout << "eigen matrix: " << 1e3*t.best(CPU_TIMER) << "ms\n"; 
    } 
    { 
     Eigen::Matrix3Xf A(3, SIZE); 
     BENCH(t, tries, rep, eigen_matrix_unrolled<4>(A)); 
     std::cout << "eigen unrd<4>: " << 1e3*t.best(CPU_TIMER) << "ms\n"; 
    } 
    { 
     Eigen::Matrix3Xf A(3, SIZE); 
     BENCH(t, tries, rep, eigen_matrix_unrolled<8>(A)); 
     std::cout << "eigen unrd<8>: " << 1e3*t.best(CPU_TIMER) << "ms\n"; 
    } 
} 

私は-msse2-msse4.2または-mavx2でコンパイルするのはほぼ独立した非常に似回を取得しています:

no unroll: 66.72ms 
unrolled:  66.83ms 
eigen matrix: 57.56ms 
eigen unrd<4>: 50.39ms 
eigen unrd<8>: 51.19ms 

明らかに、AligenedVector3の亜種は一貫して最も遅く、アンローリングとは有意差がありません。行列のバリアントは、行列のバリアントを手作業で展開して(反復ごとに4つまたは8つの列を処理する)約7/8時間かかるので、元の時間の約3/4に短縮されます。

これは、メモリ帯域幅がすべてのベクトル化バリアントのボトルネックである可能性が高いことを示しています。展開された行列の変形は、実際の操作(または個々のスカラの手動コピー)によって制限されることがあります。

ベンチマークは、Ubuntu 16.04でg ++ 5.4.1を使用しているIntel Core i5-4210U CPU @ 1.70GHzで行われ、比較的最近のEigen開発ブランチのチェックアウトが行われました。

+0

完全な答えをありがとうが、メモリ帯域制限をチェックする方法はありますか? SSEを使用するか、機械に固有のものを使用するかどうかを判断するための「経験則」はありますか?正直、私はsqrtで単純な追加を置き換えても、私はインテルのコアi7-7700HQ @ 2.8GHZのWindows 10ではスピードアップを得られません。 – CzakCzan

+0

メモリの帯域幅のチェックに関しては、例えば:https://stackoverflow.com/questions/3386042/howウィンドウ間利用率 - メモリ - 帯域幅使用率ループ間にデータ依存関係がない場合は、手動アンローリングが大きなメリットをもたらすとは思いません。 – chtz

+0

もちろん、分岐予測が非常に悪く、部分的なループ展開によってパフォーマンスが向上する(古い命令キャッシュまたは低レベルのCPUがあるかもしれません)。 – chtz