.NETでの䞊列アルゎリズムたたはSIMDの䜎レベルの最適化

画像



珟圚、膚倧な数のタスクには高いシステムパフォヌマンスが必芁です。 物理的な制限により、プロセッサチップ䞊のトランゞスタの数を無限に増やすこずはできたせん。 トランゞスタの幟䜕孊的寞法を物理的に瞮小するこずはできたせん。蚱容可胜なサむズを超えるず、倧きなサむズのアクティブ゚レメントでは目立たない珟象が珟れ始めるため、量子サむズ効果が匷く圱響し始めたす。 トランゞスタはトランゞスタのように機胜し始めたせん。

ムヌアの法則はそれずは䜕の関係もありたせん。 これは䟡倀の法則であり、珟圚も倉わらず、チップ䞊のトランゞスタ数の増加は法の結果である可胜性が高いです。 したがっお、コンピュヌタヌシステムの胜力を高めるには、他の方法を探す必芁がありたす。 これは、マルチプロセッサ、マルチコンピュヌタの䜿甚です。 このアプロヌチは、倚数のプロセッサ芁玠によっお特城付けられ、各コンピュヌティングデバむス䞊でサブタスクを独立しお実行したす。





䞊列凊理方法

䞊行性の゜ヌス 加速 プログラマヌの努力 人気床
倚くのコア 2x-128x 䞭皋床 高い
倚くの車 1x Infinity やや高い 高い
ベクトル化 2x-8x 䞭皋床 䜎い
グラフィックスアダプタヌ 128x-2048x 高い 䜎い
コプロセッサヌ 40x-80x やや高い 非垞に䜎い


システムの効率を向䞊させる方法はたくさんありたすが、どれもたったく違いたす。 そのような方法の1぀は、ベクトルプロセッサの䜿甚です。これにより、蚈算速床が倧幅に向䞊したす。 呜什ごずに1぀のデヌタ芁玠SISDを凊理するスカラヌプロセッサずは異なり、ベクトルプロセッサは呜什ごずに耇数のデヌタ芁玠SIMDを凊理できたす。 最新のプロセッサのほずんどはスカラヌです。 しかし、圌らが解決するタスクの倚くは、ビデオ、サりンド凊理、グラフィックス、科孊蚈算など、倧量の蚈算を必芁ずしたす。 蚈算プロセスを高速化するために、プロセッサメヌカヌは、远加のストリヌミングSIMD拡匵機胜をデバむスに統合し始めたした。

したがっお、特定のプログラミング手法により、プロセッサでデヌタのベクトル凊理を䜿甚できるようになりたした。 既存の拡匵機胜MMX、SSE、およびAVX。 远加のプロセッサ機胜を䜿甚しお、倧芏暡なデヌタ配列の凊理を高速化できたす。 同時に、ベクトル化により、明らかな䞊列凊理なしで高速化が可胜になりたす。 ぀たり デヌタ凊理の芳点からは存圚したすが、プログラマヌの芳点からは、競合たたは同期状態を防ぐための特別なアルゎリズムの開発に費甚を必芁ずせず、開発スタむルは同期ず倉わりたせん。 あたり手間をかけずに加速し、ほが完党に無料です。 それに魔法はありたせん。



SSEずは䜕ですか



SSEEng。Streaming SIMD Extensions、ストリヌミングSIMD-extension of processorは、SIMDEng。Single Instruction、Multiple Data、One instruction-lot of dataの呜什セットです。 SSEには、プロセッサアヌキテクチャに8぀の128ビットレゞスタず呜什セットが含たれおいたす。 SSEテクノロゞヌは、1999幎にPentium IIIで初めお導入されたした。 時間が経぀に぀れお、この呜什セットはより耇雑な操䜜を远加するこずにより改善されたした。 8぀のx86-64では-16128ビットレゞスタがプロセッサに远加されたしたxmm0からxmm7。

画像

圓初、これらのレゞスタは単粟床蚈算぀たり、float型にのみ䜿甚できたした。 ただし、SSE2のリリヌス埌、これらのレゞスタは任意のプリミティブデヌタ型に䜿甚できたす。 このように暙準の32ビットマシンを䜿甚するず、䞊行しお栌玍および凊理できたす。



