C++で機械語(AVX命令)を実行(Windows ver.)

C++言語を使用している際に、直接アセンブリで記述した機械語(AVX命令)を実行します。

はじめに

本例はfloat型の配列内から一番小さな値を探します。値を探す処理を機械語で記述します。機械語x86、AVX、AVX-512へ対応させた3つの関数を用意します。
直前の
arques.hatenablog.com
を少し変更して、動的に機械語を差し替えます。

アセンブリコード

まず、アセンブリコードを示します。このアセンブリ言語で記述した関数はC++から呼び出されることを前提とします。このソースコード機械語を得るためのもので実際に使用するものではありません。

        public x86
x86     proc
        mov     rdx, rcx                ; src addr
        mov     rcx, r8                 ; length

loop_f:
        movss   xmm0, dword ptr [rdx]   ; load a[i]
        comiss  xmm1, xmm0              ; if min < a[i]
        jb      label1                  ;   goto label1
        movaps  xmm1, xmm0              ; min
label1:
        lea     rdx, dword ptr [rdx+4]  ; next addr
        loop    loop_f

        movaps  xmm0, xmm1              ; return value

        ret
x86     endp

;---------------------
        public Avx
Avx     proc
        mov     r11, rcx                ; src addr
        mov     rcx, r8                 ; length
        sar     rcx, 3                  ; length/=8; (8float=32bytes=256bit)

        vshufps ymm0, ymm1, ymm1, 0     ; ymm0 = initial value

loop_f:
        vminps  ymm0, ymm0, ymmword ptr [r11]
        lea     r11,  qword ptr[r11+32] ; next addr
        loop    loop_f


        vextractf128 xmm1, ymm0, 1      ; reduce ymm0
        vminps  xmm1, xmm1, xmm0
        vshufps xmm0, xmm1, xmm1, 14
        vminps  xmm1, xmm0, xmm1
        vshufps xmm0, xmm1, xmm2, 1
        vminss  xmm0, xmm1, xmm0

        ret
Avx     endp

;---------------------
        public Avx512
Avx512  proc
        mov     r11, rcx                ; src addr
        mov     rcx, r8                 ; length
        sar     rcx, 4                  ; length/=16; (16int=64bytes=512bit)

        vbroadcastss zmm0, xmm1         ; zmm0 = initial value
loop_f:
        vminps  zmm0, zmm0, zmmword ptr [r11]
        lea     r11,  qword ptr[r11+64] ; next addr
        loop    loop_f

        vextractf32x8 ymm1, zmm0, 1     ; reduce zmm0
        vminps      ymm3, ymm0, ymm1
        vextractf128 xmm2, ymm3, 1
        vminps      xmm1, xmm3, xmm2
        vpermilps   xmm0, xmm1, 14
        vminps      xmm2, xmm1, xmm0
        vpermilps   xmm1, xmm2, 1
        vminss      xmm0, xmm2, xmm1

        ret
Avx512 endp

_TEXT   ends
        end

機械語

このソースコードml64コマンドでアセンブルしobjファイルを得ます。そのobjファイルをdumpbin コマンドへ与え、どのような機械語へ変換されるか調べます。

コマンド:
ml64 /c minAsm.asm
dumpbin /DISASM minAsm.obj

x86:
  000: 48 8B D1           mov         rdx,rcx
  003: 49 8B C8           mov         rcx,r8
  006: F3 0F 10 02        movss       xmm0,dword ptr [rdx]
  00A: 0F 2F C8           comiss      xmm1,xmm0
  00D: 72 03              jb          012
  00F: 0F 28 C8           movaps      xmm1,xmm0
  012: 48 8D 52 04        lea         rdx,[rdx+4]
  016: E2 EE              loop        006
  018: 0F 28 C1           movaps      xmm0,xmm1
  01B: C3                 ret

Avx:
  01C: 4C 8B D9           mov         r11,rcx
  01F: 49 8B C8           mov         rcx,r8
  022: 48 C1 F9 03        sar         rcx,3
  026: C5 F4 C6 C1 00     vshufps     ymm0,ymm1,ymm1,0
  02B: C4 C1 7C 5D 03     vminps      ymm0,ymm0,ymmword ptr [r11]
  030: 4D 8D 5B 20        lea         r11,[r11+20h]
  034: E2 F5              loop        02B
  036: C4 E3 7D 19 C1 01  vextractf128 xmm1,ymm0,1
  03C: C5 F0 5D C8        vminps      xmm1,xmm1,xmm0
  040: C5 F0 C6 C1 0E     vshufps     xmm0,xmm1,xmm1,0Eh
  045: C5 F8 5D C9        vminps      xmm1,xmm0,xmm1
  049: C5 F0 C6 C2 01     vshufps     xmm0,xmm1,xmm2,1
  04E: C5 F2 5D C0        vminss      xmm0,xmm1,xmm0
  052: C3                 ret

