コンピューティングにおいて、ベクトルプロセッサは、ベクトルと呼ばれる 大規模な一次元データ配列に対して効率的かつアーキテクチャ的に順次動作するように設計された命令セットを実装する中央処理装置(CPU)です。ハードウェアコンポーネントとして統合されている場合、ベクトルプロセッサはベクトル処理ユニット(VPU)と呼ばれることがよくあります。これは、命令が単一のデータ項目のみを操作するスカラープロセッサとは対照的であり、また、追加の単一命令複数データ(SIMD)またはレジスタ内SIMD (SWAR)演算ユニットを持つスカラープロセッサとは対照的です。ベクトルプロセッサは、数値シミュレーション、圧縮、および同様のタスクなど、特定のワークロードのパフォーマンスを大幅に向上させることができます。 [ 1 ]
ベクトル処理技術は、ビデオゲーム機のハードウェアやグラフィックスアクセラレータでも使用されていますが、これらは例外なく単一命令複数スレッド(SIMT)であり、場合によっては単一命令複数データ(SIMD)です。
ベクトルマシンは1970年代初頭に登場し、1970年代から1990年代にかけてスーパーコンピュータの設計を席巻しました。特に、様々なCrayプラットフォームがその代表例です。しかし、従来のマイクロプロセッサ設計における価格性能比の急速な低下により、1990年代にはベクトルスーパーコンピュータの需要は減少しました。
アレイ処理の開発は、1960年代初頭にウェスティングハウス・エレクトリック社がソロモン・プロジェクトで開始しました。ソロモンの目標は、単一のマスター中央処理装置(CPU)の制御下にある多数の単純なコプロセッサを使用することで、演算性能を劇的に向上させることでした。CPUは、サイクルごとに1つの算術論理演算ユニット(ALU)に共通の命令を1つずつ供給しましたが、各ALUが処理するデータポイントはそれぞれ異なっていました。これにより、ソロモン・マシンは、配列の形式で入力された大規模なデータセットに単一のアルゴリズムを適用することが可能になり、フリンの分類法においてアレイプロセッサの例として挙げられるようになりました。
1962年、ウェスティングハウスはこのプロジェクトを中止したが、イリノイ大学アーバナ・シャンペーン校がILLIAC IVとしてこの取り組みを再開した。当初の設計では256個のALUを備えた1 GFLOPSのマシンを想定していたが、1972年に最終的に納入された時にはALUはわずか64個で、性能は100~150 MFLOPSにとどまった。それでも、基本概念が健全であることを示し、計算流体力学などのデータ集約型アプリケーションで使用した場合、ILLIACは世界最速のマシンとなった。データ要素ごとに個別のALUを使用するILLIACのアプローチは、後の設計では一般的ではなく、大規模並列コンピューティングの別のカテゴリで言及されることが多い。1972年頃、フリンはこのタイプの処理を、単一命令複数スレッド(SIMT)の初期形態として分類した。
International Computers Limited は、独自の分散アレイプロセッサ(DAP) 設計により ILLIAC コンセプトの多くの困難を回避しようとし、ILLIAC と DAP をセルラーアレイプロセッサに分類し、CDC STAR-100 や Cray 1 などの従来のベクトルプロセッサ設計よりも大幅な性能上の利点を提供する可能性があるとした。[ 2 ]
関数演算用のコンピュータは、 1967年にカルツェフによって発表され開発された。[ 3 ]
最初のベクトル型スーパーコンピュータは、コントロール・データ・コーポレーションのSTAR-100とテキサス・インスツルメンツのアドバンスト・サイエンティフィック・コンピュータ(ASC)であり、それぞれ1974年と1972年に発表された。
基本的なASC(すなわち「1パイプ」)ALUは、スカラー演算とベクトル演算の両方をサポートするパイプラインアーキテクチャを採用しており、ピーク性能は約20MFLOPSに達し、長いベクトルを処理する際に容易に達成できた。拡張ALU構成では、「2パイプ」または「4パイプ」をサポートし、それぞれ2倍または4倍の性能向上を実現した。メモリ帯域幅は、これらの拡張モードをサポートするのに十分であった。
STAR-100は、CDCが所有するCDC 7600などのスーパーコンピュータに比べて処理速度は劣っていたものの、データ関連のタスクにおいては、はるかに小型で安価でありながら、同等の処理能力を発揮できた。しかし、このマシンはベクトル命令のデコードと処理実行の準備にかなりの時間を要したため、実際に処理速度を向上させるには、非常に特殊なデータセットを扱う必要があった。
ベクトル演算技術は、1976年に有名なCray-1によって初めて本格的に活用されました。STAR-100やASCのようにデータをメモリに保持するのではなく、Crayの設計では8つのベクトルレジスタが備えられており、それぞれ64ビットワードを64個格納できました。ベクトル命令はレジスタ間で実行され、メインメモリにアクセスするよりもはるかに高速でした。STAR-100はメモリ内の長いベクトルに対して1つの演算を実行してから次の演算に進むのに対し、Crayの設計ではベクトルのより小さな部分をレジスタにロードし、そのデータに対して可能な限り多くの演算を実行することで、処理速度の遅いメモリアクセス操作を大幅に削減しました。
Crayの設計では、複数のALUではなくパイプライン並列処理を用いてベクトル命令を実装しました。さらに、この設計では、加算/減算と乗算など、異なる命令に対して完全に独立したパイプラインが使用されていました。これにより、ベクトル命令のバッチを各ALUサブユニットにパイプラインで送ることが可能になり、この手法はベクトルチェイニングと呼ばれました。Cray-1の通常の性能は約80 MFLOPSでしたが、最大3つのチェーンを実行すると、ピーク時には240 MFLOPS、平均では約150 MFLOPSに達し、当時のどのマシンよりもはるかに高速でした。

