予測と分岐・加算とクリアのプログラムを応用して、C++言語で記述したプログラムと、アセンブリ言語で記述した関数を利用したときの性能差を検証してみましょう。性能評価は「予測と分岐・加算とクリア・64ビット浮動小数点型」を用います。
arques.hatenablog.com
64ビット浮動小数点型
性能評価プログラム
C++で記述した関数と、アセンブリ言語で記述した関数を複数回呼び出します。それぞれに要した時間をclock関数で計測し、性能を評価します。common.hやアセンブリ言語で記述した関数は「予測と分岐:設定・64ビット整数型」で示したものと同じです。
#include "..\common.h" // TEMPLATES // asmbler関数名: 処理+条件+型 #define T double extern "C" { void addzltpd(T*, const T, const size_t, const T); void addzeqpd(T*, const T, const size_t, const T); void addzlepd(T*, const T, const size_t, const T); void addznepd(T*, const T, const size_t, const T); void addzgepd(T*, const T, const size_t, const T); void addzgtpd(T*, const T, const size_t, const T); } typedef void (*Dfunc)(T*, const T, const size_t, const T); extern "C" Dfunc afunc[] = { addzeqpd, addzltpd, addzlepd, addznepd, addzgepd, addzgtpd }; Dfunc cfunc[] = { caddzeq, caddzlt, caddzle, caddzne, caddzge, caddzgt }; // main int main(void) { const size_t ArrLen = 32768; static_assert(ArrLen % 16 == 0, "number of elements must be an integral multiple of 16."); const T cmpValue = 8, value = 12; try { T* a = (T*)_mm_malloc(sizeof(T) * ArrLen, 64); T* c = (T*)_mm_malloc(sizeof(T) * ArrLen, 64); clock_t start; for (int i = 0; i < sizeof(cfunc) / sizeof(*cfunc);i++) { #define LOOPS 10000 cout << "---[" << i << "]--- " << endl; init(a, c, ArrLen); start = clock(); for (int j = 0; j < LOOPS;j++) cfunc[i](c, cmpValue, ArrLen, value); print_elTime("cpp: ", start, clock()); start = clock(); for (int j = 0; j < LOOPS;j++) afunc[i](a, cmpValue, ArrLen, value); print_elTime("asm: ", start, clock()); verify(a, c, ArrLen); } _mm_free(a); _mm_free(c); } catch (char* str) { cerr << str << endl; } return 0; }
性能評価
上記プログラムへ /O2 オプションを指定してビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。横軸は比較の種類、縦軸は処理に要した時間(ミリ秒)です。
グラフだけでなく、表も示します。
次に「>」のコードが、どのように変換されるか示します。
template <typename T> void ccmpgt(T* c, const T cmpValue, const size_t length, const T value) { for (size_t i = 0; i < length; i++) { if (c[i] > cmpValue) // "!<=" -> ">" { c[i] = value; } } }
このコードを、下記のオプションでコンパイルします。
cl /c /FAs /O2 /EHsc addZPD.cpp
得られた、コードを示します。
細かく説明しませんが、xmmレジスターを用い、一回で2要素を処理します。また、配列の要素数が2の整数倍でないときの処理も組み込まれています。データー処理の命令はdouble対応の命令が用いられていますが、データー移動などや論理演算の命令ではfloat用のものが使われています。特に問題はないでしょうが、アセンブリコードを参照する人は混乱するでしょう。もっとも、普通のエンジニアが、コンパイラーが吐き出したアセンブリコードを参照することは多くないでしょう。
ベクトル化に挑戦していますが、冗長なコードが目立ちます。
オプションの/archにAVX512を指定したときのコードも示します。同じソースを、下記のオプションでコンパイルします。
cl /c /FAs /arch:AVX512 /O2 /EHsc addZPD.cpp
得られた、コードを示します。先の例は、コンパイラーが出力したコードを示しました。それでは分かりにくいので、整理して示します。
/arch:AVX512オプションを指定すると、zmmレジスターを使用し、一回で8要素を処理します。
32ビット浮動小数点型
性能評価プログラム
同様の性能評価を32ビット浮動小数点配列へ実施したものを示します。以降に、呼び出し側のソースリストの異なる部分を示します。
#include "..\common.h" // TEMPLATES // asmbler関数名: 処理+条件+型 #define T float extern "C" { void addzltps(T*, const T, const size_t, const T); void addzeqps(T*, const T, const size_t, const T); void addzleps(T*, const T, const size_t, const T); void addzneps(T*, const T, const size_t, const T); void addzgeps(T*, const T, const size_t, const T); void addzgtps(T*, const T, const size_t, const T); } typedef void (*Dfunc)(T*, const T, const size_t, const T); extern "C" Dfunc afunc[] = { addzeqps, addzltps, addzleps, addzneps, addzgeps, addzgtps }; : // main int main(void) { : }
これまでと同様ですので、説明は省きます。直前と同じ部分は省略して示します。