Avx512:
  053: 4C 8B D9           mov         r11,rcx
  056: 49 8B C8           mov         rcx,r8
  059: 48 C1 F9 04        sar         rcx,4
  05D: 62 F2 7D 48 18 C1  vbroadcastss zmm0,xmm1
  063: 62 D1 7C 48 5D 03  vminps      zmm0,zmm0,zmmword ptr [r11]
  069: 4D 8D 5B 40        lea         r11,[r11+40h]
  06D: E2 F4              loop        063
  06F: 62 F3 7D 48 1B C1  vextractf32x8 ymm1,zmm0,1
       01
  076: C5 FC 5D D9        vminps      ymm3,ymm0,ymm1
  07A: C4 E3 7D 19 DA 01  vextractf128 xmm2,ymm3,1
  080: C5 E0 5D CA        vminps      xmm1,xmm3,xmm2
  084: C4 E3 79 04 C1 0E  vpermilps   xmm0,xmm1,0Eh
  08A: C5 F0 5D D0        vminps      xmm2,xmm1,xmm0
  08E: C4 E3 79 04 CA 01  vpermilps   xmm1,xmm2,1
  094: C5 EA 5D C1        vminss      xmm0,xmm2,xmm1
  098: C3                 ret

C++コード

この機械語C++で記述したソースコードへ取り込みます。

#include <iostream>
#include <iomanip>
#include <windows.h>

using namespace std;

// alloc executale memory
void* allocMem(size_t size[])
{
    size_t maxSize = 0;     // 最大サイズを求める
    for (int i = 0; i < sizeof(size) / sizeof(size[0]); i++)
    {
        maxSize = size[i] > maxSize ? size[i] : maxSize;
    }

    void* mem = VirtualAlloc(
        NULL, maxSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);

    if (!mem)
        throw "VirtualAlloc failed.";

    return mem;
}

// free memory
void freeMem(void* mem)
{
    if (!VirtualFree(mem, 0, MEM_RELEASE))
        throw "VirtualFree failed";
}

int main(void)
{
    unsigned char x86[] = {
        0x48, 0x8B, 0xD1,                   // mov         rdx,rcx
        0x49, 0x8B, 0xC8,                   // mov         rcx,r8
        0xF3, 0x0F, 0x10, 0x02,             // movss       xmm0,dword ptr [rdx]
        0x0F, 0x2F, 0xC8,                   // comiss      xmm1,xmm0
        0x72, 0x03,                         // jb          012
        0x0F, 0x28, 0xC8,                   // movaps      xmm1,xmm0
        0x48, 0x8D, 0x52, 0x04,             // lea         rdx,[rdx+4]
        0xE2, 0xEE,                         // loop        006
        0x0F, 0x28, 0xC1,                   // movaps      xmm0,xmm1
        0xC3                                // ret
    };
    unsigned char Avx[] = {
        0x4C, 0x8B, 0xD9,                   // mov         r11,rcx
        0x49, 0x8B, 0xC8,                   // mov         rcx,r8
        0x48, 0xC1, 0xF9, 0x03,             // sar         rcx,3
        0xC5, 0xF4, 0xC6, 0xC1, 0x00,       // vshufps     ymm0,ymm1,ymm1,0
        0xC4, 0xC1, 0x7C, 0x5D, 0x03,       // vminps      ymm0,ymm0,ymmword ptr [r11]
        0x4D, 0x8D, 0x5B, 0x20,             // lea         r11,[r11+20h]
        0xE2, 0xF5,                         // loop        02F
        0xC4, 0xE3, 0x7D, 0x19, 0xC1, 0x01, // vextractf128 xmm1,ymm0,1
        0xC5, 0xF0, 0x5D, 0xC8,             // vminps      xmm1,xmm1,xmm0
        0xC5, 0xF0, 0xC6, 0xC1, 0x0E,       // vshufps     xmm0,xmm1,xmm1,0Eh
        0xC5, 0xF8, 0x5D, 0xC9,             // vminps      xmm1,xmm0,xmm1
        0xC5, 0xF0, 0xC6, 0xC2, 0x01,       // vshufps     xmm0,xmm1,xmm2,1
        0xC5, 0xF2, 0x5D, 0xC0,             // vminss      xmm0,xmm1,xmm0
        0xC3                                // ret
    };
    unsigned char Avx512[] = {
        0x4C, 0x8B, 0xD9,                   // mov         r11,rcx
        0x49, 0x8B, 0xC8,                   // mov         rcx,r8
        0x48, 0xC1, 0xF9, 0x04,             // sar         rcx,4
        0x62, 0xF2, 0x7D, 0x48, 0x18, 0xC1, // vbroadcastss zmm0,xmm1
        0x62, 0xD1, 0x7C, 0x48, 0x5D, 0x03, // vminps      zmm0,zmm0,zmmword ptr [r11]
        0x4D, 0x8D, 0x5B, 0x40,             // lea         r11,[r11+40h]
        0xE2, 0xF4,                         // loop        070
        0x62, 0xF3, 0x7D, 0x48, 0x1B, 0xC1, // vextractf32x8 ymm1,zmm0,1
        0x01,
        0xC5, 0xFC, 0x5D, 0xD9,             // vminps      ymm3,ymm0,ymm1
        0xC4, 0xE3, 0x7D, 0x19, 0xDA, 0x01, // vextractf128 xmm2,ymm3,1
        0xC5, 0xE0, 0x5D, 0xCA,             // vminps      xmm1,xmm3,xmm2
        0xC4, 0xE3, 0x79, 0x04, 0xC1, 0x0E, // vpermilps   xmm0,xmm1,0Eh
        0xC5, 0xF0, 0x5D, 0xD0,             // vminps      xmm2,xmm1,xmm0
        0xC4, 0xE3, 0x79, 0x04, 0xCA, 0x01, // vpermilps   xmm1,xmm2,1
        0xC5, 0xEA, 0x5D, 0xC1,             // vminss      xmm0,xmm2,xmm1
        0xC3                                // ret
    };
    unsigned char* codes[] = { x86, Avx, Avx512 };
    size_t sizes[] = { sizeof(x86), sizeof(Avx), sizeof(Avx512) };
    string instr[] = {"   x86 min = ", "   Avx min = ", "Avx512 min = ", };

    try
    {
        void* mem = allocMem(sizes);

        float (*func)(const float*, const float, const size_t)
                    = (float (*)(const float*, const float, const size_t))mem;

        const int ArrLen = 4096;
        float a[ArrLen];
        static_assert(ArrLen % 16 == 0,
            "number of elements must be an integral multiple of 16.");

        for (int i = 0; i < ArrLen; i++)        // init
            a[i] = (float)(rand() - (RAND_MAX / 2));
        a[ArrLen / 2 - 1] = -12345678.f;            // min

        cout << fixed << setprecision(2);
        for (int i = 0; i < sizeof(codes) / sizeof(codes[0]); i++)
        {
            memcpy(mem, codes[i], sizes[i]);
            float min = func(a, FLT_MAX, ArrLen);
            cout << instr[i] << min << endl;
        }
        freeMem( mem);
    }
    catch (const char* str)
    {
        cerr << str << endl;
        return -1;
    }
    return 0;
}