他の例も続いた。コントロール データ コーポレーションは、ETA-10マシンでハイエンド市場に再参入しようとしたが、売れ行きが悪く、これを機にスーパー コンピューティング分野から完全に撤退した。1980 年代前半から中頃にかけて、日本の富士通、日立、NEC は、Cray-1 に似たレジスタ ベースのベクトル マシンを発表した。これは通常、わずかに高速で、はるかに小型だった。富士通 VPシリーズは、1990~1991 年に世界最速のスーパー コンピュータの記録を保持したVP2600で頂点に達した。オレゴン州に拠点を置くFloating Point Systems (FPS) は、ミニ コンピュータ用のアドオン アレイ プロセッサを製造し、後に独自のミニ スーパーコンピュータを製造した。
クレイは、 Cray-2、Cray X-MP、Cray Y-MPといった一連のマシンで常に競合他社を凌駕し、性能面で常にトップの座を維持しました。それ以来、スーパーコンピュータ市場は、ベクトルプロセッサのより優れた実装よりも、大規模並列処理に重点を置いてきました。しかし、ベクトル処理の利点を認識したIBMは、複数のスカラープロセッサをベクトルプロセッサとして結合する仮想ベクトルアーキテクチャをスーパーコンピュータ向けに開発しました。IBMはまた、オプションのベクトル機能を備えたIBM 3090にクレイ式のベクトル処理を実装しました。
Cray-1のようなベクトル型スーパーコンピュータは近年人気が薄れていますが、NECはSXシリーズという形で現在に至るまでこの種のコンピュータを製造し続けています。最新のSX-Aurora TSUBASAは、プロセッサと24ギガバイトまたは48ギガバイトのメモリをHBM 2モジュール上に搭載し、グラフィックコプロセッサに似た形状のカードに収めています。しかし、コプロセッサとして機能するのではなく、メインコンピュータとして動作し、接続先のPC互換コンピュータがサポート機能を提供するという仕組みになっています。
現代のグラフィックス処理ユニット(GPU )は、演算カーネルによって駆動される可能性のあるシェーダーパイプラインのアレイを含み、通常はSIMTであるため、ベクトルプロセッサと誤って分類されることがよくあります。この2つは、Flynnの1972年のSIMD分類法におけるSIMDの非常に類似した分類です。GPUは、メモリウォールの極端な形態に遭遇すると、メモリレイテンシを隠す戦略を使用します。Flynnの1972年の論文で示されているように、SIMTベースのGPUの重要な特徴は、単一の命令デコーダ/ブロードキャスターを備えていることですが、同じ命令を受信して実行するコアはそれ以外は比較的普通です。つまり、独自のALU、独自のレジスタファイル、独自のロード/ストアユニット、独自の独立したL1データキャッシュを備えています。したがって、すべてのコアが互いに同期してまったく同じ命令を同時に実行しますが、まったく異なるメモリ位置からのまったく異なるデータを使用して実行します。これは、並列パイプライン演算の実行のみに厳密に限定される「パックドSIMD」よりもはるかに複雑で入り組んでいます。今日の商用GPUの正確な内部詳細は企業秘密ですが、MIAOW [ 4 ]チームは、AMDGPUアーキテクチャのサブセットを実装するのに十分な逸話的な情報を集めることができました。[ 5 ]
いくつかの最新のCPUアーキテクチャはベクトルプロセッサとして設計されています。RISC -Vベクトル拡張は初期のベクトルプロセッサと同様の原理に従っており、 Andes Technology AX45MPVなどの商用製品に実装されています。[ 6 ]また、ForwardComやLibre-SOCなど、いくつかのオープンソースのベクトルプロセッサアーキテクチャも開発されています。
2016年現在ほとんどの汎用CPUは、固定長SIMD命令を特徴とするアーキテクチャを実装しています。一見すると、これらは複数の(ベクトル化された、明示的な長さの)データセットを操作し、ベクトルプロセッサの機能を取り入れているため、ベクトル処理の一種とみなすことができます。しかし、定義上、SIMDを追加しただけでは、プロセッサを実際のベクトルプロセッサとして認定することはできません。なぜなら、SIMDは固定長であり、ベクトルは可変長だからです。この違いは、純粋なSIMD、述語付きSIMD、純粋なベクトル処理という3つのカテゴリを示し、比較する例を用いて以下に示します。
他のCPU設計には、複数の(ベクトル化された)データセットに対するベクトル処理のための複数の命令が含まれており、これは一般的にMIMD (複数命令、複数データ)として知られ、 VLIW(非常に長い命令語)とEPIC(明示的並列命令コンピューティング)によって実現されます。たとえば、富士通FR-Vは、VLIW機能と述語およびパックドSIMDを組み合わせています。[ 8 ]
SIMD命令セットは、ベクトル命令セットと比較すると、重要な機能が欠けている。最も重要なのは、ベクトルプロセッサは、その誕生以来、定義と設計上、常に可変長ベクトルを扱うように設計されており、2のべき乗に限定されないということである。
純粋な(固定幅、述語なし)SIMDは、しばしば誤って「ベクトル」であると主張されます(SIMDが処理するデータはたまたまベクトルであるため)。歴史的および現代のISAを詳細に分析および比較すると、実際のベクトルISAには、実行時にベクトルの長さを2のべき乗ではない値に設定する方法があることがわかります。言い換えれば、要素数はSIMD ISAのように命令にハードエンコードされていません。これは、「SIMDは有害である」という記事で説明されています。[ 9 ]
VL命令、[ 10 ]vsetvlRISC-V RVV の命令、[ 11 ]lvlNEC SX の指示、[ 12 ]VLVCU[ 13 ]と名付けられていました。もう一つの重要な機能は、要素レベルの述語付きマスキングです。Cyber 200 モデルでは、要素レベルのビットマスク自体がメモリに格納されたベクトルでした。一部のベクトルプロセッサは、Cyber 200 と同様に、要素ごとに 1 ビットをマスクとして効率的に使用する Cray-1 などのベクトルマスクレジスタを備えています。しかし、RISC-V などの他のベクトル ISA は、メインのベクトルレジスタ ファイルからのレジスタを使用することを選択しました。[ 15 ]
述語付き SIMD ( Flynn の分類法の一部) は、ARM SVE2 [ 16 ]およびAVX-512で現在利用可能な、各要素に対する包括的な個々の要素レベルの述語マスクですが、これらの命令セットのどちらにも、明示的な「ベクトルの長さを設定」命令はありません。述語付き SIMD は固定幅 SIMD ALU を使用しますが、ユニットのローカル制御 (述語付き) によるアクティベーションを許可することで、可変長ベクトルの外観を実現します。以下の例は、これらのカテゴリの区別を説明するのに役立ちます。
もう一つの重要な違いは、SIMDは固定幅のバッチ処理を使用するため、設計上、反復処理や削減処理に対応できない点です。この点については、以下の例でさらに詳しく説明します。

