はじめてのSSE その3

さて、SSEの話も第三回目。


今回は、SSEの得意な処理と不得意な処理についてです。


もうお気づきかも知れませんが、SSEにはnot(bitwise not)すらないのです。


何故無いのでしょうか?


それは、notが単項演算だからです。SSEは、2つのデータに対して何か演算を行ない、1つのデータを出力するというような流れ作業を想定しています。


1つのデータに対して演算をして一つのデータを出力するというような処理には向いてないのです。
notもその一つなのです。


ではnotを実現するにはどうすればいいのでしょう?


andnotという命令があります。1命令でnotしたあと、andまで出来るというすごい命令です。はっきり言って無用の長物です。notしたいだけなのに!


つまり、andnotを使ってnotをするなら次のようにする必要があります。

  m = m andnot 0xffffffffffffffffffffffffffffffff
  // m = (not m) and 0xffffffffffffffffffffffffffffffff の意味


SSEの命令で書くと次のようになります。

#define Bitboard_Not(b)  (b).m = _mm_andnot_si128((b).m,_mm_set1_epi8(0xff))

_mm_set1_epi8というのは8bitの数値を指定するから、この8bitの値を16個並べて128bitにしてねという命令です。16bitの値を指定してそれを8個並べるなら_mm_set1_epi16、64bitの値を指定してそれを2個並べるなら_mm_set1_epi64と言った具合です。


以上のように128bitのnotをするのに、少なくともSSEの命令が2命令必要だとわかりました。


x64なら、

p[0] = !p[0];
p[1] = !p[1];

と64bit単位にnotを2回しても2命令です。果たしてどちらが速いでしょうか?


これが…、答えにくいのですが、1回だけの処理であれば後者のほうがおそらく速いです。SSEの1命令のほうが、x64の普通のnotより遅いと考えられるからです。将来的には同じぐらいの速度になるかも知れませんが、Core i7 860で実験したところ、SSEで処理するほうが遅かったです。


ところが、SSEのほうのコードは、_mm_set1_epi8というのは定数を生成しているだけですから、もしループのなかで使うような場合や、2回連続で使うような場合は、この定数の生成は1度で済みます。そうすると、SSEで処理したほうが速いという逆転現象が起こります。


ただ、ループのなかで使ってもその定数生成がループ外に行くとは限らないので、なかなか難しいところではあります。ループをunrollして、例えば、次の式

for(int i=0;i<1024;++i)
{
  Bitboard_not(B[i]);
}


これを、次のように書き換えれば、定数生成が1回になることは約束されます。

for(int i=0;i<1024;i+=2)
{
  Bitboard_not(B[i+0]);
  Bitboard_not(B[i+1]);
}


前者に近い書き方でSSEで処理すると1.5倍ぐらい遅くなり、後者の書き方ならSSEで処理したほうが20%ほど速かったです。
※ ソースの微妙な書き方次第で変わる可能性もあるので、以上の話はあくまで参考程度にしてください。


ともかく、生成されたコードを眺めながら両者を状況に応じて使い分けるのがベストなのですが、そんなことをしてられない!という人は、x64モードではnotだけはSSEは使わないほうがトータルでは得かも知れません。


また、x86モードではnotには常にSSEを使っておけばいいと思います。


つづく