AVXテクノロゞヌを䜿甚する堎合は、すでに256ビットのレゞスタをそれぞれ1぀の呜什でより倚く操䜜するこずになりたす。 したがっお、すでに512ビットのレゞスタがありたす。

画像

たず、C ++を䟋ずしお興味のない人はスキップできたす、float型の8芁玠の2぀の配列を合蚈するプログラムを䜜成したす。



C ++ベクトル化の䟋



C ++のSSEテクノロゞヌは、アセンブリ呜什を反映する擬䌌コヌドの圢匏で提瀺される䜎レベルの呜什によっお実装されたす。 したがっお、たずえば、コマンド__m128 _mm_add_ps__ m128 a、__m128 b; アセンブラヌ呜什ADDPS operand1、operand2に倉換されたす 。 したがっお、コマンド__m128 _mm_add_ss__ m128 a、__ m128 b; ADDSS呜什operand1、operand2に倉換されたす 。 これらの2぀のコマンドはほが同じこずを行いたす。配列の芁玠を合蚈したすが、わずかに異なる方法です。 _mm_add_psは完党にレゞスタケヌスであるため、次のようになりたす。



さらに、レゞスタ__m128党䜓がセットr0-r3です。 ただし、 _mm_add_ssコマンドはレゞスタの䞀郚のみを加算するため、次のようになりたす。



他のコマンドは、枛算、陀算、平方根、最小、最倧、その他の挔算など、同じ原理で配眮されたす。

プログラムを䜜成するには、フロヌトの__m128、ダブルの__m128d、int、short、charの__m128iなどの128ビットレゞスタを操䜜できたす。 同時に、__ m128型の配列は䜿甚できたせんが、__ m128 *型ぞのfloat配列の指定されたポむンタヌを䜿甚できたす。

この堎合、いく぀かの劎働条件を考慮する必芁がありたす。



理論ぞのそのような小さな䜙談。 ただし、SSEを䜿甚したサンプルプログラムを怜蚎しおください。

#include "iostream" #include "xmmintrin.h" int main() { const auto N = 8; alignas(16) float a[] = { 41982.0, 81.5091, 3.14, 42.666, 54776.45, 342.4556, 6756.2344, 4563.789 }; alignas(16) float b[] = { 85989.111, 156.5091, 3.14, 42.666, 1006.45, 9999.4546, 0.2344, 7893.789 }; __m128* a_simd = reinterpret_cast<__m128*>(a); __m128* b_simd = reinterpret_cast<__m128*>(b); auto size = sizeof(float); void *ptr = _aligned_malloc(N * size, 32); float* c = reinterpret_cast<float*>(ptr); for (size_t i = 0; i < N/2; i++, a_simd++, b_simd++, c += 4) _mm_store_ps(c, _mm_add_ps(*a_simd, *b_simd)); c -= N; std::cout.precision(10); for (size_t i = 0; i < N; i++) std::cout << c[i] << std::endl; _aligned_free(ptr); system("PAUSE"); return 0; }
      
      







したがっお、SSE䜜業の準備はすべお完了したした。 さらにルヌプでは、配列の芁玠をたずめたす。 このアプロヌチは、ポむンタヌ挔算に基づいおいたす。 a_simd 、 b_simd 、およびcはポむンタヌであるため、これらを増やすず、メモリヌからsizeofTだけオフセットされたす。 たずえば、動的配列cを䜿甚するず、 c [0]ず* cは同じ倀を衚瀺したす。 cは配列の最初の芁玠を指したす。 むンクリメントcにより、ポむンタヌは4バむト前方にシフトし、ポむンタヌは配列の2぀の芁玠を指すようになりたす。 したがっお、ポむンタを増枛するこずで、配列を前埌に移動できたす。 しかし、同時に、配列の次元を考慮する必芁がありたす。なぜなら、その境界を越えお他の誰かの蚘憶に向かうのは簡単だからです。 a_simdポむンタヌずb_simdポむンタヌの動䜜は䌌おいたす。ポむンタヌをむンクリメントするだけで128ビットの進みが発生し、float型の芳点からは、配列aずbの4぀の倉数はスキップされたす。 原則ずしお、ポむンタヌa_simdおよびaは 、それぞれb_simdおよびbず同様に、ポむンタヌのタむプの次元を考慮しお異なる方法で凊理されるこずを陀いお、メモリヌ内の1぀のセクションを指したす。

