非常にニッチな実装の話題ですが先日必要になったのでメモ書き程度に。
ベクトルの各要素に対して8bit要素のmin/maxを計算したい場合、専用のmin/max命令( vpminub
/ vpmaxub
など)を使うのが普通だと思います。
void f(__m512i x, __m512i y, __m512i &result) {
result = _mm512_min_epu8(x, y);
}
ほとんどの場合はこれで十分なはずです。
素直な解法で発生した問題点
上記の専用命令は非常に高速ですが、強いて言うなら命令レベルの並列性がないという問題があります。Agner fogのInstruction tablesによると、vpmin/max系の命令はレイテンシ1、スループット1、実行portは0のみのようだと伺えます。
つまり、min/maxを大量に行う場合は、ここが性能上のボトルネックになりかねません。
port0の圧迫を改善する
しょうがないのでvpminub/vpmaxubを別の命令で代替できないかを考えます。
もちろん、代替実装にport0を使うのは本末転倒です。また、代替実装自体が重すぎるのも問題があるため、できるだけ高速な実装にする必要があります。
今回は次のような実装をひねり出しました。
vpcmpub k1, zmm0, zmm1, 1 ; a, b を比較
vpsubb zmm30 {k1} {z}, zmm0, zmm1 ; a - b (比較結果がfalseなら0)
vpaddb zmm0, zmm30, zmm1 ; (a - b) + b = a (比較結果がfalseなら0 + b = b)
ゼロ化マスクを使うことで比較結果に応じて演算結果を0にしてから「足し戻す」ことで、比較結果に応じた最終的な値の分岐を実現しました。ここの実装は悩ましいものがあり、
- blendが直接的だがport0, 5しか使えなくなる上に遅いらしい(レイテンシ3もある)
- マスクレジスタによる分岐も暗黙のblendが入るらしいので同様に回避したい
- 同様のアルゴリズムならxorが自然かなと思いきやepi8のものが存在しない
- 要素が32bitの場合は可能かもしれません
などなど、限られた命令でやりくりする必要があり、かなり違和感のある実装です……。この命令列は全体としてはレイテンシ5になるため専用命令よりずっと遅いですが、(比較以外は)実行ポートとして1, 5も使えます。
min/maxのうちこれで一部を置き換えてやることで、現行CPU(Skylake-SP)においては若干のスループット向上が見られました。