Denver の最適化機能を調べる

NVIDIA 社の開発した Denver という 64bit ARM CPU は、ARM 機械語を最適化する機能を持っています。これをいくらか見てみようと思います

コード https://bitbucket.org/fixstars/blog/src/d2bfdedb9bdee4d994016f3583d1640425830c4c/small_test/denver-ipc.c?at=master

実行結果 : https://bitbucket.org/fixstars/blog/src/d2bfdedb9bdee4d994016f3583d1640425830c4c/small_test/denver-log.txt?at=master

Nexus9 の Android 上 で 64bit ELF を動かしています。コンパイルは、TADPを入れた環境で、

$ /usr/local/cuda-android/android-ndk-r10c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-gcc –sysroot /usr/local/cuda-android/android-ndk-r10c/platforms/android-21/arch-arm64 -fPIE -pie -O2 -g

などのようにしてください。

IPC は、 perf_event の PERF_COUNT_HW_CPU_CYCLES、PERF_COUNT_HW_INSTRUCTIONS から計算しています。

大量のnop

ループに大量の nop を入れます。

最初は IPC1.1ぐらいですが、二回目からは29 ぐらいになっています。

このことから、パフォーマンスカウンタの実行命令数はARM命令単位でカウントしていることと、IPCがスペック上の理論値 7 を超えていることから、nop は消えてしまうことがわかります。

無限にIPC増やしていけそうですが、どこかでリミットがかかっているようで、無限に増えるわけではないようです(組み合わせによっては52ぐらい出るのですがそれ以上確認できず)。

無駄な命令(整数)

いくらか演算したあとに、レジスタの値を初期化します

これも命令が消えているように見えますね。無駄なaddは消えることがわかります。

ただ、ループARM命令の最適化はC言語の最適化よりも難しい処理で、割りこみルーチンがレジスタを読み書きするというのを考えると、割り込みが入る可能性のある箇所では、レジスタ単位でもとのプログラムの値を正しく再現する必要があります。おそらく、そういう理由があるので、プログラムが複雑になると、最適化はされないようです。

例えば、

のように、レジスタに定数を入れる処理をすぐ隣に置くと、命令は削除されるようですが、

のように、レジスタの初期化をループ外に出したり、

のように、途中に条件分岐を入れたりすると、命令は削除されないようです。

無駄な命令(浮動小数)

浮動小数演算はかなりシンプルな例でも削除されないようです。

は、IPCが3.8ぐらいになります。

ARMv8 はよく知らないのでなんとも言えないですが、FPUは入力値やモード設定によって例外出す可能性があるので、命令を削除する最適化は入っていないのではないかという気がします。

最大IPC

NVIDIA社のblogにあるDenverの紹介を見ると

http://blogs.nvidia.com/blog/2014/08/11/tegra-k1-denver-64-bit-for-android/

JSR, ICU0, ICU1, FP0, FP1, LS0, LS1 が並んでいるように見えます。

名前からすると、ジャンプx1、整数x2、浮動小数x2、ロードストアx2 で 7way だと推測されます

いくらか試した範囲だと、↓ のようにすれば、IPC 5.4 になるようです。

5.4というのもよくわからない値ですね。あと整数命令は3つ入れたほうがIPCが上がるようです。 nop のところで書いたように消えたARM命令もカウントされてしまうので、何かそういうのが効いてるのかもしれません。

最適化がかかっていない最初の実行ではIPC 1程度になっています。

一応依存の無いようにARM命令並べていますが、それではIPC上がらないようなので、ARMのハードウェアデコーダは、スループット1命令なのではないかと予想されます

ついでにいくつか試した値を書いておくと、

これが IPC8.8。32bit ARMは連続するアドレスを複数レジスタにロードする命令があるので、それに近いものに変換されてるように見えます。

これが 2.4。 謎の値ですね。

これが 3.8。 整数加算は4並列できる。

and は4.1。3.8との差は謎ですね。

整数演算のスケジューリング

Denver は、in-order でも高IPCという宣伝がされているので、なんらかの命令スケジューリングがされていると予想されます。実際、以下のようなコードを入れると、

何回か実行すると、IPC 3 ぐらいになるので、スケジューリングが行われていることが確認できます。

上の関数では、計測用に32回関数を実行していて、9〜23回目の処理は、スケジューリングができないようにしています。

結果は、上のとおりです。

メモリアクセスを投機実行しているはずで、9〜23回目では、これは失敗します。失敗したあと、しばらく0.75ぐらいが続いて、またしばらくすると、3程度に戻ります。

二回目のテストでは、

このように、投機実行に失敗しているのを感じさせない値になります。よほど特殊な命令を実装しているのでない限り、これをひとつのループで実現することはできないと思うので、

  • ポインタ重なったときのループ
  • ポインタ違うときのループ

の2バージョンのループを作っているのではないかと思います。

浮動小数演算のスケジューリング

浮動小数で同じような処理を実行した場合の結果が、

このようになります。

整数では、投機実行失敗後、しばらくするとIPC3 に戻りましたが、浮動小数では、1回遅くなると、24回目以降も遅くなったままなので、投機実行をやめているように見えます。

unroll

ループ中では依存があり、ループのイテレーション間では依存が無いという処理です。

これはIPC3.1程度になるので、アンロールかそれに相当する最適化をしていると予想されます。

間接分岐

関数ポインタを付けかえて間接分岐します。

使う関数の数(noarg_func_ptr = ret するまでの数)を変えると、

関数の数 1 2 3 4 5 6 7 8 9
IPC 2.0 1.2 0.7 1.1 0.6 0.7 0.6 2.0 0.5

のようになります。最適化の結果なのか、BTBのway構成の結果なのかはちょっとよくわからないですね…

感想

あまり派手なことはしていないですが、地道な作業はひととおりやっているという印象を受けます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です