Hi, everyone!
自分は C, C++ で書いたコードがどのようにコンパイルされているかちょいちょい調べることがあります。ベクタライズ化 (SIMD 化) されていないコードでも,実は内部では 128-bit SSE 命令になっていることも珍しくありません。しかし自動ベクタライズ化は確かに正しいコードを吐き出すのですが,アライメント制約をコンパイルに投げることができないので,どのようなアライメントでもいいようなコードを吐き出すのも事実です。
本当は C と C+intrinsic から生成されたコードを比較仕様と思ったのですが,個人的に書くのがつらいので,intrinsic 版だけここで書こうかと思います。
オリジナルなコードをよんでわかれば,私の解説は必要ありません。
まず最初に 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 長です。
まあ詳しい話はここを参照してください。基本的な算術・論述演算なので,そこまでの知識は必要ないですが。
上のページを読めばわかりますが,左から順に RCX, RDX, R8, R9 に引数が保存されます。
引数割り当て
RCX ← input$ = 32
RDX ← in_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
スタックに積んだので,スタックポインターをずらす。
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
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
pop r14
スタックから R14 に取り出す
ret 0
解説終わり♪
いろいろ調べながら書いたので間違いがあるかもしれません><
実際に調べて AMD64 にかなり詳しくなったと思います。それと同時に遅延なども考えてコード履いているみたいな印象もここから見られる気がします(少し実行が前後していたりするので。)
それでは~