SPU上のSoAベクトル


8

SIMD命令を使用する場合にスループットを向上させるために、データを一般的な「構造体の配列」(AoS)ではなく「構造体の配列」(SoA)に編成することの利点についてたくさん読みました。「なぜ」は私には完全に理にかなっていますが、ベクトルのようなものを扱うときにこれをどの程度行うべきかわかりません。

ベクトル自体は、データの(固定サイズ)配列の構造体として扱うことができるため、これらの配列をX、Y、Z配列の構造体に変換できます。これにより、一度に1つではなく、4つのベクトルを同時に処理できます。

今、私はこれをGameDevに投稿している特定の理由のために:

これはSPUでベクターを操作するのに意味がありますか?より具体的には、単一のベクトルだけで複数のアレイをDMAすることは理にかなっていますか?それとも、ベクターの配列をDMA処理し、それらをさまざまなコンポーネントに展開して操作する方が良いでしょうか?

(「AoS」を実行した場合)アンロールを削除することの利点はわかりましたが、このルートを使用して複数のベクトルのセットを一度に処理していた場合、DMAチャネルがすぐに不足する可能性があります。

(注:Cellの専門的な経験はまだありませんが、しばらくの間、OtherOSをいじっています)

回答:


5

1つのアプローチは、AoSとSoAのハイブリッドであるAoSoA(read:Array of Struct of Array)アプローチを使用することです。考え方は、N構造体相当のデータを連続したチャンクにSoA形式で格納し、次に次のN構造体相当のデータをSoA形式で格納することです。

4つの構造体の粒度でスウィズルされた16個のベクトル(ラベル0、1、2 ... F)のAoSフォームは次のとおりです。

000111222333444555666777888999AAABBBCCCDDDEEEFFF
XYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZ

SoAの場合、これは次のとおりです。

0123456789ABCDEF
XXXXXXXXXXXXXXXX

0123456789ABCDEF
YYYYYYYYYYYYYYYY

0123456789ABCDEF
ZZZZZZZZZZZZZZZZ

AoSoAの場合、これは次のようになります。

01230123012345674567456789AB89AB89ABCDEFCDEFCDEF
XXXXYYYYZZZZXXXXYYYYZZZZXXXXYYYYZZZZXXXXYYYYZZZZ

AoSoAアプローチには、AoSの以下の利点があります。

  • 構造体のチャンクをSPUローカルメモリに転送するには、DMA転送が1つだけ必要です。
  • 構造体には、すべてのデータがキャッシュラインに収まる可能性があります。
  • ブロックのプリフェッチは非常に簡単です。

AoSoAアプローチには、SoAフォームのこれらの利点もあります。

  • データをスウィズルすることなく、SPUローカルメモリから128ビットのベクトルレジスタに直接データをロードできます。
  • 一度に4つの構造体を操作できます。
  • 基本的な分岐がない場合(つまり、ベクトル演算に未使用のレーンがない場合)は、ベクトルプロセッサのSIMDを十分に活用できます。

AoSoAアプローチには、SoAフォームのこれらの欠点のいくつかがまだあります。

  • オブジェクト管理は、スウィズリング粒度で行う必要があります。
  • 完全な構造体のランダムアクセス書き込みは、分散したメモリにアクセスする必要があります。
  • (これらは、構造体の構成/管理方法とその寿命に応じて、問題にならない場合があります)

ところで、これらのAoSoAの概念は、SSE / AVX / LRBniや、非常に広いSIMDプロセッサに例えられるGPUに非常によく適用されます。ベンダー/アーキテクチャに応じて32/48/64幅。


実際にフロートとして使用する非ベクターデータをパックしない限り、コンポーネントごとにパックしないよりも、これがどのように利点をもたらすかはわかりません。その場合は勝利があると思います。また、SPUにはメインメモリとの通信以外にキャッシュラインがないことに注意してください。
Kaj

