これまでのSIMD演算は、すべて_レーンごと_に動作してきました。
結果のレーンiの値は、入力のレーンiから計算されます。
しかし、レーン間で値を移動させることが必要だったり、そうしたい場面は数多くあります。 たとえば、これから行いたい計算にとって、レーンの並び順が合っていないことがあります。
データをレーンをまたいで移動させるSIMD命令には、さまざまなやり方のものがあります。
シャッフルは、レジスターのバイトやレーンを並べ替える操作で、転送先の各レーンには、どこにあるものでもよい転送元のレーンから値を持ってきます。 同じレーンを複数の転送先に選ぶこともでき、そのため値をブロードキャストすることもできます。
シャッフル命令は、そのサイズと実行ドメインによって動作が異なります。
pshufd命令は、転送元の4つの32ビットレーンを並べ替えて転送先に格納します。
選択には8ビットの即値を使い、これを2ビットずつの4つのフィールドとして読みます。フィールドは転送先のレーンごとに1つです。 各フィールドは、4つある転送元レーンのうちどれをその転送先レーンにコピーするかを選びます。
| フィールド | レーンのインデックス |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
即値の中でフィールドがどの位置にあるか(右から左へ読む)が、選んだレーンをどこに挿入するかを示します。 転送元のレーンは複数回選ぶこともでき、これが1つのレーンをレジスター全体にブロードキャストする方法です。
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
浮動小数点のシャッフル命令は2つあり、どちらも同じ一般的な構文に従います。shuf + p + サイズ接尾辞(sまたはd)です。
これらの命令も、レーンを選ぶのに即値を使います。
shufpsは、先ほどの整数のシャッフルと同じ2ビットのフィールド符号化を使います。
ただし、128ビットのオペランドには64ビットのレーンが2つしかないため、shufpdは1ビットのフィールド符号化しか使いません。
これらの命令が対応する整数命令と違うのは、レーンを1つではなく_2つ_のオペランドから取る点です。
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
両方のオペランドが結果に寄与するため、2つのベクターを1ステップでインターリーブするのに使えます。 転送元と転送先が同じレジスターであれば、すべてのレーンがそこから取られます。
pshufbはもっとも汎用的なシャッフルです。
pshufbとpshufdは名前がよく似ていてサイズ接尾辞だけが違いますが、行う操作はまったく異なります。
まず、pshufdはレーンの位置を選ぶのに即値を使うのに対し、pshufbは転送元オペランドの制御ベクターを使います。
この制御ベクターは、xmmレジスターか16バイトのメモリーオペランドです。
16ある転送先レーンのそれぞれについて、制御ベクターの下位4ビットが転送元のバイトのインデックス(0〜15)を与えます。
制御ベクターのレーンiが転送元のバイトのインデックスjを保持しているなら、転送元のj-th番目のレーンが転送先のi番目のレーンに移動します。
これは、2つの命令の2つ目の違いを浮き彫りにします。
pshufdはシャッフルするレーンを別の転送元オペランドから取りますが、pshufbは転送先の中でその場でシャッフルを行います。
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufbはさらに柔軟で、任意のレーンをクリアできます。
制御ベクターのレーンiで最上位ビットが立っていると、転送先のレーンi-thはゼロにされます。
これによりpshufbは、1つの命令で、任意のバイトの並べ替えと選択的なクリアの両方をこなします。
pshufbはバイト単位で動作するため、隣り合うレーンをまとめることで、異なるサイズのレーンをシャッフルするのにも使えます。
たとえば、ダブルワードをシャッフルするのに使えます。
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
アンパック命令は、2つのオペランドからレーンを取り出し、2つを交互に織り交ぜます。 整数版と浮動小数点版があり、どちらもほぼ同じ一般的な構文構造に従いますが、2つの違いがあります。
lまたはhの接尾辞があり、各オペランドの下位半分(l)と上位半分(h)のどちらに作用するかを示します。bwやqdqです(16バイトのサイズ接尾辞は、movdquのようにdqです)。punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
パック命令は逆方向の操作で、2つのオペランドのレーンを組み合わせて、転送先の半分の幅のレーンにします。
これらの命令にはpの接頭辞が付きません。
それ以外の点では、構文はこれまでに見てきた要素を組み合わせたものになっています。
pack。sまたはu。s(以前のコンセプトで見た飽和演算と同じ)。unpckとは逆の挙動です)。_パック_操作は幅を狭めるので、lやhの接尾辞は必要ありません。
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
これらの命令は飽和します。つまり、出力値はより狭い範囲にクランプされます。 ダブルワードからワードへ、ワードからバイトへと動作します。 クワッドワードからダブルワードへの版はありません。
入力は常に符号付きとして解釈されることに注意してください。
符号付きか符号なしかという型は、_出力_に対するものです。
クランプする範囲を示しています。
たとえばpacksswbは、符号付きバイトの範囲、つまり[-128, 127]にクランプします。
結果の下位のレーンは転送先オペランドから、上位のレーンは転送元から来ます。
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
これらの命令に相当する浮動小数点命令はありません。
これまでは、SIMDレジスターと汎用レジスターの間でデータを移動させるのに、いつもmovq/movdを使ってきました。
これらの命令は、SIMDレジスターの最下位のレーンにしか書き込めず、そこからしか読み取れません。また、書き込むときは他のすべてのレーンをクリアします。
先頭だけでなく任意のレーンに対して同じことを行い、他のレーンには手を付けない命令もあります。
どちらも整数の構文に従います。p + insr/extr + サイズ接尾辞(b、w、d、qのいずれか)です。
どちらの場合も、汎用レジスターは通常32ビット幅です。
pinsrqとpextrqだけが64ビットのオペランドを必要とします。
メモリーオペランドは常に操作のサイズと同じです。pinsrb/pextrbなら8ビット、pinsrw/pextrwなら16ビット、といった具合です。
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
これらの命令は、movdやmovqとよく似ていて、生のバイトをそのまま移動させるだけであることに注意してください。
つまり、メモリーや汎用レジスターに格納された浮動小数点値にも使えます。
ソフトウェア画像パイプラインの内側のループ、つまりテクスチャを準備し、レイヤーを合成して画面に届く前の段階を書きます。 パイプラインはピクセルをブロック単位で処理し、ブロック全体に同じ操作を適用します。
ピクセルは、赤、緑、青、アルファという4つの1バイトのチャンネルが、この順に並んだものです(RGBA)。
1ブロックは4ピクセルなので、全部で16バイトです。
タスクは5つあります。
オペランドはメモリアドレスを通して受け取り、答えは結果のアドレスを通して書き込みます。 この演習のすべてのメモリアドレスは16バイトに整列されています。
この演習の計算は、スカラー演算ではなくSIMD命令を使って行う必要があります。
テクスチャはRGBAで保存されていますが、このパイプラインが描画するフレームバッファーは各ピクセルをBGRAの順で期待します。赤と青のチャンネルを入れ替え、緑とアルファのチャンネルはそのままにします。
画像はブロックの並びとして届き、すべてのブロックが同じ方法で変換されます。
to_display_order関数を実装します。この関数は、画像全体を1ブロックずつRGBAからBGRAに変換します。
チャンネル並べ替えの制御マスクは、メモリ上のパックされた定数として定義し、毎回のブロックで再利用するのがよいでしょう。
この関数は、次の順序で引数を受け取ります。
result: 変換後のブロックを書き込むバッファーのメモリアドレス。1ブロックあたり16バイト。pixels: 変換元ブロックのメモリアドレス。1ブロックあたり4ピクセルで、各ピクセルはRGBA順の4バイト。block_count: ブロックの数。常に0より大きい。pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
この関数に戻り値はありません。
領域をクリアしたり、平坦な範囲を塗ったりするために、パイプラインは領域のすべてのピクセルに単一の色を書き込みます。
fill_region関数を実装します。この関数は、block_count個のブロックからなる領域を、1つの色のコピーで埋めます。
この関数は、次の順序で引数を受け取ります。
result: 塗りつぶしたブロックを書き込むバッファーのメモリアドレス。1ブロックあたり16バイト。color: 1ピクセルのメモリアドレス。RGBA順の4バイト。block_count: 塗りつぶすブロックの数。常に0より大きい。color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
この関数に戻り値はありません。
2つの単一チャンネルのレイヤーを、サンプルを交互に並べて1つのバッファーに統合する必要があります。 結果は、最初のレイヤーのサンプル、次に2番目のレイヤーのサンプル、という順に交互になります。
weave_scanlines関数を実装します。この関数は、それぞれ16サンプルの2つの行を、32サンプルの1つの行に交互に並べます。
この関数は、次の順序で引数を受け取ります。
result: 交互に並べた32サンプルを書き込むバッファーのメモリアドレス。first: 1番目の行のメモリアドレス。16サンプルで、各サンプルは8ビットの値。second: 2番目の行のメモリアドレス。16サンプルで、各サンプルは8ビットの値。first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
この関数に戻り値はありません。
明るさの処理では、各サンプルを16ビットの作業精度でスケールします。そのため、明るすぎるサンプルは255を超えることがあり、差は0を下回ることがあります。
最終段階では、それらの作業値を表示用の8ビットサンプルに戻します。0より小さい値は0に、255より大きい値は255にクランプします。
pack_samples関数を実装します。この関数は、8つの作業値からなる2つのグループを、16サンプルの1つの行に順番に狭めます。
この関数は、次の順序で引数を受け取ります。
result: クランプした16サンプルを書き込むバッファーのメモリアドレス。各サンプルは8ビットの値。first: 最初の8つの作業値のメモリアドレス。各値は16ビットの符号付き整数。second: 次の8つの作業値のメモリアドレス。各値は16ビットの符号付き整数。first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
この関数に戻り値はありません。
テクスチャや頂点のデータは、各点のxとyがまとめられたインターリーブ状態で届くことがよくあります。
しかし、効率的に処理するには、それらを分けておく必要がよくあります。すべてのxの値を1つのベクトルに、すべてのyの値を別のベクトルにまとめます。
split_coordinates関数を実装します。この関数は、インターリーブされた4つの(x, y)の点を、x座標のベクトルとy座標のベクトルに分けます。
この関数は、次の順序で引数を受け取ります。
xs: 4つのx座標を書き込むバッファーのメモリアドレス。4つの32ビット浮動小数点数。ys: 4つのy座標を書き込むバッファーのメモリアドレス。4つの32ビット浮動小数点数。first: 最初の2つの点のメモリアドレス。{x0, y0, x1, y1}としての4つの32ビット浮動小数点数。second: 次の2つの点のメモリアドレス。{x2, y2, x3, y3}としての4つの32ビット浮動小数点数。first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
この関数に戻り値はありません。
Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。