スカラーコードは、ある条件に応じて分岐するために、さまざまな命令が設定するフラグに頼ります。 しかし、パックされた値は、1つではなく多くの値を並列に表します。 1つの条件が、あるレーンでは成り立たず、別のレーンでは成り立つことがあります。
そのため、SIMDコードはデフォルトで_ブランチレス_です。
フラグに頼る代わりに、パック比較は通常、デスティネーションオペランドにマスクを生成します。
各レーンについて、比較は、条件が真のときはそのレーン全体を1で埋め、偽のときは0で埋めます。
符号付き整数として読むと、真のレーンは-1になり、偽のレーンは0になります。
このマスクは、その後、ビット演算と組み合わせて特定のレーンをフィルタリングできます。
スカラーのcmpは、さまざまなフラグを同時に設定するために使われるという意味で汎用的です。
別の命令が、それらのフラグを消費して、分岐したり計算を行ったりできます。
しかし、パック比較は条件をチェックすると同時にマスクを計算するため、汎用的ではありません。 比較には、テストする正確な条件を与える必要があります。
これには2つの方法があります。
eq、より大きいにはgtです。
その他のバリエーションは、これらのいずれかの結果を組み合わせて作ります。整数比較で特定の条件サフィックスを使う点を除けば、構文はすでに見てきたのと同じ構造に従います。
p + cmp + 条件 + サイズ(b、w、d、またはq)です。cmp + p + サイズ(sまたはd)です。
条件は、追加のオペランドとして即値で渡します。すでに述べたように、整数比較には等価と「より大きい」の2種類しかありません。
| instruction | description |
|---|---|
pcmpeqb、pcmpeqw、pcmpeqd、pcmpeqq
|
レーンごとの等価 |
pcmpgtb、pcmpgtw、pcmpgtd、pcmpgtq
|
レーンごとの符号付き「より大きい」 |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
より小さい比較を作るには、オペランドを入れ替えてgtを使います。つまりa < b == b > aです。
比較は符号付きであることに注意してください。 符号なし比較を行うには、両方のオペランドの最上位ビットを反転します。 これは、最上位ビットだけがセットされたマスクとのXORで行えます。
便利なイディオムが2つあります。
たとえば、
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
すべて0のマスクとすべて1のマスクは、それぞれ「どこでも偽」と「どこでも真」をエンコードするためによく使われます。
これらは、パックされた0やパックされた-1を表すのにも使えます。これらはよく使われる番兵値です。
たとえば、文字列の終わりを示すNULは0です。
浮動小数点のレーンでは形が異なります。cmpps(64ビットレーンの場合はcmppd)という1つの命令を使い、条件は即値として指定します。
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASMには、正しい即値に対応する、覚えやすい擬似命令もあります。
以下ではすべて、pxのxはs(32ビット浮動小数点数)またはd(64ビット浮動小数点数)です。
| pseudo-op | immediate | comparison |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | aがNaN、またはbがNaN |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | aもbもNaNでない |
マスクは、条件の結果をエンコードします。 これを使うと、その条件に従って、2組の値のどちらかをレーンごとに選べます。 マスクが真のレーンでは一方の値から、偽のレーンではもう一方の値から、そのレーンを取ります。
; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0 ; xmm0 holds the mask, keep a copy
pand xmm2, xmm3 ; xmm2 = a AND mask: lanes of a where mask is true
pandn xmm0, xmm4 ; xmm0 = NOT mask AND b: lanes of b where mask is false
por xmm2, xmm0 ; combine the two halves
ここではpandnの非対称性が役立ちます。マスクはデスティネーションに置かれ、否定され、1つの命令でbから選択します。
このパターンは、ブランチレス選択のパック形式です。
すべてのレーンが計算され、どの値が生き残るかを決めるのはマスクだけで、どこにもjccは現れません。
同じ選択を直接行う命令もあり、マスクレジスタから要素ごとに1ビットを読み取ります。 これらはブレンド命令と呼ばれます。
| instruction | element | mask source |
|---|---|---|
pblendvb |
バイト | 暗黙のxmm0
|
blendvps |
32ビットレーン | 暗黙のxmm0
|
blendvpd |
64ビットレーン | 暗黙のxmm0
|
最初の命令は整数の構文に従い、残りの2つは浮動小数点の構文に従うことに注意してください。 しかし、これらの命令は生のバイトを選択するだけなので、どれも整数と浮動小数点の両方で使えます。
各要素について、ブレンド命令は、対応するマスク要素の最上位ビットがクリアならデスティネーションを保持し、セットされていればソースを取ります。
参照されるのはその最上位ビットだけであり、比較マスクはその条件を満たします。そのレーンはすべて1かすべて0だからです。
マスクレジスタは常にxmm0で、これは暗黙です。
movaps xmm0, [rel mask] ; the selecting mask must be in xmm0
movaps xmm1, [rel b] ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a] ; source: taken where the mask bit is set
また、pblendvbを使って、他の任意のサイズの比較マスクからレーンを選択することもできます。
真のレーンのバイトはすべて1なので、pblendvbはそれらをすべて選択します。
これらの命令は、行う操作(blend)のあとにvを付けます。
このvは_variable_を表します。選択が静的ではなく、レジスタに依存するからです。
vのないバリエーションもあり、こちらは即値に従って選択します。
これらも同じパターンに従い、即値のビットiがセットされていればレーンiを選択します。
SIMDコードは強力ですが、スカラーコードの柔軟性の多くを欠いています。 多くの場面で、パックレジスタからスカラー命令の世界へ戻す必要があります。
movmsk命令の一族は、この2つの世界の橋渡しをします。
これらの命令は、各レーンの最上位ビットを汎用レジスタに取り出します。
| instruction | gathers | result width |
|---|---|---|
pmovmskb |
16バイトそれぞれの最上位ビット | 16ビット |
movmskps |
4ダブルワードそれぞれの最上位ビット | 4ビット |
movmskpd |
2クワッドワードそれぞれの最上位ビット | 2ビット |
比較のあとに使うと、セットされた各ビットが「真」のレーンを、クリアされた各ビットが「偽」のレーンを表します。
この結果は、その後、通常どおりスカラー命令で操作できます。
たとえば、popcntは一致した数を数え、tzcntは最初のものを探します。
汎用レジスタは32ビット幅でも64ビット幅でもかまいません。
スカラー命令testにはパック版もあり、それがptestです。
スカラーの対応命令と似ており、2つのオペランドの間でAND演算を行いますが、それらを変更しません。
testと違い、ptestはANDN演算も行い、最初のオペランドを否定します。
したがって、ptestはpandとpandnの非破壊版で、結果に応じてフラグを設定すると考えられます。
この2つの命令とほぼ同じように、ptestはSIMDレジスタ全体を1つのレーンとして扱うため、サイズプレフィックスを取りません。
AND演算の結果が0ならZFがセットされ、ANDN演算の結果が0ならCFがセットされます。
つまり、ptestはすべて1のマスクとすべて0のマスクの両方をチェックするのに使えます。
ptestを使うと、そのレジスタがすべて0の場合にのみZFがセットされます。
これは、レジスタが0かどうかをチェックするために自身でtestを使うという、よくあるスカラーのイディオムを再現します。ptestを使うと、そのレジスタがすべて1の場合にのみCFがセットされます。
また、そのレジスタがすべて0の場合にのみZFをセットするので、両方のマスクを一度にチェックできます。pxor xmm0, xmm0 ; all zeros
pcmpeqb xmm1, xmm1 ; all ones
pcmpeqb xmm2, xmm2
ptest xmm0, xmm0 ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1 ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1 ; CF is set only if xmm2 is all ones
ptestの結果は、通常どおり、分岐や、setccやcmovccのようなブランチレス命令で使えます。
学校の採点ステーションを担当し、クラスの結果をブロック単位で採点します。
各ブロックには4つの結果が入っており、ステーションはブロック内のすべての結果に同じ演算を適用します。 スコアは32ビットの浮動小数点数です。 いくつかのステップではマスクを扱います。マスクとは4つのレーンからなるブロックで、各レーンはすべて1(その結果に対する「はい」)か、すべて0(「いいえ」)のどちらかです。
タスクは5つあります。 オペランドはメモリアドレスを通じて受け取ります。 タスクによっては答えを結果のアドレスに書き込み、そうでないものは直接返します。
この演習のメモリアドレスはすべて16バイトに整列されています。
この演習の計算は、SIMD命令を使って行ってください。
最初のステップでは、各結果をしきい値と比較して評価します。 結果のスコアがしきい値より厳密に大きいとき、その結果は基準を満たします。 しきい値以下のスコアは基準を満たしません。
flag_above_threshold関数を実装します。この関数は、しきい値を超えるスコアにはすべて1のレーンを、それ以外にはすべて0のレーンを設定したマスクを作成します。
この関数は、次の順序で引数を受け取ります。
result: 4つのマスクレーンが書き込まれるバッファのメモリアドレス。scores: スコアのメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。thresholds: 各レーンのしきい値のメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
この関数に戻り値はありません。
別のレポートでは、満点の結果、つまり到達しうる最高得点に達した結果に注目します。
flag_perfect関数を実装します。この関数は、最高得点と等しいスコアにはすべて1のレーンを、それ以外にはすべて0のレーンを設定したマスクを作成します。
この関数は、次の順序で引数を受け取ります。
result: 4つのマスクレーンが書き込まれるバッファのメモリアドレス。scores: スコアのメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。maxima: 各レーンの最高得点のメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
この関数に戻り値はありません。
各スコアには1から3までの順位が付きます。
50.0以下のスコアは順位1。assign_ranks関数を実装します。この関数は各スコアの順位を書き込みます。
合格しきい値と順位の値は、メモリ内のパックされた定数として定義してください。 前の2つのタスクの関数を再利用できます。スコアがしきい値を超えていれば少なくとも順位2で、最高得点と等しければ順位3です。
この関数は、次の順序で引数を受け取ります。
result: 4つの順位が書き込まれるバッファのメモリアドレス。各順位は32ビットの符号なし整数です。scores: スコアのメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。maxima: 各レーンの最高得点のメモリアドレス。4つの32ビット正規浮動小数点数(NaNにはなりません)が入っています。scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
この関数に戻り値はありません。
1年の間に、各生徒は合計の順位を積み上げていきます。 ステーションは、補習を何クラス実施するかを計画するために、生徒全員のうち合格しきい値を下回る順位がいくつあるかを集計します。
count_failures関数を実装します。この関数は、すべてのブロックを通して、合格しきい値より厳密に小さい順位がいくつあるかを返します。
しきい値は4つの同一のレーンからなるブロックとして与えられるので、一度読み込んでおけば、すべてのブロックで再利用できます。
この関数は、次の順序で引数を受け取ります。
ranks: 順位のメモリアドレス。4レーンからなるブロックが整数個分あり、各順位は32ビットの符号なし整数です。block_count: 4レーンブロックの数。常に0より大きくなります。pass_threshold: 合格しきい値のメモリアドレス。4つの同一の32ビット整数が入っています。ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
この関数は、符号付き32ビット整数として個数を返します。
記録を保管する前に、ステーションは生徒全員に問題がないかどうかを確認します。どのブロックでも不合格の結果が1つもなければ、合格です。
all_passed関数を実装します。この関数は、全員が合格していれば1を、そうでなければ0を返します。
ある生徒のfailing配列の対応するレーンがすべて0であれば、その生徒は合格です。
この関数は、次の順序で引数を受け取ります。
failing: 不合格マスクのメモリアドレス。4レーンからなるブロックが整数個分あり、各レーンはすべて1かすべて0のいずれかです。block_count: 4レーンブロックの数。常に0より大きくなります。failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
この関数は、1か0のいずれかを符号付き32ビット整数として返します。
Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。