2
1.すべてのものと同様に、走行距離は正確なデータ/アルゴリズム/プロセッサーによって異なります。レジスターが制約されている場合、すべてのXフィールドを同じレジスターにシャッフルする前に、4つの一時レジスターの必要性を回避すると便利です。しかし、再び、YMMV。2.私の答えは、概念がデータ並列プログラミングの分野内で十分に伝達されるため、より一般的でした。キャッシュラインの考慮事項はGPU / SSEにより適切ですが、すべて同じように言及する必要があると感じました:)
jpaver

1
十分に公正で、私は悟りを開いて立っており、より微妙に批評することを学びます!あなたの洞察を共有してくれてありがとう:o)
Kaj

3

コードのベクトル化に関しては、SPUは実際には興味深い特殊なケースです。命令は「算術」ファミリと「ロード/ストア」ファミリに分けられ、2つのファミリは別々のパイプラインで実行されます。SPUは、サイクルごとに各タイプを1つ発行できます。

数学コードは明らかに数学命令に強く拘束されているため、通常、SPUのMathyループには、ロード/ストアパイプに多数のオープンサイクルが存在します。ロード/ストアパイプでシャッフルが発生するため、オーバーヘッドなしでxyzxyzxyzxyzフォームをxxxxyyyyzzzzフォームにスウィズルするのに十分な空きロード/ストア命令があることがよくあります。

この手法は少なくともNaughty Dogで使用されています。詳細については、SPUアセンブリのプレゼンテーション(パート1およびパート2)を参照してください。

残念ながら、コンパイラーは多くの場合、これを自動的に実行するほどスマートではありません。この方法を選択する場合は、自分でアセンブリを作成するか、組み込み関数を使用してループを展開し、アセンブラーをチェックして必要なものであることを確認する必要があります。したがって、SPUで正常に実行される一般的なクロスプラットフォームコードを記述したい場合は、SoAまたはAoSoAを使用することをお勧めします(jpaverが示唆しています)。


ああ、結局は同意します:o)SPUを必要に応じてスウィズルします。
Kaj

1

他の最適化と同様に、プロファイルを作成してください。可読性が最初であり、プロファイリングが特定のボトルネックを特定し、高レベルのアルゴリズムを調整するためのすべてのオプションを使い果たした場合にのみ犠牲にする必要があります(作業を行うための最も速い方法は、作業を行う必要がないことです!)常に再プロファイルする必要があります低レベルの最適化に従って、特にセルのように風変わりなパイプラインを使用して、逆ではなく実際に高速化したことを確認します。

次に使用する手法は、ボトルネックの詳細によって異なります。一般に、ベクタータイプを操作する場合、結果で無視するベクターコンポーネントは無駄な作業を表します。SoA / AoSの切り替えは、そのような未使用のコンポーネントを埋めることによってより有用な作業を実行できない限り意味がありません(たとえば、PS3のPPUの1つのドット積と同じ時間内の4つのドット積の並列)。あなたの質問に対処するために、単一のベクトルに対して1つの操作を実行するためだけにコンポーネントをシャッフルする時間を費やすことは、私にとって悲観化のように聞こえます!

SPUの反対面は、小規模なDMA転送のコストの大部分がセットアップされていることです。128バイト未満のものは、転送に同じ数のサイクルがかかり、約1キロバイト未満のものは、数サイクル以上かかります。したがって、必要以上に多くのデータをDMA転送することについて心配する必要はありません。トリガーされるシーケンシャルDMA転送の数を減らし、DMA転送の発生中に作業を実行し、ループのプロローグとエピローグを展開してソフトウェアパイプラインを形成することは、SPUのパフォーマンスを向上させる鍵であり、余分なデータをフェッチすることでコーナーケースを処理するのが最も簡単です。 /部分的に計算された結果を破棄して、フープをジャンプして、読み取って処理する必要がある正確な量のデータを整理しようとします。


