レジスタ内SIMD(SWAR )は、「パックドSIMD」 [ 1 ]とも呼ばれ、プロセッサレジスタに含まれるデータに対して並列演算を実行するための技術です。SIMDは、単一命令複数データを意味します。
多くの現代の汎用コンピュータプロセッサには、SIMD用の機能があり、レジスタ群とそれらを利用する命令群が用意されています。SWAR は、SIMD 演算に優れた専用処理エンジンを使用するのではなく、これらのレジスタと命令を使用することを指します。また、さまざまな斬新なソフトウェアのトリックによって、当時 SIMD 用に設計されていなかった汎用レジスタと命令で SIMD を使用することも指します。[ 3 ]
SWARアーキテクチャとは、レジスタの独立したサブワードまたはフィールドに格納されたデータに対して並列演算を実行することを明示的に目的とした命令を含むアーキテクチャのことである。SWAR対応アーキテクチャとは、その目的のために明示的に設計された命令が含まれていなくても、これらのフィールドに格納されたデータを独立して処理できるようにするのに十分な命令セットを含むアーキテクチャのことである。
1958年に稼働した最初の歴史的な例の1つは、リンカーン研究所のTX-2で、36ビット幅のメモリを持ち、ALUで1つの36ビット、または2つの18ビット、または4つの9ビットのサブワードで動作できる命令を備えていました。TX -2はSIMDという用語の発明よりも前に登場しました。[ 4 ]
SWARアーキテクチャの初期の代表的な例としては、 MMX拡張セットを実装したIntel Pentium with MMXが挙げられる。一方、 Intel Pentiumにはそのような命令は含まれていなかったが、綿密な手動コーディングやコンパイラ技術を用いることで、SWARアーキテクチャとして動作させることができた。
初期のSWARアーキテクチャには、DEC Alpha MVI、ヒューレット・パッカードのPA-RISC MAX、シリコン・グラフィックス社のMIPS MDMX、サン社のSPARC V9 VISなどがあります。MMXと同様に、SWARの命令セットの多くは、より高速なビデオコーディングを目的としています。[ 5 ]
ウェスリー・A・クラークは1950年代に分割サブワードデータ操作を導入しました。これはSWARの非常に初期の先駆けと見なすことができます。レスリー・ランポートは1975年に「フルワード命令による複数バイト処理」 [ 6 ]というタイトルの論文でSWAR技術を発表しました。
1996年にインテルがMMXマルチメディア命令セット拡張機能を導入したことで、SIMD並列処理機能を備えたデスクトッププロセッサが普及した。当初、これらの命令は手書きのアセンブリコードでしか使用できなかった。
1996年秋、ハンク・ディーツ教授はパデュー大学電気・コンピュータ工学部で、学部生向けのコンパイラ構築コースの講師を務めていた。このコースでは、学生にMMXをターゲットとしたシンプルなコンパイラを構築する一連のプロジェクトを課した。入力言語は、MasParのMPLのサブセット方言であるNEMPL(Not Exactly MPL)だった。
学期が進むにつれて、授業担当のティーチングアシスタントであるランドール(ランディ)・フィッシャーは、MMXにはNEMPLコンパイラのバックエンド構築を困難にする多くの問題があることに気づいた。例えば、MMXには16ビットデータの乗算命令はあるが、8ビットデータの乗算命令はない。NEMPL言語はこの問題を考慮していなかったため、プログラマは8ビット乗算を必要とするプログラムを作成できてしまう。
Intelのx86アーキテクチャだけがSIMDライクな並列命令を組み込んだアーキテクチャではなかった。SunのVIS、SGIのMDMX 、その他のマルチメディア命令セットは、いわゆるニューメディアアプリケーションをサポートするために、他社の既存の命令セットアーキテクチャに追加されていた。これらの拡張機能は、データの精度やサポートされる命令の種類において大きな違いがあった。
ディーツとフィッシャーは、対象アーキテクチャの詳細を知らなくてもプログラミングがモデルをターゲットにできる、明確に定義された並列プログラミングモデルのアイデアの開発に着手した。このモデルはフィッシャーの博士論文の基礎となった。ディーツとフィッシャーは、ある日パデュー大学のMSEE棟にあるハンクのオフィスで「SWAR」という頭字語を考案した。[ 7 ] これは、この形式の並列処理、この種の処理をネイティブに実行するように設計されたアーキテクチャ、そしてフィッシャーの博士論文である汎用プログラミングモデルを指している。
これらの大きく異なるアーキテクチャ向けにコンパイルする問題については、LCPC98で発表された論文で議論された。[ 5 ]
SWAR処理は、画像処理[ 8 ] 、 暗号ペアリング[ 9 ] 、 ラスター処理[ 10 ] 、 計算流体力学[ 11 ] 、 および通信[ 12 ]で使用されています。
SWAR技術は、特別なハードウェアサポートのないシステムでも使用できます。 論理演算はビット単位で動作するため、レジスタの各ビットに対して独立して動作します。加算と減算の使用はより困難ですが、レーン間で不要な桁上がり伝播を避けるように注意すれば有効です。この桁上がり伝播を除けば、1回の64ビット加算または減算は、8回の8ビット加算または減算と同じです。
SWAR技術の典型的な例は、レジスタ内のビット数(セットされているビット数)を求めることだろう。レジスタは、1ビット、2ビット、4ビットなどのフィールドの連続として順次扱われる。
まず、1ビットフィールドの要素数は、そのフィールド自体であることに注意してください。2ビットフィールドの要素数を求めるには、それを構成する2つの1ビットフィールドの要素数を合計します。これは、64ビット値に含まれる32個の2ビットフィールドに対して並列に実行できますx。
x2 := (x & 0x5555555555555555) + ((x >> 1) & 0x5555555555555555);
16進定数は20x5進数で0101 2となり、偶数ビットを分離します。加算は各2ビットフィールドをオーバーフローすることはありません。なぜなら、可能な最大和は2だからです。
これを繰り返すことで、2ビットのフィールドを4ビットのフィールドに結合することができます。ここでは、バイナリ0011 2、または16進数のマスクを使用して0x3、ビットのペアを分離します。
x4 := (x2 & 0x3333333333333333) + ((x2 >> 2) & 0x3333333333333333);
これで、各4ビットフィールドには0から4までのカウント値が格納されます。4ビットフィールドは最大15までの値を格納できるため、2つの4ビットのカウント値を加算してもオーバーフローは発生しません。そのため、マスキング処理は加算後に行うことができ、被加数ごとに1回行う必要はありません。
x8 := (x4 + (x4 >> 4)) & 0x0f0f0f0f0f0f0f0f;
この時点で、8ビットのフィールドは最大255までの値を保持できるため、最後までそれ以上のマスキングは必要ありません。
x16 = x8 + (x8 >> 8); x32 = x16 + (x16 >> 16); x64 = x32 + (x32 >> 32); population_count = x64 & 0xff;
これにはいくつかのよく知られたバリエーションがあります。特に、最後の 3 つのシフトと加算のステップは次のように組み合わせることができます。
population_count = (x8 * 0x0101010101010101) >> 56;
シフトと加算の3段階の処理には6つの命令が必要で、各命令は前の命令のデータに依存するため、少なくとも6クロックサイクルを要します。乗算は通常、より高速に実行できます。32ビットワードを扱う場合は、3サイクル乗算が一般的であるため、状況はそれほど明確ではありません。
2つ目の方法は、最初のステップを変更するものです。各2ビットフィールドの2ビットb1とb0を足し合わせるのではなく、2ビットフィールドの初期値を2b1 + b0とします。ここからb1を減算すると、1回のマスキング操作だけで目的の合計が得られます。
x2 := x − ((x >> 1) & 0x5555555555555555);
文字列の中からヌル終端文字を探すのはよくある処理です。64ビットプロセッサは一度に8バイトを処理できるため、1バイトずつ検索するのは非効率的です。
同様の手法は、まず対象のバイト値と排他的論理和演算を行うことで、パス名の区切り文字やその他の区切り文字を検索するためにも使用できます。
一部のアーキテクチャには、8バイトの比較を一度に実行するための特別な命令が含まれています。例えば、DEC Alphaには8バイトの比較を一度に実行する命令が含まれていましたCMPBGE。しかし、ゼロバイトの検索は、特別なサポートがなくても実行できます。
一つの方法としては、上記のビットカウントの例とよく似た方法で、8ビットをOR演算で結合する方法があります。
x2 = x | x<<1; x4 = x2 | x2<<2; x8 = x4 | x4<<4; byte_map = ~x8 & 0x8080808080808080;
これにより、byte_map元々ゼロだったバイトの最上位ビットに1ビットが入る結果となる。
しかし、算術演算によるキャリー伝播を利用することで、より高速に処理できます。各バイトに0x7f(バイナリ 01111111 2 ) を加算すると、下位 7 ビットがゼロでない場合、ビット 7 にキャリーが発生します。課題は、キャリー伝播がビット 7 で停止し、他のバイトに影響を与えないようにすることです。これは、各バイトの下位 7 ビットと上位ビットを別々に処理することで実現できます。まず、加算する前に、各バイトの下位 7 ビットをAND 演算して抽出します。0x7f0x7f
x7 = (x & 0x7f7f7f7f7f7f7f7f) + 0x7f7f7f7f7f7f7f7f;
次に、最も重要な部分と組み合わせます。
x8 = x7 | x;
この値では、各8ビットフィールドの最上位ビットが、そのバイトがゼロでない場合、1に設定されます。最後に:
byte_map = ~(x8 | 0x7f7f7f7f7f7f7f7f);
各バイトの不要な下位ビットをすべて設定し、その後すべてを反転させ、対応する入力バイトがゼロの場所にのみ 1 ビットを残します。(これは と同等です~x8 & 0x80...80が、同じ定数値を使用します。) 1 ビットがない場合は、次の単語で検索を続けることができます。 1 ビットがある場合は、その位置から文字列の長さを計算できます。
リトルエンディアンプロセッサで最初のゼロバイトを見つけることが目的であれば、 2つの異なる定数を使用して、より少ない操作で最下位ゼロバイトを見つけることが可能です。 [ 13 ]
x7 = x − 0x0101010101010101; byte_map = x7 & ~x & 0x8080808080808080;
各バイトbについて、 b − 1byte_mapの msbitが設定され、 bの msbit がクリアされている場合、その msbit が設定されます。これは、 b = 0 の場合にのみ発生します。
上記の記述は、に借用がない場合にのみ真となります。借用がある場合、b = 1 の場合でも条件は真となります。ただし、このような借用は下位ゼロバイトによってのみ生成されるため、最下位ゼロバイトは意図どおり正しく識別されます。
これによりバイナリ演算が1つ削減されるだけでなく、すべての演算が順次依存しているわけではないため、「and not」(ビットクリア)命令が存在することを前提として、2サイクルで実行できます。
ビットマップの一般化として、非常に小さなルックアップテーブルを単一のレジスタに格納することが可能です。例えば、1ヶ月の日数は28日から31日まで変化し、4つの値の範囲があります。これは12×2 = 24ビットで格納できます。
days_table = 0xeefbb3 + (is_leap_year << 2); days_in_month = 28 + (days_table >> 2*month & 3);
(これは0から始まる月番号を想定しています。1から始まる月番号の場合は、桁をずらすことで対応できますdays_table。)
この表が1つのレジスターにきれいに収まるため、閏年に合わせて簡単に修正できる。
{{cite web}}: CS1 maint: タイトルとしてアーカイブされたコピー (リンク)