🧮

AVX-512によるlog(x)の実装で採用しなかった手法

2023/06/30に公開

初めに

今回は「AVX-512によるvpermpsを用いたlog(x)の実装」や計算科学技術特論A(2023)の資料「SIMDの基礎と関数の実装例」の補足です。
「こうやって実装した」ではなく、「こういう方法は取らなかった」という例をいくつか紹介します。異なるCPUやアーキテクチャでは今回採用しなかった方法がよい場面があるかもしれません。作業メモと引き出しを増やすために残します。
補足なので、上記資料は読まれていることを前提にします。

rip相対かrax+offsetか

rip相対によるメモリアドレッシングは常に32ビットオフセットとなります。アクセス範囲が小さい場合はたとえばreg+disp8 offsetの方が命令長が短くなります。

00000000 62F27558B805F6FFFFFF  vfmadd231ps zmm0, zmm1, [rel @L1]{1to16}
0000000A 62F27558B84001               vfmadd231ps zmm0, zmm1, [rax+4]{1to16}

そこでコードを小さくできる後者の方法も実装したのですが、なぜか前者よりも遅くなってしまいました。

- rip相対によるread reg+offsetによるread
log 19.09 20.54

比較したコードはrip相対reg+offset. コード差分.
試した環境はXeon Platinum 8280(turbo boost off)でXeon Gold 6238でも同様の結果。
理由は正直分かりません。古い世代(Sandy Bridge?)の頃だとRIP相対はマイクロフュージョンの恩恵を受けられないみたいな話があった(気がする)ので逆なら分かるのですが。
RIP相対は完全に定数だから最近は何かバイパスされてるんですかね。

vpermpsとvpermi2ps

vpermpsとvpermi2psはどちらもテーブルルックアップに使えますが、vpermpsはZMMレジスタ1個を使うのに対してvpermi2psは2個使います。
当然vpermi2psの方がテーブルサイズが倍になり近似精度を1ビット増やせるのでよいです。
そしてagner.orgのinstruction tablesをみる限りはどちらもレイテンシ3、スループット1で同じです。
それならいつでも後者を使うべきと思ったのですが、vpermi2psを使うと遅くなりました。
vpermi2psはレジスタを2個使うのでインデックスを破壊します。今回はテーブルルックアップを2回するためインデックスを別のレジスタに退避しなければなりません。
退避するためのvmovapsはマイクロopレベルでは消去されてコストは無いと予想したのですが、何か負荷になったのかもしれません。あるいは余計な依存関係?

若干の速度低下はテーブルサイズが大きくなることにより、ローラン展開の次数を下げられたらペイするかと思ったのですが、残念ながらそうではない感じでした。
純粋に精度向上のために採用するのはよいと思います。

aの逆数近似でvrcp14psを使う

aの逆数近似を求めるところでテーブルルックアップの代わりにvrcp14psを使うことも考えました。
しかし、今回は近似値b(~1/a)に対応するlog(b)も必要です。それをするにはvrcp14psの結果の下位(23-L)ビットをマスクしなければなりません。
よりよくするためには(22-L)ビット目に応じて丸める。そのためにはvrndscalepsでMを適切に設定するなども考えられます。
いずれにせよダイレクトにテーブルルックアップするよりは命令数が増えてしまうので採用しませんでした。

log(1/x)=-log(x)を使う

今回は x \sim 1 の分岐は別にしました。挿入結果で示したようにその処理コストは4clkです。
最初に x = 2^n a として \log(x) = n \log(2) + \log(a) として計算するところで x が1より大きく1に近いなら n=0 かつ x=a なのですが、x が1より小さいとそれが成り立ちません。
そこで x < 1 のときに \log(1/x)=-\log(x) を使って x \ge 1 にしてしまい、x \sim 1 専用処理を削除できないと検討しました。

しかし、逆数周りの分岐をその時間内でできそうになかったのと、できたとしてもテーブルルックアップのところで誤差を落とさないようにうまく接続できなかったので今回はあきらめました。もしかしたらうまくできる方法があるのかもしれません。

log(x)の別の近似計算方法

\log(x) の近似計算の別の方法を紹介しましょう。

\log(1+x)=x-x^2/2+x^3/3-x^4/4+\cdots

なので

\log(1-x)=-x-x^2/2-x^3/3-x^4/4+\cdots.

上の式から下の式を引くと

\log((1+x)/(1-x))=\log(1+x)-\log(1-x)=2(x+x^3/3+x^5/5+\cdots).

ここで x=(y-\sqrt{2})/(y+\sqrt{2}) とすると (1+x)/(1-x)=y/\sqrt{2}.

よって \log(y) = \log(\sqrt{2})+2x(1+x^2/3+x^4/5+\cdots).

1 \le y < 2 なので -3+2\sqrt{2} \le x < 3-2\sqrt{2}.

|x| \le 3-2\sqrt{2} = 0.176... より x^2 \le 0.029...

講義で紹介したやりかたはL=4のときで誤差は 1/2^5 = 0.03 に近いので近似多項式としてはほぼ同じ項数にできます。
ただ今回の方式では x(1+x^2/3+\cdots) なので x を掛けるところと x^2 を求めるところで乗算が2回余計に必要です。
更に y から x を求めるところでも乗算や逆数近似が必要になります。そこで、テーブルルックアップが有利な方式を採用しました。

まとめ

今回使わなかった案や試行錯誤を紹介しました。こういった情報は頭の片隅に置きつつも、なんとなく消えてしまうことが多いです。
何か別のところで使えないか検討材料の一つになれば幸いです(自分も含めて)。もしかしたらIntel CPUの世代が変わると(あるいはZen 4だと)結果が変わるかもしれませんね。

GitHubで編集を提案

Discussion