AOSAOのアプローチに従ってそれらを解凍する場合は、少なくとも一度に複数のベクターを一度に取り込みます。また、バッチでプルし、処理中に次のバッチでプルすることもできます。最初のバッチを送信するときに、2番目のバッチを処理し、3番目のバッチをプルします。そうすることで、可能な限り多くの待ち時間を隠すことができます。
Kaj

0

いいえ、ほとんどのベクトルオペコードはベクトル全体で動作し、個別のコンポーネントでは動作しないため、一般的にはそれほど意味がありません。したがって、すでに1つの命令でベクトルを乗算できますが、別々のコンポーネントを分割すると、4つの命令を費やすことになります。したがって、基本的には構造体の一部で一般的に多くの操作を行うので、配列にパックする方が優れていますが、ベクトルの1つのコンポーネントのみで処理を実行することはほとんどありません。アウトは機能しません。
もちろん、ベクトルの(たとえば)xコンポーネントのみに何かをしなければならない状況が見つかった場合、それは機能するかもしれませんが、実際のベクトルが必要なときにすべてを元に戻すというペナルティは安くないので、最初にベクトルを使用するべきではないのかと思いますが、たまたまベクトルオプコードが特定の計算を実行できるようにする浮動小数点の配列だけです。


2
ベクトル演算のSoAのポイントがありません。作業しているオブジェクトが1つしかないことはめったにありません。実際には、配列を反復して、同じことを多くのオブジェクトに対して実行しています。4つのドット積を行うことを検討してください。ベクトルをAyzとしてxyz0形式で格納する場合、2つのベクトルのドットを取得するには、multiply-shuffle-add-shuffle-add-5命令が必要です。4ドット製品を実行するには、20の手順が必要です。一方、SoA形式(xxxx、yyyy、zzzz、xxxx、yyyy、zzzz)で8つのベクトルを保存している場合、3つの命令(mul、madd、madd)だけで4つのドット積を実行できます。これは6倍以上高速です。
チャーリー

フェアポイント。ただし、2つの観察。私は常にWを維持するので、20の命令は必要ありません。次に、残りのオーバーヘッドのほとんどは、他の命令のレイテンシに隠されます。タイトなループは、深刻なパイプラインの停止に苦しむでしょう。6回作ることは理論的な最適化です。ですから、そうですが、操作をバッチ処理したいのですが、前述のデータに対して他に何もせずに、ドット積の迅速なバッチ処理を実行する必要があることはほとんどありません。PPU側での泡抜き/散布のコストは、私にとってはあまりにも多くの犠牲になります。
Kaj

うめき声、私は修正された立場です-SPUでは、単純に行われると20が必要になります(ただし、その場でシャッフルします)。それは私がそれを最適にするためにたくさんのスウィズルをやった結果の一つです。360には素晴らしいドットが組み込まれています(ただし、すばらしいビット操作はありません)。
Kaj

ええ、今考えてみると、 "4ドット積"を実行しようとする場合は、後の加算のいくつかを組み合わせることができるため、20命令よりも優れた命令を実行できます。しかし、ベクトルをxxxx、yyyy、zzzzとしてレジスタに登録すると、スウィズルしたか、SoAとして保存したかに関係なく、これらのシャッフルが完全に取り除かれます。とにかく、SoAがブランチロジックコードを遅くするのは正しいことですが、そのような多くの場合の解決策は、データをバケット化し、ブランチロジックを素敵なフラットループにリファクタリングすることです。
チャーリー

同意した。古いSPUコード(以前の会社ではできません)を上書きする場合、特に気づかずに最適化のためにxxxxyyyyzzzz形式に移動した場合があると確信しています。PPUからその形式で提供したことはありません。注意してください、OPは、dma-ing x、y、zを別々に考えています。それは間違いなく私にはうまくいきません。xxxxyyyyzzzz形式ですべてがうまく機能するわけではないので、私も(私がしたように)ローカルでスウィズルしたいと思います。お奨めあなたの戦いは私が推測します。SPUの最適化は非常に効果的であり、タイトなソリューションを手に入れるとひどく賢くなります:o)
Kaj
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.