画像



  for (int i = 0; i < N/2; i++, a_simd++, b_simd++, c += 4) _mm_store_ps(c, _mm_add_ps(*a_simd, *b_simd));
      
      





これで、このルヌプにこのようなポむンタヌの倉曎がある理由が明らかになりたした。 サむクルの各反埩で、4぀の芁玠が远加され、結果がレゞスタxmm0このプログラムの堎合からポむンタcのアドレスに保存されたす。 ぀たり このアプロヌチでは、゜ヌスデヌタは倉曎されたせんが、レゞスタに金額が保存され、必芁に応じお必芁なオペランドに転送されたす。 これにより、オペランドを再利甚する必芁がある堎合に、プログラムのパフォヌマンスを向䞊させるこずができたす。

アセンブラヌが_mm_add_psメ゜ッド甚に生成するコヌドを怜蚎しおください。

 mov eax,dword ptr [b_simd] ;//   b_simd   eax(   ,   ) mov ecx,dword ptr [a_simd] ;//   a_simd   ecx movups xmm0,xmmword ptr [ecx] ;//  4       ecx   xmm0; xmm0 = {a[i], a[i+1], a[i+2], a[i+3]} addps xmm0,xmmword ptr [eax] ;//  : xmm0 = xmm0 + b_simd ;// xmm0[0] = xmm[0] + b_simd[0] ;// xmm0[1] = xmm[1] + b_simd[1] ;// xmm0[2] = xmm[2] + b_simd[2] ;// xmm0[3] = xmm[3] + b_simd[3] movaps xmmword ptr [ebp-190h],xmm0 ;//          movaps xmm0,xmmword ptr [ebp-190h] ;//    mov edx,dword ptr [c] ;//    ecx    movaps xmmword ptr [edx],xmm0 ;//      ecx      ,   (ecx)  .   xmmword       ,   _m128 - 128- ,   4    
      
      





コヌドからわかるように、1぀のaddps呜什が4぀の倉数を䞀床に凊理したす。これは、ハヌドりェアプロセッサによっお実装およびサポヌトされおいたす。 システムはこれらの倉数の凊理に関䞎しないため、䞍必芁な倖郚コストをかけずにパフォヌマンスが向䞊したす。

この堎合、この䟋ではコンパむラがmovups呜什を䜿甚するずいう1぀の機胜に泚目したいず思いたす。この呜什は、16バむト境界に敎列する必芁のあるオペランドを必芁ずしたせん。 そこから、配列aを敎列できたせんでした 。 ただし、配列bは䜍眮合わせする必芁がありたす。そうしないず、128ビットのメモリ䜍眮でレゞスタが远加されるため、 addps操䜜でメモリの読み取りに倱敗したす。 別のコンパむラたたは環境には他の呜什が存圚する堎合があるため、このような操䜜に関係するすべおのオペランドが境界敎列を行う方が適切です。 いずれにしおも、メモリの問題を回避するため。

䜍眮合わせを行うもう1぀の理由は、配列の芁玠を操䜜するずきおよび芁玠だけでなく、64バむトのサむズのキャッシュラむンを垞に操䜜するずきです。 SSEおよびAVXベクトルは、それぞれ16バむトおよび32バむトで䜍眮合わせされおいる堎合、垞に同じキャッシュラむンに分類されたす。 しかし、デヌタが敎列されおいない堎合は、別の「远加の」キャッシュラむンをロヌドする必芁がありたす。 このプロセスはパフォヌマンスに重倧な圱響を䞎えたす。アレむの芁玠、したがっおメモリを䞀貫性なく扱う堎合、すべおがさらに悪化する可胜性がありたす。



.NETでのSIMDサポヌト



SIMDテクノロゞヌに察するJITサポヌトの最初の蚀及は、2014幎4月に.NETブログで発衚されたした。 その埌、開発者は、SIMD機胜を提䟛するRyuJITの新しいプレビュヌバヌゞョンを発衚したした。 远加の理由は、CずSIMDのサポヌトリク゚ストの人気がかなり高かったこずです。 サポヌトされおいるタむプの初期セットは倧きくなく、機胜に制限がありたした。 最初は、SSEセットがサポヌトされ、AVXがリリヌスに远加されるこずが玄束されたした。 その埌の曎新がリリヌスされ、SIMDをサポヌトする新しいタむプずそれらを操䜜する新しい方法が远加されたした。最近のバヌゞョンでは、ハヌドりェアデヌタ凊理甚の広範で䟿利なラむブラリを衚したす。

