モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

<mini> intrinsic 命令から生成された AMD64 アセンブリ コードを読む【小ネタ】

 Hi, everyone!

 自分は C, C++ で書いたコードがどのようにコンパイルされているかちょいちょい調べることがあります。ベクタライズ化 (SIMD 化) されていないコードでも,実は内部では 128-bit SSE 命令になっていることも珍しくありません。しかし自動ベクタライズ化は確かに正しいコードを吐き出すのですが,アライメント制約をコンパイルに投げることができないので,どのようなアライメントでもいいようなコードを吐き出すのも事実です。

 本当は C と C+intrinsic から生成されたコードを比較仕様と思ったのですが,個人的に書くのがつらいので,intrinsic 版だけここで書こうかと思います。

gist.github.com

 オリジナルなコードをよんでわかれば,私の解説は必要ありません。


 まず最初に intrinsic なコードを読んでみたいと思います。

const __m128i base   = _mm_set1_epi32(0xC0000000);
const __m128i mask_r = _mm_set1_epi32(0xFFC00000);
const __m128i mask_g = _mm_set1_epi32(0x003FF000);
const __m128i mask_b = _mm_set1_epi32(0x00000FFC);

const __m128i tmp = _mm_load_si128(input0++);
const __m128i r = _MM_AND_SRLI_EPI32(tmp, mask_r, 22);
const __m128i g = _MM_AND_SRLI_EPI32(tmp, mask_g,  2);
const __m128i b = _MM_AND_SLLI_EPI32(tmp, mask_b, 18);

_mm_stream_si128(output0++, _MM_OR_SI128_OP4(base, r, g, b));

 疑似コードを書きます。

 uint32x4_t 型は uint32_t が 4 つ集まったものと考えてください。

struct _uint32x4
{
  union
  {
    struct
    {
      uint32_t x, y, z, w;
    };
    uint32_t a[4];
    __m128i v;
  }

  _uint32x4(uint32_t i): x(i), y(i), z(i), w(i) {}
};
typedef struct _uint32x4 uint32x4_t;
uint32x4_t base   = uint32x4_t(0xC0000000);
uint32x4_t mask_r = uint32x4_t(0xFFC00000);
uint32x4_t mask_g = uint32x4_t(0x003FF000);
uint32x4_t mask_b = uint32x4_t(0x00000FFC);

uint32x4_t tmp = (uint32x4_t *)input0++;
uint32x4_t r = (tmp & mask_r) >> 22;
uint32x4_t g = (tmp & mask_g) >>  2;
uint32x4_t b = (tmp & mask_b) << 18;

*(output0++) = base | r | g | b;

 4 つの 32-bit 符号なし整数を同時に演算するイメージで,そこまで難しくないと思います。実際,演算子オーバーロードなどを使って C++ クラスを定義してやれば普通の演算とさほど変わらずに計算することができるでしょう。


 AMD64 では 64-bit 汎用レジスターが 16 本あります [RAX, RBX, RC, RD, RBP, RSI, RDI, RSP, R8~15]。

 これをすごいと思う人はハードウェア畑の人ですが(どうでもいいか;;),ソフトウェア畑の人は少ないと感じるかも。あくまで AMD64 仮想アーキテクチャでの話で,今は案外マイクロアーキテクチャで内部命令に変換してから実行するのも珍しくないですね。依存性を減らすためにレジスターリネイミングとか,実はマイクロプロセッサーは高度なことをしています。まあ余談ですが,こんな話は忘れてください。今から話すのは,AMD64 アーキテクチャです。

 また,SSE 向けの SSE レジスターも 16 本あります [XMM0~15]。128-bit 長です。

 MMX や FPU 向けのレジスターもあります [FPR0/MMX0~7]が,整数演算中心に話すので割愛。補足しておくと 80-bit 長です。

 まあ詳しい話はここを参照してください。基本的な算術・論述演算なので,そこまでの知識は必要ないですが。

x64 アセンブリーの概要 | iSUS

プログラミングノート - x86

 上のページを読めばわかりますが,左から順に RCX, RDX, R8, R9 に引数が保存されます。


引数割り当て

RCX ← input$ = 32

RDXin_linesize$ = 40

R8 ← start_y$ = 48

R9 ← end_y$ = 56

XMM6 ← output$ = 64

out_linesize$ = 72


mov QWORD PTR [rsp+24], rsi

mov QWORD PTR [rsp+32], rdi

*(RSP+24) ← RSI

*(RSP+32) ← RDI

push r14

スタックに R14 を積む

sub rsp, 16

スタックに積んだので,スタックポインターをずらす。

RSPRSP - 16


mov esi, DWORD PTR out_linesize$[rsp]

ESI ← out_linesize

mov eax, r9d

EAX ← R9 (end_y)


in_linesize < out_linesize ? in_linesize : out_linesize

cmp edx, esi

EDX (in_linesize) と ESI (out_linesize) の比較。

mov r11d, esi

R11 ← ESI (out_linesize)

cmovb r11d, edx

compare move if bigger といったところか。cmp 命令で < であれば,R11 ← EDX (in_linesize)

便宜上,R11 を linesize と呼ぶ。


width_d4 = linesize / 16;

