性能評価:予測と分岐・設定

予測と分岐・設定のプログラムを応用して、C++言語で記述したプログラムと、アセンブリ言語で記述した関数を利用したときの性能差を検証してみましょう。性能評価は「予測と分岐:設定・64ビット整数型」を用います。
arques.hatenablog.com

性能評価プログラム

C++で記述した関数と、アセンブリ言語で記述した関数を複数回呼び出します。それぞれに要した時間をclock関数で計測し、性能を評価します。common.hやアセンブリ言語で記述した関数は「予測と分岐:設定・64ビット整数型」で示したものと同じです。

#include "..\common.h"  // TEMPLATES

// asmbler関数名: 処理+条件+型
#define T long long
extern "C"
{
    void cmpltq(T*, const T, const size_t, const T);
    void cmpeqq(T*, const T, const size_t, const T);
    void cmpleq(T*, const T, const size_t, const T);
    void cmpneq(T*, const T, const size_t, const T);
    void cmpgeq(T*, const T, const size_t, const T);
    void cmpgtq(T*, const T, const size_t, const T);
}
typedef void (*Dfunc)(T*, const T, const size_t, const T);

extern "C"
Dfunc afunc[] = { cmpeqq, cmpltq, cmpleq, cmpneq, cmpgeq, cmpgtq };
Dfunc cfunc[] = { ccmpeq, ccmplt, ccmple, ccmpne, ccmpge, ccmpgt };

// main, cmpValueと条件に従って比較し、 trueならvalueへ
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 オプションを指定してビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。横軸は比較の種類、縦軸は処理に要した時間(ミリ秒)です。

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

速度差が約4.9倍から約29.7倍あります。アセンブリ言語で開発した関数が高速なのは予想できます。それでも一回で8要素処理することから、最大でも8倍程度高速化できれば十分でしょう。オーバーヘッドを考えると、せいぜい6倍程度が限度と予想します。実際には、最初の3つが約5倍程度で予想に近いです。次の「!=」が約3倍となり予想の半分です。次の二つは、C++言語で開発した関数がかなり低速になるため、約30倍近く高速化されます。ただ、この値はC++言語で開発した関数とアセンブリ言語で開発した関数の相対値です。アセンブリ言語で開発した関数を観察すると、前半の3つと、後半の3つで処理時間は一定です。

C++言語で開発した関数の「>=」と「>」がとても低速なのは何が理由かは不明です。そこで、C++言語の関数が、どのようにアセンブリ言語へ翻訳されているか観察してみます。まず「==」のコードが、どのように変換されるか示します。

template <typename T>
void ccmpeq(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 chgByCondQ.cpp
得られた、コードを示します。

;   COMDAT ??$ccmpeq@_J@@YAXPEA_J_J_K1@Z
_TEXT   SEGMENT
c$ = 8
cmpValue$ = 16
length$ = 24
value$ = 32
??$ccmpeq@_J@@YAXPEA_J_J_K1@Z PROC          ; ccmpeq<__int64>, COMDAT

; 38   :     for (size_t i = 0; i < length; i++)

    xor eax, eax
    test    r8, r8
    je  SHORT $LN3@ccmpeq
$LL9@ccmpeq:

; 39   :     {
; 40   :         if (c[i] == cmpValue)

    cmp QWORD PTR [rcx+rax*8], rdx
    jne SHORT $LN10@ccmpeq

; 41   :         {
; 42   :             c[i] = value;

    mov QWORD PTR [rcx+rax*8], r9
$LN10@ccmpeq:

; 38   :     for (size_t i = 0; i < length; i++)

    inc rax
    cmp rax, r8
    jb  SHORT $LL9@ccmpeq
$LN3@ccmpeq:

; 43   :         }
; 44   :     }
; 45   : }

    ret 0
??$ccmpeq@_J@@YAXPEA_J_J_K1@Z ENDP          ; ccmpeq<__int64>

これでは観察しにくいので、整理してみましょう。

_TEXT   SEGMENT                      コード開始
$ccmpeq PROC                             関数開始
    xor eax, eax
    test    r8, r8
    je  SHORT $LN3@ccmpeq                要素数が0なら終了

                                         for (size_t i = 0; i < length; i++)
$LL9@ccmpeq:                             {
    cmp QWORD PTR [rcx+rax*8], rdx           if (c[i] == cmpValue)
    jne SHORT $LN10@ccmpeq                   {
    mov QWORD PTR [rcx+rax*8], r9                c[i] = value;
                                             }
$LN10@ccmpeq:
    inc rax                              }
    cmp rax, r8                          終了か?
    jb  SHORT $LL9@ccmpeq                    No、ループの先頭へ
$LN3@ccmpeq:
    ret 0
ccmpeq@ ENDP                             関数終了
_TEXT   ENDS                         コード終了

ごく単純に、C++のソースがアセンブリ言語へ変換されています。

次に「>」のコードが、どのように変換されるか示します。

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;
        }
    }
}

コンパイル結果を整理して示します。

_TEXT   SEGMENT                      コード開始
ccmpgt  PROC                             関数開始
    xor eax, eax
    test    r8, r8
    je  SHORT $LN3@ccmpgt                要素数が0なら終了

                                         for (size_t i = 0; i < length; i++)
$LL9@ccmpgt:                             {
    cmp QWORD PTR [rcx+rax*8], rdx           if (c[i] > cmpValue)
    jle SHORT $LN10@ccmpgt                   { 
    mov QWORD PTR [rcx+rax*8], r9                c[i] = value;
                                             }
$LN10@ccmpgt:
    inc rax                              }
    cmp rax, r8                          終了か?
    jb  SHORT $LL9@ccmpgt                    No、ループの先頭へ
$LN3@ccmpgt:
    ret 0
ccmpgt  ENDP                             関数終了
_TEXT   ENDS                         コード終了

ごく単純に、C++のソースがアセンブリ言語へ変換されています。
「==」のコードは、cmp命令に続くジャンプ命令がjneです。つまり等しくないときジャンプします。「>」のコードでは、cmp命令に続くジャンプ命令がjleです。つまり以下のときジャンプします。基本的に、同じようなコードへコンパイルされているため、静的な原因ではなさそうです。そこで、比較値に依存するのではないかと考え、cmpValue を 8000 へ変更してみました。すると、以下のような結果になります。

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

先の表と比べると、分布が変化していることに気づくでしょう。cmpValue をほかの値にも変更してみたところ、処理速度に変化が見られます。このことから、渡される値によって、C++言語で開発した関数はダイナミックステップが変化するのが原因、アセンブリ言語で開発した関数はマスクレジスターの値が変わるため実際に読み書きするデータ量が変化するのが原因と想像できます。
いずれにしても、アセンブリ言語で開発した関数が高速化し、一定範囲で処理が完了しています。このことから、予想は大きく外れていません。性能向上の考察は、この程度で収めます。