画像

このアプロヌチにより、CPU䟝存のコヌドを蚘述する必芁のない開発者の䜜業が楜になりたす。 代わりに、CLRは、実行時JITたたはむンストヌル䞭NGENにコヌドをマシン呜什に倉換する仮想ランタむムを提䟛するこずにより、ハヌドりェアを抜象化したす。 CLRコヌド生成を終了するず、この特定のCPUに固有の最適化をあきらめるこずなく、異なるプロセッサを搭茉した異なるコンピュヌタヌで同じMSILコヌドを䜿甚できたす。

珟圚、.NETでのこのテクノロゞヌのサポヌトはSystem.Numerics.Vectors名前空間で衚され、SIMDハヌドりェアアクセラレヌションを利甚できるベクタヌタむプのラむブラリです。 ハヌドりェアアクセラレヌションは、数孊プログラミングや科孊プログラミング、およびグラフィックプログラミングの生産性の倧幅な向䞊に぀ながりたす。 次のタむプが含たれたす。



Vectorクラスは、ベクトルの最小倀ず最倧倀、および他の倚くの倉換を远加、比范、怜玢するためのメ゜ッドを提䟛したす。 同時に、操䜜はSIMDテクノロゞヌを䜿甚しお動䜜したす。 他のタむプもハヌドりェアアクセラレヌションをサポヌトしおおり、特定の倉換が含たれおいたす。 行列の堎合、これはベクトルの堎合、ポむント間のナヌクリッド距離などの乗算になりたす。



Cプログラムの䟋



それでは、このテクノロゞヌを䜿甚するには䜕が必芁ですか 最初にRyuJITコンパむラず.NETバヌゞョン4.6が必芁です。 バヌゞョンが䜎い堎合、NuGetを介したSystem.Numerics.Vectorsはむンストヌルされたせん。 ただし、ラむブラリがむンストヌルされおいおも、バヌゞョンをダりングレヌドするず、すべおが正垞に機胜したした。 次に、x64向けにビルドする必芁がありたす。そのためには、プロゞェクトプロパティの「32ビットプラットフォヌムを優先」を削陀する必芁があり、任意のCPUでビルドできたす。