shr r11d, 4

R11 (linesize) を 右 4-bit シフト。÷16 に相当。


mov r10d, r8d

R10 ← R8 (start_y)

movaps XMMWORD PTR [rsp], xmm6

スタックポインタ RSP の場所に XMM6 のデータを格納。

以後,*(RSP) ← XMM6 のように書きます。

mov edi, edx

EDI ← EDX (in_linesize)


マスクの用意

movdqa xmm3, XMMWORD PTR __xmm@c0000000c0000000c0000000c0000000

movdqa xmm4, XMMWORD PTR __xmm@ffc00000ffc00000ffc00000ffc00000

movdqa xmm5, XMMWORD PTR __xmm@003ff000003ff000003ff000003ff000

movdqa xmm6, XMMWORD PTR __xmm@00000ffc00000ffc00000ffc00000ffc

各即値ロード

※ 便宜上並べて書いています。副作用がないためですが。


mov r14, rcx

R14 ← RCX (input)


ループ 1: 条件分岐

cmp r8d, r9d

R8 (start_y) と R9 (end_y) の比較。

jae $LN3@decompress

jump if 符号なし greater than or equal to。ループ続行/終了

※符号なし ge, le は それぞれ a, b が割り当てられる。わかりづらい。


mov QWORD PTR [rsp+32], rbx

*(RSP+32) ← RBP

mov r9d, edx

imul r9d, r10d

R9 (input0p) ← R9/EDX (in_linesize) × R10 (start_y)

imul r8d, esi

R8 (output0p) ← R8 (start_y) × ESI (out_linesize)

sub eax, r10d

EAX (height) ← REAX (end_y) - R10 (start_y)

mov QWORD PTR [rsp+40], rbp

*(RSP+40) ← RBP

mov rbp, QWORD PTR output$[rsp]

RBP ← *RSP (output)

mov ebx, eax

EBX ← EAX (height)

npad 8


LL4@decompress:

mov edx, r9d

mov eax, r8d

EDX ← R9 (input0p)

EAX ← R8 (output0p)

add rdx, r14

add rax, rbp

EDX (input0) ← EDX + R14 (input)

EAX (output0) ← EAX + RBP (output)


test r11d, r11d

logical compare 命令。結果 R11 が 0 なら ZF に 1 をセット。

参照: http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/TEST.html

je SHORT $LN2@decompress

jump if equal 命令。レジスター ZF が 1 ならばジャンプ。


mov r10d, r11d

R10 (x) ← R11 (width_d4)

LL7@decompress:

mov rcx, rdx

RCX ← RDX (input0)

add rdx, 16

RDXRDX + 16

movdqa xmm2, XMMWORD PTR [rcx]

XMM2 (tmp) ← *RCX

mov rcx, rax

RCX ← RAX (output0)


movdqa xmm1, xmm2

movdqa xmm0, xmm2

XMM1 ← XMM2 (tmp)

XMM0 ← XMM2 (tmp)

pand xmm1, xmm4

pand xmm0, xmm5

XMM1 ← XMM1 & XMM4 (mask_r)

XMM0 ← XMM0 & XMM5 (mask_g)

psrld xmm1, 22

XMM1 ® ← XMM1 << 22

pand xmm2, xmm6

por xmm1, xmm3

XMM2 ← XMM2 & XMM6 (mask_b)

psrld xmm0, 2

XMM0 (g) ← XMM0 >> 2

por xmm1, xmm0

XMM1 (r | g) ← XMM1 | XMM0

pslld xmm2, 18

XMM2 (b) ← XMM2 << 18

add rax, 16

RAX ← RAX (output0) + 16

por xmm1, xmm2

XMM1 (r | g | b) ← XMM1 | XMM2

movntdq XMMWORD PTR [rcx], xmm1

*rcx ← XMM1

sub r10, 1

R10 ← R10 (x) - 1

jne SHORT $LL7@decompress

jump if not sign 命令。

SF が 0 のとき,すなわち test 命令の MSB が 0 すなわち x が整数のとき,LL7@decompress へ。


add r9d, edi

add r8d, esi

R9 ← R9 (input0p) + EDI (in_linesize)

R8 ← R8 (output0p) + ESI (out_linesize)

sub rbx, 1

EBX ← EBX (height) - 1

jne SHORT $LL4@decompress


汚染したものを元に戻す

mov rbp, QWORD PTR [rsp+40]

mov rbx, QWORD PTR [rsp+32]

RBP ← *(RSP+40)

RBX ← *(RSP+32)

LL3@decompress:

mov rsi, QWORD PTR [rsp+48]

mov rdi, QWORD PTR [rsp+56]

RSI ← *(RSP+48)

RDI ← *(RSP+56)

movaps xmm6, XMMWORD PTR [rsp]

XMM6 ← *RSP

add rsp, 16

RSPRSP + 16

pop r14

スタックから R14 に取り出す

ret 0


解説終わり♪

 いろいろ調べながら書いたので間違いがあるかもしれません><

 実際に調べて AMD64 にかなり詳しくなったと思います。それと同時に遅延なども考えてコード履いているみたいな印象もここから見られる気がします(少し実行が前後していたりするので。)

 それでは~