性能評価:予測と分岐・加算とクリア

予測と分岐・加算とクリアのプログラムを応用して、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)
{
    :
}

これまでと同様ですので、説明は省きます。直前と同じ部分は省略して示します。

性能評価

上記プログラムへ /O2 オプションを指定してビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。横軸は比較の種類、縦軸は処理に要した時間(ミリ秒)です。

グラフだけでなく、表も示します。

C++言語で開発した関数とアセンブリ言語で開発した関数ともに2倍の時間を必要としています。両社の性能差は、先のプログラムと同様です。条件が「==」の時のみ、C++言語で開発した関数が高速に動作しています。この違いは、先に説明いた値によるものと想像できます。