allocMem関数

本関数は、受け取ったsize_tの配列から最大サイズを求め実行可能メモリーを割り当てます。そして、そのメモリーアドレスをvoid*で返します。

freeMem関数

これまでと同じで、allocMem関数で割り付けたメモリーアドレスをvoid*で受け取り、そのメモリーを解放します。

main関数

本関数の処理を順次説明します。
unsigned char[]のx86x86命令で記述した機械語、AvxがAVX命令で記述した機械語、そしてAvx512がAVX-512命令で記述した機械語です。

allocMem関数を呼び出し、最大サイズの実行可能メモリーを割り当てます。返却値はvoid*ですので、関数のシグネチャへ合わせたいためポインタの代入を行います。これによってfuncを通常の関数と同じように呼び出すことができます。
float配列にランダムな値を設定します。最小値を配列の中央あたりに設定しますが、これはデバッグ用ですのでなくても構いません。
forループを利用し、各命令後に準じた機械語を実行し、結果を表示します。AVX命令はたいていのパソコンで実行できるでしょうが、AVX-512命令を実装していないCPUも多いでしょう。そのようなパソコンでは、最後の機械語呼び出しで例外が発生します。

実行

機械語で作成した関数を呼び出し、結果を表示します。

 実行結果

C:\>cl /O2 /EHsc min.cpp
...
C:\>.\min
x86 min = -12345678.00
Avx min = -12345678.00
Avx512 min = -12345678.00

AVX-512命令に対応していないCPU

AVX-512命令に対応していないCPUを利用中の場合;

    unsigned char* codes[] = { x86, Avx, Avx512 };
    size_t sizes[] = { sizeof(x86), sizeof(Avx), sizeof(Avx512) };

    unsigned char* codes[] = { x86, Avx };            // for skl
    size_t sizes[] = { sizeof(x86), sizeof(Avx) };    // for skl

に書き換えてしまえば、AVX-512用の機械語は実行されません。

あるいは、SDEを利用しAVX-512命令をエミュレートすると良いでしょう。

 実行結果

C:\>cl /O2 /EHsc min.cpp
...
C:\>set PATH=%PATH%;C:\sde-external-9.99.9-9999-99-99-win\
...
C:\>sde -tgl -- min
x86 min = -12345678.00
Avx min = -12345678.00
Avx512 min = -12345678.00