C++で置き換えてみたのですが、圧倒的に元のEigenの方が速かったです。それで、Eigen核心の部分のコードをDisassembleして見たのですが、次のように僅か5命令で、疎行列とベクタをロードし積和演算を行うことが達成されていました。見事なコードです。 ソースはC++であり、どのように書いたらこのようなコードが生成されるのか、謎ですが、真似て書いても、中々、下のようなコードは生成されませんでした。(少なくとも二重ベクタループでは駄目でした。)
C++のvectorを使った二重ループの速度比較 - Qiita
00007FF6340034A0 movsxd rax,dword ptr [r11+rcx*4] 00007FF6340034A4 movsd xmm0,mmword ptr [r9+rax*8] 00007FF6340034AA mulsd xmm0,mmword ptr [r10+rcx*8] 00007FF6340034B0 inc rcx 00007FF6340034B3 addsd xmm1,xmm0 00007FF6340034B7 cmp rcx,rdx 00007FF634003ま
また、マルチスレッド化は、OpenMPを用いた記述になっており、推定要素数に応じてマルチスレッドが駆動されます。実際、今回のインスタンスについては、ほぼ、スレッド数倍速になっており、既に大変よく効いていることが分かりました。
static void run(const SparseLhsType& lhs, const DenseRhsType& rhs, DenseResType& res, const typename Res::Scalar& alpha) { LhsEval lhsEval(lhs); Index n = lhs.outerSize(); #ifdef EIGEN_HAS_OPENMP Eigen::initParallel(); Index threads = Eigen::nbThreads(); #endif for(Index c=0; c1 && lhsEval.nonZerosEstimate() > 20000) { #pragma omp parallel for schedule(dynamic,(n+threads*4-1)/(threads*4)) num_threads(threads) for(Index i=0; i 疎行列とベクタの積は、SpMVというみたいです。いくつか論文がヒットし、実装を見てみました。
動的な並列実行機構を用いたSpMV実装の性能評価 | 文献情報 | J-GLOBAL 科学技術総合リンクセンター (jst.go.jp)
疎行列の問題は、疎であるが故に、メモリからのロードがネックになり最近のCPUの演算能力を生かせないことにあります。上記のような試みはなされていますが、本質的には、解決が難しいということです。Eigenのコードにしても、残る改善の余地は、ループアンローリングしかありません。しかし、これをC++で記述しても、Eigen程の精緻なコードを生成しないので、アセンブラで記述するしかないでしょう。
アンローリングは、行うことにしても必要な性能(IPMの3倍速)は、得られそうにありません。そこで、さらなる改善としてNSPに特化した行列演算を試みることにしました。SIMDの命令セットを見たのですが、残念ながら効力があるだろうと思われるのは、AVXという命令セット以降です。最近のCPUならば、備えているとは思いますが、以降の検討は、AVXにすることにしました。(巨大インスタンスについては、それ以外に立ち向かう方法を思いつきません。)
記述は、アセンブラで行いますが、VisualStudioのアセンブラの使い方が良く分からず、JITアセンブラXbyakを使います。試してみたのですが、良い感じで使えそうでした。
また、現在の開発環境は古いCPUでAVXの評価が出来ないので、中古サーバを手配し開発環境を更新します。
にても、大規模NSP問題を解決するために、FirstOrderSolverを導入、FirstOrderSolverの問題を解決するために、疎行列演算問題を解く羽目に。疎行列問題を解決するために、SIMD
に手を出すことになるとは思ってもみませんでした。
0 件のコメント:
コメントを投稿