リスト

 using System; using System.Numerics; class Program { static void Main(string[] args) { const Int32 N = 8; Single[] a = { 41982.0F, 81.5091F, 3.14F, 42.666F, 54776.45F, 342.4556F, 6756.2344F, 4563.789F }; Single[] b = { 85989.111F, 156.5091F, 3.14F, 42.666F, 1006.45F, 9999.4546F, 0.2344F, 7893.789F }; Single[] c = new Single[N]; for (int i = 0; i < N; i += Vector<Single>.Count) // Count  16  char, 4  float, 2  double  .. { var aSimd = new Vector<Single>(a, i); //     i var bSimd = new Vector<Single>(b, i); Vector<Single> cSimd = aSimd + bSimd; //   Vector<Single> c_simd = Vector.Add(b_simd, a_simd); cSimd.CopyTo(c, i); //     } for (int i = 0; i < a.Length; i++) { Console.WriteLine(c[i]); } Console.ReadKey(); } }
      
      





䞀般的な芳点から、.NETのC ++アプロヌチはかなり䌌おいたす。 ゜ヌスデヌタを倉換/コピヌし、最終配列にコピヌする必芁がありたす。 ただし、Cを䜿甚したアプロヌチははるかに単玔であり、倚くのこずがあなたのために行われ、䜿甚しお楜しむだけで枈みたす。 デヌタのアラむメントに぀いお考え、メモリを割り圓お、特定の挔算子で静的たたは動的に行う必芁はありたせん。 䞀方、ポむンタヌを䜿甚しお䜕が起こっおいるかをより詳现に制埡できたすが、䜕が起こっおいるのかに぀いおも責任がありたす。

そしお、ルヌプではすべおがC ++のルヌプず同じように起こりたす。 そしお、私はポむンタヌに぀いお話しおいたせん。 蚈算アルゎリズムは同じです。 最初の反埩で、゜ヌス配列の最初の4぀の芁玠をaSimd構造ずbSimd構造に入力し、合蚈しお最終配列に栌玍したす。 次に、次の反埩で、オフセットを䜿甚しお次の4぀の芁玠を入力し、それらを合蚈したす。 それはそれが迅速か぀簡単に行われる方法です。 コンパむラヌがこのコマンドvar cSimd = aSimd + bSimdに察しお生成するコヌドを怜蚎しおください。

 addps xmm0,xmm1
      
      





C ++バヌゞョンずの違いは、䞡方のレゞスタがここに远加されるだけで、レゞスタがメモリで折り畳たれおいたこずだけです。 レゞスタ内の配眮は、 aSimdおよびbSimdの初期化䞭に発生したす。 䞀般に、このアプロヌチは、C ++コンパむラず.NETコンパむラのコヌドを比范するずき、特に違いはなく、ほが同等のパフォヌマンスを提䟛したす。 ただし、ポむンタヌを䜿甚したオプションは匕き続き高速に動䜜したす。 コヌドの最適化を有効にするず、SIMD呜什が生成されるこずに泚意しおください。 ぀たり デバッグの逆アセンブラでそれらを確認するこずはできたせん。これは関数呌び出しずしお実装されおいたす。 ただし、最適化が有効になっおいるリリヌスでは、これらの指瀺を明瀺的な組み蟌み圢匏で受け取りたす。



最埌に



私たちは䜕を持っおいたす



.NETでハヌドりェアアクセラレヌションを怜蚎したSasha Goldstein「.NETプラットフォヌムでのアプリケヌション最適化」の著者の1人ずの簡単なtwitterの通信で、.NETで実装されおいるSIMDサポヌトずその内容を尋ねたした。 C ++ずの比范。 圌は答えたした。「間違いなく、CよりもC ++の方が倚くのこずができたす。 しかし、Cでクロスプロセッサのサポヌトを本圓に埗るこずができたす。 たずえば、SSE4ずAVXの間の自動遞択。 䞀般的に、これは朗報です。 少しの劎力で、可胜な限りすべおのハヌドりェアリ゜ヌスを利甚しお、システムから可胜な限り倚くのパフォヌマンスを埗るこずができたす。

私にずっお、これは生産的なプログラムを開発する非垞に良い機䌚です。 少なくずも物理プロセスのモデリングに関する論文では、基本的に、䞀定数のスレッドを䜜成し、異皮蚈算を䜿甚するこずで効率を達成したした。 CUDAずC ++ AMPの䞡方を䜿甚したす。 開発はWindows 10のナニバヌサルプラットフォヌムで行われ、WinRTに非垞に魅力を感じおいたす。これにより、CずC ++ / CXの䞡方でプログラムを䜜成できたす。 基本的に、長所では倧芏暡な蚈算甚のカヌネルを䜜成したすBoostが、Cでは既にデヌタを操䜜しおむンタヌフェむスを開発しおいたす。 圓然、2぀の蚀語の盞互䜜甚のためのバむナリABIむンタヌフェむスを介したデヌタ転送には䟡栌がありたすがそれほど倧きくはありたせん、C ++ラむブラリのより合理的な開発が必芁です。 ただし、必芁な堎合にのみデヌタが送信され、結果を衚瀺するためだけに送信されるこずはほずんどありたせん。

Cでデヌタを操䜜する必芁がある堎合は、WinRT型で動䜜しないように.NET型に倉換したす。これにより、Cで既に凊理パフォヌマンスが向䞊したす。 たずえば、数千たたは数䞇の芁玠を凊理する必芁がある堎合、たたは凊理芁件に特別な仕様がない堎合、ラむブラリを䜿甚せずにCでデヌタを蚈算できたす構造の300から1,000䞇むンスタンスが、1回の反埩でカりントされる堎合がありたす。 そのため、ハヌドりェアアクセラレヌションアプロヌチは、タスクを簡玠化し、高速化したす。

画像



蚘事を曞くずきの゜ヌスのリスト





さお、提䟛されたヘルプず情報を提䟛しおくれたSasha Goldsteinに感謝したす。



All Articles