C++言語を使用している際に、直接アセンブリで記述した機械語を実行します。
はじめに
本例はfloat型の配列内から一番大きな値を探します。値を探す処理を機械語で記述します。機械語はx86、AVX、AVX-512へ対応させた3つの関数を用意します。以前の
arques.hatenablog.com
を少し変更するのみです。
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 0x77, 0x03, // ja 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, 0x5F, 0x03, // vmaxps ymm0,ymm0,ymmword ptr [r11] 0x4D, 0x8D, 0x5B, 0x20, // lea r11,[r11+20h] 0xE2, 0xF5, // loop 02B 0xC4, 0xE3, 0x7D, 0x19, 0xC1, 0x01, // vextractf128 xmm1,ymm0,1 0xC5, 0xF0, 0x5F, 0xC8, // vmaxps xmm1,xmm1,xmm0 0xC5, 0xF0, 0xC6, 0xC1, 0x0E, // vshufps xmm0,xmm1,xmm1,0Eh 0xC5, 0xF8, 0x5F, 0xC9, // vmaxps xmm1,xmm0,xmm1 0xC5, 0xF0, 0xC6, 0xC2, 0x01, // vshufps xmm0,xmm1,xmm2,1 0xC5, 0xF2, 0x5F, 0xC0, // vmaxss 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, 0x5F, 0x03, // vmaxps zmm0,zmm0,zmmword ptr [r11] 0x4D, 0x8D, 0x5B, 0x40, // lea r11,[r11+40h] 0xE2, 0xF4, // loop 063 0x62, 0xF3, 0x7D, 0x48, 0x1B, 0xC1, // vextractf32x8 ymm1,zmm0,1 0x01, 0xC5, 0xFC, 0x5F, 0xD9, // vmaxps ymm3,ymm0,ymm1 0xC4, 0xE3, 0x7D, 0x19, 0xDA, 0x01, // vextractf128 xmm2,ymm3,1 0xC5, 0xE0, 0x5F, 0xCA, // vmaxps xmm1,xmm3,xmm2 0xC4, 0xE3, 0x79, 0x04, 0xC1, 0x0E, // vpermilps xmm0,xmm1,0Eh 0xC5, 0xF0, 0x5F, 0xD0, // vmaxps xmm2,xmm1,xmm0 0xC4, 0xE3, 0x79, 0x04, 0xCA, 0x01, // vpermilps xmm1,xmm2,1 0xC5, 0xEA, 0x5F, 0xC1, // vmaxss xmm0,xmm2,xmm1 0xC3 // ret }; unsigned char* codes[] = { x86, Avx, Avx512 }; size_t sizes[] = { sizeof(x86), sizeof(Avx), sizeof(Avx512) }; string instr[] = {" x86 max = ", " Avx max = ", "Avx512 max = ", }; //unsigned char* codes[] = { x86, Avx }; // for skl //size_t sizes[] = { sizeof(x86), sizeof(Avx) }; // for skl 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; // max cout << fixed << setprecision(2); for (int i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) { memcpy(mem, codes[i], sizes[i]); float max = func(a, -FLT_MAX, ArrLen); cout << instr[i] << max << endl; } freeMem( mem); } catch (const char* str) { cerr << str << endl; return -1; } return 0; }
main関数
本関数の処理を順次説明します。
unsigned char[]のx86がx86命令で記述した機械語、AvxがAVX命令で記述した機械語、そしてAvx512がAVX-512命令で記述した機械語です。
allocMem関数を呼び出し、最大サイズの実行可能メモリーを割り当てます。返却値はvoid*ですので、関数のシグネチャへ合わせたいためポインタの代入を行います。これによってfuncを通常の関数と同じように呼び出すことができます。
float配列にランダムな値を設定します。最大値を配列の中央あたりに設定しますが、これはデバッグ用ですのでなくても構いません。
forループを利用し、各命令後に準じた機械語を実行し、結果を表示します。AVX命令はたいていのパソコンで実行できるでしょうが、AVX-512命令を実装していないCPUも多いでしょう。そのようなパソコンでは、最後の機械語呼び出しで例外が発生します。
実行
機械語で作成した関数を呼び出し、結果を表示します。
実行結果
C:\>cl /O2 /EHsc max.cpp
...
C:\>.\max
x86 max = 12345678.00
Avx max = 12345678.00
Avx512 max = 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 max.cpp
...
C:\>set PATH=%PATH%;C:\sde-external-9.99.9-9999-99-99-win\
...
C:\>sde -tgl -- max
x86 max = 12345678.00
Avx max = 12345678.00
Avx512 max = 12345678.00
アセンブリ言語のソースリストと機械語
機械語を最初から16進数で得るのは難しいです。そこで、一旦アセンブリ言語で記述したソースリストをアセンブルし、そのオブジェクトをディスアセンブラで機械語(16進数)として取得します。
アセンブリ言語
まず、アセンブリコードを示します。このアセンブリ言語で記述した関数は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 max > a[i] ja label1 ; goto label1 movaps xmm1, xmm0 ; max 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: vmaxps ymm0, ymm0, ymmword ptr [r11] lea r11, qword ptr[r11+32] ; next addr loop loop_f vextractf128 xmm1, ymm0, 1 ; reduce ymm0 vmaxps xmm1, xmm1, xmm0 vshufps xmm0, xmm1, xmm1, 14 vmaxps xmm1, xmm0, xmm1 vshufps xmm0, xmm1, xmm2, 1 vmaxss 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: vmaxps zmm0, zmm0, zmmword ptr [r11] lea r11, qword ptr[r11+64] ; next addr loop loop_f vextractf32x8 ymm1, zmm0, 1 ; reduce zmm0 vmaxps ymm3, ymm0, ymm1 vextractf128 xmm2, ymm3, 1 vmaxps xmm1, xmm3, xmm2 vpermilps xmm0, xmm1, 14 vmaxps xmm2, xmm1, xmm0 vpermilps xmm1, xmm2, 1 vmaxss xmm0, xmm2, xmm1 ret Avx512 endp _TEXT ends end
機械語へ
このソースコードをml64コマンドでアセンブルしobjファイルを得ます。そのobjファイルをdumpbin コマンドへ与え、どのような機械語へ変換されるか調べます。
コマンド:
ml64 /c maxAsm.asm
dumpbin /DISASM maxAsm.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: 77 03 ja 0000000000000012 00F: 0F 28 C8 movaps xmm1,xmm0 012: 48 8D 52 04 lea rdx,[rdx+4] 016: E2 EE loop 0000000000000006 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 5F 03 vmaxps ymm0,ymm0,ymmword ptr [r11] 030: 4D 8D 5B 20 lea r11,[r11+20h] 034: E2 F5 loop 000000000000002B 036: C4 E3 7D 19 C1 01 vextractf128 xmm1,ymm0,1 03C: C5 F0 5F C8 vmaxps xmm1,xmm1,xmm0 040: C5 F0 C6 C1 0E vshufps xmm0,xmm1,xmm1,0Eh 045: C5 F8 5F C9 vmaxps xmm1,xmm0,xmm1 049: C5 F0 C6 C2 01 vshufps xmm0,xmm1,xmm2,1 04E: C5 F2 5F C0 vmaxss 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 5F 03 vmaxps zmm0,zmm0,zmmword ptr [r11] 069: 4D 8D 5B 40 lea r11,[r11+40h] 06D: E2 F4 loop 0000000000000063 06F: 62 F3 7D 48 1B C1 vextractf32x8 ymm1,zmm0,1 01 076: C5 FC 5F D9 vmaxps ymm3,ymm0,ymm1 07A: C4 E3 7D 19 DA 01 vextractf128 xmm2,ymm3,1 080: C5 E0 5F CA vmaxps xmm1,xmm3,xmm2 084: C4 E3 79 04 C1 0E vpermilps xmm0,xmm1,0Eh 08A: C5 F0 5F D0 vmaxps xmm2,xmm1,xmm0 08E: C4 E3 79 04 CA 01 vpermilps xmm1,xmm2,1 094: C5 EA 5F C1 vmaxss xmm0,xmm2,xmm1 098: C3 ret