チェイニングは、ベクトル演算の部分的な出力を、最初の演算が完了するのを待たずに後続の演算の入力として使用できるパフォーマンス技術です。[ 17 ]この技術は、 1976年にシーモア・クレイが80MHz のCray 1スーパーコンピュータで初めて使用しました。 [ 6 ]
コンピュータは、あらかじめ定義された意味を持つマシンコード(命令セット)に従うことで、一度に 1 個または複数のデータを操作できます。たとえば、加算命令は、本質的に「A に B を加えて、その結果を C に格納する」ことを実行します。A、B、C のデータは、少なくとも理論的には、命令に直接エンコードできます。しかし、効率的な実装では、物事はめったにそれほど単純ではありません。データは生の形式で送信されることはほとんどなく、代わりに、データが格納されているメモリ位置のアドレスを渡すことで「指し示されます」。このアドレスをデコードしてメモリからデータを取り出すには時間がかかり、その間、CPU は従来、要求されたデータが表示されるのを待ってアイドル状態になります。CPU の速度が向上するにつれて、このメモリの遅延は、歴史的にパフォーマンスの大きな障害となってきました。ランダムアクセスメモリ § メモリウォールを参照してください。
これらのステップにかかる時間を短縮するために、最新のCPUのほとんどは、命令パイプラインと呼ばれる技術を使用しています。この技術では、命令が複数のサブユニットを順番に通過します。最初のサブユニットはアドレスを読み取ってデコードし、次のサブユニットはそのアドレスにある値を「フェッチ」し、さらに次のサブユニットが演算を実行します。パイプライン処理の「コツ」は、アセンブリラインのように、最初の命令がCPUから出る前に次の命令のデコードを開始することです。これにより、アドレスデコーダは常に使用されます。個々の命令の完了にかかる時間(レイテンシと呼ばれる時間)は同じですが、CPUは、一度に1つずつ処理する場合よりもはるかに高速かつ効率的に、複数の操作をオーバーラップさせてバッチ処理することができます。
ベクトルプロセッサは、この概念をさらに一歩進めています。命令だけでなく、データ自体もパイプライン処理します。プロセッサには、単にAにBを加算するだけでなく、「ここからここまで」のすべての数値を「そこからあそこまで」のすべての数値に加算するという命令が与えられます。命令をデコードして必要なデータを取得する作業を繰り返す代わりに、プロセッサはメモリから単一の命令を読み込み、その命令の定義自体に、前回よりも1つ大きいアドレスにある別のデータ項目に対して再度演算を行うことが暗黙的に含まれています。これにより、命令の効率が大幅に向上します。
これがどれほど大きな違いを生むかを説明するために、10個の数字からなる2つのグループを足し合わせるという単純なタスクを考えてみましょう。通常のプログラミング言語では、各数字のペアを順番に取得して足し合わせる「ループ」を作成します。CPUにとっては、これは次のようなものになります。
; 仮想RISCマシン; a、b、cはそれぞれのレジスタのメモリ位置であると仮定します; aの10個の数値をbの10個の数値に加算し、結果をcに格納しますmove $10 , count ; count := 10 loop: load r1 , a load r2 , b add r3 , r1 , r2 ; r3 := r1 + r2 store r3 , c add a , a , $4 ; 移動add b , b , $4 add c , c , $4 dec count ; デクリメントjnez count , loop ; countがまだ0でない場合はループバックしますretしかし、ベクトルプロセッサにとって、このタスクは全く異なるものに見える。
; ベクトルレジスタv1~v3があり、サイズが10以上であると仮定します。 move $10 , count ; count = 10 vload v1 , a , count vload v2 , b , count vadd v3 , v1 , v2 vstore v3 , c , count ret命令の中にループが全く含まれていないことに注目してください。これは、 10個の連続した操作を実行したのはハードウェアであるためです。つまり、ループ回数は命令ごとに明示的に指定されます。
CrayスタイルのベクトルISAは、これをさらに一歩進めて、ベクトル長(VL)と呼ばれるグローバルな「カウント」レジスタを提供します。
; 再び、サイズが 10 以上のベクトルレジスタ v1-v3 があると仮定します。 setvli $10 # ベクトル長 VL=10 を設定vload v1 , a # a から 10 をロードvload v2 , b # b から 10 をロードvadd v3 , v1 , v2 # 10 を加算vstore v3 , c # c に 10 をストアretこのアプローチにはいくつかの節約効果がある。[ 18 ]
さらに、より現代的なベクトルプロセッサISAでは、「フェイルオンファースト」または「フォールトファースト」が導入されており(下記参照)、さらに多くの利点をもたらしています。
しかし、それ以上に、高性能なベクトルプロセッサでは、複数の機能ユニットがこれらの数値を並列に加算する場合があります。ベクトル命令は複数の独立した演算を指定するため、これらの数値間の依存関係のチェックは不要です。これにより、必要な制御ロジックが簡素化され、ストールを回避することでパフォーマンスをさらに向上させることができます。したがって、数学演算は全体的に大幅に高速化され、制限要因はメモリからデータをフェッチするのに必要な時間となります。
全ての問題がこのような解決策で対処できるわけではありません。このような命令を含めると、コアCPUの複雑さが必然的に増します。その複雑さによって、通常は他の命令の実行速度が低下します。つまり、連続して多数の数値を加算する場合以外では、他の命令の実行速度が低下します。より複雑な命令はデコーダの複雑さも増し、通常の加算などの一般的な命令のデコード速度が低下する可能性があります。(これは、ISA全体をRISCの原則に維持することで多少軽減できます。RVVは高度な機能を備えていても、約190個のベクトル命令しか追加しません。[ 19 ])
ベクトルプロセッサは従来、処理対象データが大量にある場合にのみ最高の性能を発揮するように設計されていました。そのため、これらのCPUは主にスーパーコンピュータに搭載されていました。スーパーコンピュータ自体も、一般的に気象予報センターや物理学研究所など、膨大な量のデータが処理される場所に設置されていたからです。しかし、前述のように、ベクトルISAの効率性は、組み込み用途においても魅力的なその他の利点をもたらします。
上記のベクトル擬似コードの例は、ベクトルコンピュータが一度に10個以上の数値を処理できるという大きな前提に基づいています。ベクトルレジスタに格納される数値の数が増えると、コンピュータがそれほど大きなレジスタを持つことは現実的ではなくなります。そのため、ベクトルプロセッサはループ処理を自ら行う機能を持つか、あるいはプログラマに何らかのベクトル制御(ステータス)レジスタ(通常はベクトル長と呼ばれる)を公開することになります。
自己反復命令は、STAR-100のような初期のベクトルコンピュータに見られ、上記の動作は単一の命令( のような)で記述されます。また、 x86アーキテクチャでは、 のプレフィックスとしてvadd c, a, b, $10存在します。しかし、この方法でハードウェア上で効率的に実行できる計算は、非常に単純なものに限られ、コストが大幅に増加します。STAR-100アーキテクチャではすべてのオペランドをメモリに格納する必要があるため、アクセスによるレイテンシも非常に大きくなりました。REP
BroadcomはVideocore IV ISAのすべてのベクトル演算にフィールド用の領域を含めたが、繰り返しにメモリを使用するSTAR-100とは異なり、Videocore IVの繰り返しは算術ベクトル演算を含むすべての演算に適用される。繰り返しの長さは2のべき乗のREP小さな範囲にするか、スカラーレジスタのいずれかから取得することができる。[ 20 ]
Cray -1は、プロセッサレジスタを使用してベクトルデータをバッチ単位で保持するというアイデアを導入しました。バッチ長(ベクトル長、VL)は特別な命令で動的に設定でき、Videocore IV(そして後述するようにSIMDも)と比較して重要な点は、繰り返し長を命令エンコーディングに含める必要がないことです。この方法により、各バッチで実行できる処理量が大幅に増加し、命令エンコーディングもはるかに洗練されコンパクトになります。唯一の欠点は、この追加のバッチ処理能力を最大限に活用するために、メモリのロードおよびストア速度もそれに応じて向上させる必要があったことです。これは、Crayスタイルのベクトルプロセッサの欠点として指摘されることがありますが、富士通VPシリーズはこの間違いを犯しました。実際には、 GPUに見られるように、これは高性能スループットを実現するための要素であり、GPUも全く同じ問題に直面しています。
最新のSIMDコンピュータは、通常のスカラーパイプラインのみを使用する場合と比較して、複数のALUを直接使用することで並列度を高め、初期のCrayコンピュータよりも優れていると主張しています。最新のベクトルプロセッサ(SX-Aurora TSUBASAなど)は、複数の内部パイプラインSIMD ALUに複数のデータを発行することで、両方を組み合わせています。発行されるデータの数は、実行時にベクトルプログラムによって動的に選択されます。マスクを使用して、メモリ位置にデータを選択的にロードおよびストアしたり、同じマスクを使用してSIMD ALUの処理要素を選択的に無効にしたりできます。SIMDを備えた一部のプロセッサ(AVX-512、ARM SVE2)は、このような要素ごとの選択的(「述語付き」)処理が可能であり、これらのプロセッサは「ベクトルプロセッサ」という名称にふさわしい、あるいは少なくとも「ベクトル処理」が可能であると主張するに値します。要素ごとの述語を持たないSIMDプロセッサ(MMX、SSE、AltiVec)は、断じてそうではありません。
多数の小型演算ユニット(それぞれが独立したSIMD ALUを持つ)を備えた最新のGPUは、単一命令複数スレッド(SIMT)方式を採用しています。SIMTユニットは、共有の単一のブロードキャスト同期命令ユニット(BUI)から実行されます。「ベクトルレジスタ」は非常に幅広く、パイプラインは長くなる傾向があります。SIMTの「スレッド化」とは、各演算ユニット上でデータが独立して処理される方法を指します。
さらに、Broadcom Videocore IV などの GPU やNEC SX-Aurora TSUBASAなどの外部ベクトルプロセッサでは、幅が示すよりも少ないベクトルユニットを使用する場合があります。64 個の数値を格納できるレジスタに 64 個のユニットを使用する代わりに、ハードウェアはハイブリッド アプローチとして 16 個のユニットでパイプライン ループを実行する可能性があります。Broadcom Videocore IV もこのハイブリッド アプローチに対応しています。命令では SIMD QPU エンジンが 16 個の長の FP 配列演算をサポートすると明記されていますが、実際には (別の) 形式の「スレッド」として 4 つずつ実行します。[ 21 ]
この例では、アルゴリズム(「IAXPY」)から始め、まずスカラー命令、次にSIMD、次に述語付きSIMD、最後にベクトル命令で示します。これにより、従来のベクトルプロセッサと最新のSIMDプロセッサの違いを段階的に説明できます。この例は、C言語の「DAXPY」関数の32ビット整数版から始まります。
void iaxpy ( size_t n , int a , const int x [], int y []) { for ( size_t i = 0 ; i < n ; i ++ ) { y [ i ] = a * x [ i ] + y [ i ]; } }各反復処理において、yの各要素にxの要素にaを乗じた値が加算されます。プログラムの可読性を高めるため、スカラー線形形式で記述されています。
このスカラー版では、xとyをそれぞれ1つずつ読み込み、1つの計算を実行し、1つの結果を保存して、ループします。
loop: load32 r1 , x ; 32ビットデータを1つロードload32 r2 , y mul32 r1 , a , r1 ; r1 := r1 * a add32 r3 , r1 , r2 ; r3 := r1 + r2 store32 r3 , y addl x , x , $4 ; x := x + 4 addl y , y , $4 subl n , n , $1 ; n := n - 1 jgz n , loop ; n > 0 の場合、ループバックout: retSTARのようなコードは簡潔さを保っているが、STAR-100のベクトル化はメモリアクセスを基本としていたため、情報処理には追加のメモリスロットが必要となる。また、メモリアクセスが増えるため、レイテンシも2倍になる。
; tmp は事前割り当てされていると仮定しますvmul tmp , a , x , n ; tmp[i] = a * x[i] vadd y , y , tmp , n ; y[i] = y[i] + tmp[i] ret多くの名称で知られる最新のパックドSIMDアーキテクチャ(Flynnの分類法に記載)は、ほとんどの演算をバッチで実行できます。コードはスカラー版とほぼ同じです。ここでは、xとyの両方が適切にアラインメントされていること(16の倍数からのみ開始すること)、およびnが4の倍数であることを前提としています。そうでない場合は、マスクを計算したり、スカラー版を実行したりするためのセットアップコードが必要になります。また、簡略化のために、SIMD命令にはARM NEONのようにスカラーオペランドを自動的に繰り返すオプションがあると仮定することもできます。[ 22 ]そうでない場合は、「スプラット」(ブロードキャスト)を使用して、スカラー引数をSIMDレジスタ全体にコピーする必要があります。
splatx4 v4 、a ; v4 = a,a,a,a所要時間は、上記で説明したベクトル実装の場合とほぼ同じですy = mx + c。
vloop: load32x4 v1 , x load32x4 v2 , y mul32x4 v1 , a , v1 ; v1 := v1 * a add32x4 v3 , v1 , v2 ; v3 := v1 + v2 store32x4 v3 , y addl x , x , $16 ; x := x + 16 addl y , y , $16 subl n , n , $4 ; n := n - 4 jgz n , vloop ; n > 0 の場合は戻るout: retxポインタとyポインタの両方が16ずつ増加することに注意してください。これは、32ビット整数4個の長さ(バイト単位)が16であるためです。このアルゴリズムは4ビット幅のSIMDのみに対応するという決定がなされたため、定数はプログラムにハードコーディングされています。
SIMDにとって残念なことに、手がかりは上記の「nは4の倍数である」という仮定と「整列アクセス」にあり、これは明らかに限定された専門的なユースケースである。
現実的には、ポータブルライブラリなどの汎用ループでは、nをこのように制限できないため、SIMD幅の倍数でない値に対処するためのSIMDのセットアップとクリーンアップのオーバーヘッドは、ループ内の命令数をはるかに超える可能性があります。最悪の場合、ハードウェアがアライメントされていないSIMDメモリアクセスを実行できないと仮定すると、実際のアルゴリズムは次のようになります。
8 幅の SIMD では、最初の SIMD 要素と最後の SIMD 要素 (0 <= n <= 7) をカバーするために、まず 4 幅の SIMD 要素、次に 2 幅の SIMD、次に 1 (スカラー) で内側のループ アルゴリズムを繰り返し、それぞれの間にテストと分岐を挿入する必要があります。
これによりコードのサイズは3倍以上になり、極端な場合には命令数が桁違いに増加します。これは、 gccのオプションを使用して、 iaxpyの例をAVX-512用にコンパイルすることで簡単に実証できます。"-O3 -march=knl"
ISAは性能向上を目指して進化を続けており、その結果、ISA設計者は2ビット幅のSIMD、4ビット幅のSIMD、8ビット幅のSIMDなどを追加していくことになります。このようにして、x86アーキテクチャにAVX-512が存在する理由が理解できるでしょう。
述語がない場合、SIMD の幅が広くなるほど問題は悪化し、オペコードの大量増殖、パフォーマンスの低下、電力消費の増加、不必要なソフトウェアの複雑さにつながります。[ 23 ]
一方、ベクトルプロセッサは、任意のカウント n に対して可変長の計算を実行するように設計されているため、セットアップはほとんど必要なく、クリーンアップも不要です。マスクを持つ(ただしsetvl命令を持たない)SIMD ISAと比較しても、ベクトルプロセッサは、最後の数個の要素をカバーするために明示的なマスク計算を実行する必要がないため、はるかにコンパクトなコードを生成します(下図参照)。
仮に述語付き(マスク対応) SIMD ISAを想定し、さらにSIMD命令がアライメントのずれたデータに対応できると仮定すると、命令ループは次のようになります。
vloop: # マスクを準備します。ただし、min を持つ ISA はほとんどありません。min t0 、n 、$4 ; t0 = min(n, 4) shift m 、$1 、t0 ; m = 1<<t0 sub m 、m 、$1 ; m = (1<<t0)-1 # ここで、m ビットでマスクされた演算を実行しますload32x4 v1 、x 、m load32x4 v2 、y 、m mul32x4 v1 、a 、v1 、m ; v1 := v1 * a add32x4 v3 、v1 、v2 、m ; v3 := v1 + v2 store32x4 v3 、y 、m # 次のループのために x、 y、 n を更新しますaddl x 、t0 * 4 ; x := x + t0*4 addl y , t0 * 4 subl n , n , t0 ; n := n - t0 # ループ? jgz n , vloop ; n > 0 の場合、戻るout: retここでは、コードがはるかに簡潔であるものの、やや複雑になっていることがわかります。ただし、少なくともセットアップやクリーンアップはありません。ループの最後のイテレーションでは、述語マスクが 0b0000、0b0001、0b0011、0b0111、または 0b1111 のいずれかに設定され、それぞれ 0 ~ 4 つの SIMD 要素操作が実行されます。もう 1 つの潜在的な複雑な点として、一部の RISC ISA には「min」命令がなく、代わりに分岐またはスカラー述語比較を使用する必要があります。
述語付きSIMDは、述語マスクを用いることで可変長ベクトルに対応できるため、少なくとも「ベクトル対応」という用語にふさわしいことは明らかです。しかし、「真の」ベクトルISAへの最終段階は、ISA内にSIMD幅に関する痕跡を一切持たず、それを完全にハードウェアに委ねることです。
RVVのようなCrayスタイルのベクトルISAでは、「 setvl 」(ベクトル長設定)と呼ばれる命令が使用されます。ハードウェアはまず、1つの「ベクトル」で処理できるデータ値の数を定義します。これは実際のレジスタの場合もあれば、内部ループ(前述のハイブリッド方式)の場合もあります。この最大数(ハードウェアの「レーン」の数)は「MVL」(最大ベクトル長)と呼ばれます。SX-AuroraやVideocore IVに見られるように、MVLは実際のハードウェアレーン数、または仮想レーン数である場合があることに注意してください。(注:ARM SVE2チュートリアルで述べられているように、プログラマはベクトル幅が固定されていると想定する間違いを犯してはなりません。したがって、MVLはプログラマが知っておく必要のある量ではありません。これは、長年SIMDの考え方に慣れ親しんできた人にとっては少し戸惑うかもしれません。)
処理すべき未処理データ要素の数を指定してsetvl を呼び出すと、 setvl はそれを最大ベクトル長 (MVL) に制限することが許可されており (実質的には必須)、後続のベクトル命令でハードウェアが処理できる実際の数を返し、内部特殊レジスタ「VL」をその数に設定します。ARM は、SVE2 のチュートリアルでこの手法を「ベクトル長に依存しない」プログラミングと呼んでいます。[ 24 ]
以下は、上記のSIMDスタイルのループと同じものに対するCrayスタイルのベクトルアセンブラです。ハードコードされた定数の代わりに、t0(VLの便利なコピーを含み、可変)が使用されていることに注意してください。
vloop: setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # ベクトル x をロードvld32 v1 , y # ベクトル y をロードvmadd32 v1 , v0 , a # v1 += v0 * a vst32 v1 , y # Y を格納add y , t0 * 4 # y を VL*4 だけ進めるadd x , t0 * 4 # x を VL*4 だけ進めるsub n , t0 # n -= VL (t0) bnez n , vloop # n != 0 の場合、繰り返すこれは基本的にSIMDバージョン(MVLが4の場合、ループごとに4つのデータ要素を処理)や初期のスカラーバージョン(1つだけを処理)とそれほど違いはありません。nには処理すべき残りのデータ要素の数が格納されますが、t0にはVLのコピー、つまり各イテレーションで処理される数値が格納されます。各イテレーションの後、nからt0が減算され、nがゼロであればすべての要素が処理されたことになります。
予測型SIMDアセンブリバリアントと比較する際に注意すべき点がいくつかあります。
setvl命令にはmin命令が埋め込まれているこのように、ベクトルISAがいかに命令数を削減するかが非常に明確にわかる。
また、述語付き SIMD バリアントと同様に、x と y へのポインタは両方とも 32 ビットのデータを指しているため、t0 の 4 倍だけ進みますが、n は t0 だけデクリメントされることに注意してください。固定サイズの SIMD アセンブラと比較すると、明らかな違いはほとんどありません。x と y はハードコードされた定数 16 だけ進み、n はハードコードされた 4 だけデクリメントされるため、最初は重要性を理解するのが難しいです。違いは、ベクトル ハードウェアが 4 個の同時演算、または 64 個または 10,000 個の同時演算を実行できる可能性があるという認識にあります。それらすべてに対してまったく同じベクトル アセンブラが使用され、SIMD クリーンアップ コードはまだ存在しません。述語対応 SIMD と比較しても、よりコンパクトで、より明確で、より洗練されており、使用するリソースも少なくなっています。
これは、よりコンパクトなプログラムであるだけでなく(L1キャッシュのサイズを節約できる)、前述したように、ベクトル版ではALUにより多くのデータ処理を発行できるため、命令のデコードと発行がアイドル状態になる可能性があり、電力消費を抑えることができます。
さらに、関数に入力される要素数はゼロから始めることができます。これにより、ベクトルの長さがゼロに設定され、実行時にすべてのベクトル命令が事実上無効になり、何もしない命令になります。したがって、非述語型SIMDとは異なり、処理する要素がない場合でも、n=0の場合でも、無駄なクリーンアップコードやプリアンブルは発生しません。
IBM 3090では、さらなる利点(命令数のさらなる削減)が達成された。[ 25 ]
VLVCU GR4 # VCTをロードし、GR4とCコードを更新vloop: vld32 v0 , x , v0 # ベクトルxをロードし、v0を更新vld32 v1 , y # ベクトルyをロード(v1は更新しない)vmadd32 v1 , v0 , a # v1 += v0 * a vst32 v1 , y , v1 # Yを保存し、v1を更新VLVCU GR4 # VCTをロードし、GR4とCコードを更新BC 3 , vloop # VCT>0の場合に分岐このVLVCU命令は、ベクトルカウント長(VCT)を設定するだけでなく、スカラーレジスタから新しいベクトル長を減算し、分岐命令がテストできる条件コードを更新します。事実上、VLVCU2つ(条件コードの設定を含めると3つ)を1つにまとめています。
# IBM 370 VLVCU は次の処理を実行します: setvl GR4 # VL=min(MVL, GR4) sub GR4 , VL # n -= VL (GR4)、CC も設定さらに、ベクトルロード命令とベクトルストア命令の両方でアドレスレジスタの更新を実行できるため、ベクトルの各要素をロード/ストアする際に、実際には既に裏側で実行されていたアドレス計算を実行するための明示的な命令を削減できます。
全く同じ原理がLibre-SOCでも使用されており、DAXPY(64ビット浮動小数点)の例で説明されています。[ 26 ]
# r5: n カウント; r6: x ポインタ; r7: y ポインタ; fp1: a mtctr 5 # n を CTR に移動vloop: setvl MAXVL = 32 , VL = CTR # 実際には VL=MIN(MAXVL,CTR) # DAXPY は 8 バイトなので、8(r6) と 8(r7) sv.lfdup * 32 , 8 ( r6 ) # x を fp32-63 にロードし、x をインクリメントsv.lfd / els * 64 , 8 ( r7 ) # y を fp64-95 にロードし、インクリメントしないsv.fmadd * 64 ,* 64 , 1 ,* 32 # (*y) = (*y) * (*x) + a sv.stfdup * 64 , 8 ( r7 ) # y に格納し、y を後インクリメントsv.bc / ctr vloop # CTR をデクリメントVL、ジャンプ!ゼロ更新後モードは、Load-with-Update および Store-with-Update のベクトル化バージョンに追加され、CPU の Load/Store 内部処理でアドレス計算が既に行われている場合に、アドレス計算のための別の命令が不要になります。ここでも IBM 370 の例と同様に、ベクトル y アドレスは Load-with-Update ではなく Store-with-Update によって更新されることに注意してください。合計カウントにはスカラー レジスタではなくPower ISAsv.bc/CTR CTR レジスタが使用され、命令は現在のベクトル長 (VL) で CTR を減らし、CTR がゼロかどうかをテストして、ゼロでない場合は分岐を実行します。
アプローチは異なりますが、IBM 3090 と同じ最終結果、つまり既に非常にコンパクトであると考えられている命令の大幅な圧縮を実現します。[ 27 ] RISC -V Vector DAXPY の内部ループの例は 10 命令ですが、上記の Libre-SOC と IBM 370 はどちらも 6 命令で、40% 削減されています。
この例では、削減を含むアルゴリズムから始まります。前の例と同様に、まずスカラー命令、次にSIMD、最後にベクトル命令で示します。C言語から始めます。
void ( size_t n , int a , const int x []) { int y = 0 ; for ( size_t i = 0 ; i < n ; i ++ ) y += x [ i ]; return y ; }ここでは、アキュムレータ(y)を使用して、配列x内のすべての値を合計します。
このスカラー版では、x の各値を読み込み、y に加算し、ループを繰り返します。
set y , 0 ; y をゼロに初期化loop: load32 r1 , x ; 32 ビットのデータを 1 つロードadd32 y , y , r1 ; y := y + r1 addl x , x , $4 ; x := x + 4 subl n , n , $1 ; n := n - 1 jgz n , loop ; n > 0 の場合、ループバックout: ret y ; 結果 y を返すこれは非常に単純です。「y」はゼロから始まり、32ビット整数が1つずつr1にロードされ、yに加算され、配列「x」のアドレスが配列内の次の要素に移動します。
問題はここから始まります。SIMDは設計上、「要素間」の算術演算を実行できません。あるSIMDレジスタの要素0は別のレジスタの要素0に加算できますが、要素0は別の要素0以外には加算できません。これは、実装の可能性に深刻な制約をもたらします。簡略化のため、nは正確に8であると仮定します。
addl r3 , x , $16 ; x の 2 番目の 4 要素load32x4 v1 , x ; x の最初の 4 要素load32x4 v2 , r3 ; x の 2 番目の 4 要素add32x4 v1 , v2 , v1 ; 2 つのグループを追加現時点で4回の追加処理が実行されています。
x[0]+x[4]- 最初のSIMD加算:最初のグループの要素0が2番目のグループの要素0に加算されますx[1]+x[5]- 2回目のSIMD加算:第1グループの要素1が第2グループの要素1に加算されますx[2]+x[6]- 3回目のSIMD加算:第1グループの要素2が第2グループの要素2に加算されるx[3]+x[7]- 4回目のSIMD加算:第1グループの要素3が第2グループの要素3に加算されるしかし、4 幅 SIMD は設計上、加算などの演算ができないx[0]+x[1]ため、汎用 IAXPY ループに SIMD を使用する場合と同様に、状況は急速に悪化します。4 つの部分結果を合計するには、2 幅 SIMD を使用し、その後に単一のスカラー加算を行うことで最終的な答えが得られますが、多くの場合、最後のスカラー演算を実行する前に、専用の SIMD レジスタからデータを転送する必要があります。
一般的なループ (n は固定ではない) であっても、4 幅の SIMD を使用する唯一の方法は、それぞれ 4 要素ずつオフセットされた 4 つの別々の「ストリーム」を想定することです。最後に、4 つの部分的な結果を合計する必要があります。SIMD ハードウェアの幅が増加するにつれて問題は複雑化し (まったく新しい命令セットが導入される)、さらに悪いことに、AMD XOP_instruction_setなどの水平加算を実行するための一部の命令は、数年後に削除されました。いくつかの手法にはシャッフルが含まれます。AVX -512およびSSEの「水平加算」の実行方法に関する非常に詳細な知識を必要とするオンラインの例が見つかります[ 28 ] [ 29 ]
プログラムの規模や複雑さとは別に、浮動小数点演算が関係する場合、もう一つ潜在的な問題が生じる。それは、値が厳密な順序で合計されない(4つの部分的な結果)ため、丸め誤差が発生する可能性があるということだ。
ベクトル命令セットには、ISAに組み込まれた算術還元演算があります。nが最大ベクトル長以下であると仮定すると、必要な命令はわずか3つです。
setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # ベクトル x をロードvredadd32 y , v0 # y にリデュース加算nが最大ベクトル長よりも大きい場合のコードはそれほど複雑ではなく、最初の例(「IAXPY」)と同様のパターンになります。
set y , 0 vloop: setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # ベクトル x をロードvredadd32 y , y , v0 # すべての x を y に追加add x , t0 * 4 # x を VL*4 だけ進めるsub n , t0 # n -= VL (t0) bnez n , vloop # n != 0 の場合、繰り返すret yこのアルゴリズムのシンプルさは、SIMDと比較すると際立っています。IAXPYの例と同様に、このアルゴリズムは長さに依存しません(最大ベクトル長が1しかない組み込み実装でも同様です)。
ハードウェア実装では、正しい答えが生成されることが確実な場合、並列で削減を実行することがあります。一部のベクトルISAでは、丸め誤差が問題にならないことがプログラマーにわかっており、低遅延が重要な場合に備えて、並列削減モードを明示的なオプションとして提供しています。[ 30 ]
この例は、真のベクトルプロセッサと、ベクトルプロセッサの機能を模倣したSIMDプロセッサ(ほとんどの商用GPUを含む)との間の、重要な根本的な違いを改めて浮き彫りにしている。
ベクトルプロセッサを謳うSIMDプロセッサと比較すると、プログラムサイズの桁違いの削減は驚くべきものだ。しかし、ISAレベルでのこの洗練された設計は、ハードウェアレベルではかなりのコストがかかる。
全体的に見て、
こうした明確な違いこそが、ベクトルプロセッサとSIMDプロセッサを区別する点である。
多くのSIMD ISAが以下のリストを参考にしたり、そこから着想を得たりしているのに対し、ベクトルプロセッサが持つ典型的な機能は次のとおりです。[ 31 ] [ 32 ] [ 33 ]
x[i] = y[i] + x[i-1]リダクションは の形式です。x = y[0] + y[1]… + y[n-1]多くの3Dシェーダーアプリケーションでは、三角関数演算と、一般的な演算(RGB、ARGB、XYZ、XYZW)のための短いベクトルが必要となるため、ベクトルプロセッサに搭載されているものに加えて、最新のGPUでは通常、以下のサポートが提供されています。
ARM SVE2とRISC-V RVVで導入されたのは、投機的逐次ベクトルロードの概念です。ARM SVE2には「First Fault Register」と呼ばれる特別なレジスタがあり[ 43 ] 、 RVVはこのレジスタでベクトル長(VL)を変更(切り捨て)します[ 44 ] 。
ffirstの基本原理は、大きなシーケンシャル ベクトル ロードを試みるものの、ハードウェアが実際にロードするデータを、メモリ フォルトを発生させずに成功する量、または単に最も都合の良い量(ゼロより大きい値)に任意に切り捨てることを可能にすることです。重要なのは、後続の命令が実際に成功したロードの回数を通知または正確に判断し、その回数を使用して実際にロードされたデータに対してのみ処理を行うことです。
この状況をSIMDと比較してみましょう。SIMDは固定(柔軟性のない)ロード幅と固定データ処理幅を持ち、ページ境界をまたぐロードには対応できません。たとえ対応できたとしても、実際に成功した処理に適応することはできません。しかし、逆説的ですが、SIMDプログラムが(各内部ループで毎回)何が最適に成功するかを事前に調べようと試みたとしても、それらの命令は必然的に重要な内部ループの一部となるため、パフォーマンスを阻害するだけです。
これは、 ffirstが非常に革新的である理由を示唆し始めており、標準的な 128 ビット非述語非 ffirst SIMD で実装された memcpy または strcpy で最もよく説明されます。IBM POWER9 の場合、strncpy を実装するための手動最適化命令の数は 240 を超えています。[ 45 ] 対照的に、手動最適化された RVV アセンブラの同じ strncpy ルーチンはわずか 22 命令です。[ 46 ]
上記の SIMD の例では、読み取ろうとする値が多すぎるためにメモリの末尾で障害が発生し、失敗する可能性があります。同様に境界をまたぐことで、多数のページ障害やアライメントエラーが発生する可能性もあります。対照的に、ベクトルアーキテクチャにロードする要素数を自由に決定させることで、strncpy の最初の部分が、最適ではないメモリ境界から開始された場合でも、ループの次の反復でベクトル化されたメモリ読み取りのバッチが基となるキャッシュや仮想メモリの配置と最適にアライメントされるように、十分なロードを返すことができます。さらに、ハードウェアは、任意のループ反復のメモリ読み取りをページ境界で正確に終了する機会を利用することを選択できます(コストのかかる 2 回目の TLB ルックアップを回避)。投機的実行により、データが現在のループで処理されている間に次の仮想メモリページが準備されます。これらすべては、プログラム自体ではなく、ハードウェアによって決定されます。[ 47 ]
ベクトル速度比をr 、ベクトル化比をfとする。ベクトルユニットが64個の数値の配列を加算するのにかかる時間が、同等のスカラーユニットの10倍速い場合、r = 10となる。また、プログラム内の演算の総数が100で、そのうちスカラー演算が10個(ベクトル化後)のみの場合、f = 0.9となり、つまり、処理の90%がベクトルユニットによって行われる。達成可能な高速化は以下の通りである。
したがって、ベクトルユニットの性能が非常に高い場合でも()速度向上は以下これは、比率fがパフォーマンスにとって極めて重要であることを示唆している。この比率は、メモリ内の要素の隣接性など、コンパイルの効率に依存する。
は異なり、ベクトル長レジスタ vl を備えているため、ベクトル命令は任意の n の値で動作します。