SIMDの使い方メモ
勉強を始めたばかりなので間違ってるところもあるかもしれないので、ツッコミがあると嬉しいかも。
SIMD - Simple Instruction Multiple Data
単一命令で複数データを扱う仕組み。イメージとしては
と4回足し算するよりも
の方が足し算一回で速いだろ?って感じ。
実際の実装としてはIntelのSSE(=Streaming SIMD Extensions)とかその後継のAVX(=Advanced Vector eXtensions)がある。
例えばSSEは128bitレジスタをCPUに用意していて、単精度浮動点小数点数(float型)の場合なら32bit×4個の計算を同時に行うことができる。
実際に見てみると、例えばHaswellだと浮動点小数点数の加算命令FADDのレイテンシは3クロックであるのに対して、SSEの加算命令ADDPSも同じく3クロックである。
演算結果をメモリに読み書きするタイミングや回数さえ注意すれば、理論上4倍速にすることができるというわけだ。
【参考】http://www.agner.org/optimize/instruction_tables.pdf
実際に使う
それでは、一番最初にイメージとして出した計算をSSEで計算してみる。
#include <cstdio> #include <xmmintrin.h> int main() { __m128 a, b, c; a = _mm_setr_ps(2.0f, 3.0f, 9.0f, 12.0f); b = _mm_setr_ps(5.0f, 6.0f, 4.0f, 16.0f); c = _mm_add_ps(a, b); float* r = (float*)&c; printf("%.2f, %.2f, %.2f, %.2f\n", r[0], r[1], r[2], r[3]); }
実行結果
7.00, 9.00, 13.00, 28.00
こういった感じ。SSEを使う際にはxmmintrin.hというヘッダをインクルードする。g++ならあとはそのままコンパイルできる。
__m128というのが128bitのデータ型みたい。これがAVXになると__m256とか__m512とか出てくる。
gccの場合はコンパイル時に-msseオプションが必要。
ちなみに、上記のプログラムを-O3で最適化して逆アセンブルするとこんな感じ。
.section __TEXT,__text,regular,pure_instructions .section __TEXT,__literal4,4byte_literals .align 2 LCPI0_0: .long 1094713344 ## float 12 LCPI0_1: .long 1098907648 ## float 16 .section __TEXT,__text,regular,pure_instructions .globl _main .align 4, 0x90 _main: ## @main .cfi_startproc ## BB#0: pushq %rbp Ltmp2: .cfi_def_cfa_offset 16 Ltmp3: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp4: .cfi_def_cfa_register %rbp subq $160, %rsp leaq L_.str(%rip), %rdi leaq -144(%rbp), %rax movl $1073741824, -68(%rbp) ## imm = 0x40000000 movl $1077936128, -72(%rbp) ## imm = 0x40400000 movl $1091567616, -76(%rbp) ## imm = 0x41100000 movl $1094713344, -80(%rbp) ## imm = 0x41400000 movss LCPI0_0(%rip), %xmm0 movss -72(%rbp), %xmm1 unpcklps %xmm0, %xmm1 ## xmm1 = xmm1[0],xmm0[0],xmm1[1],xmm0[1] movss -76(%rbp), %xmm0 movss -68(%rbp), %xmm2 unpcklps %xmm0, %xmm2 ## xmm2 = xmm2[0],xmm0[0],xmm2[1],xmm0[1] unpcklps %xmm1, %xmm2 ## xmm2 = xmm2[0],xmm1[0],xmm2[1],xmm1[1] movaps %xmm2, -96(%rbp) movaps %xmm2, -112(%rbp) movl $1084227584, -4(%rbp) ## imm = 0x40A00000 movl $1086324736, -8(%rbp) ## imm = 0x40C00000 movl $1082130432, -12(%rbp) ## imm = 0x40800000 movl $1098907648, -16(%rbp) ## imm = 0x41800000 movss LCPI0_1(%rip), %xmm0 movss -8(%rbp), %xmm1 unpcklps %xmm0, %xmm1 ## xmm1 = xmm1[0],xmm0[0],xmm1[1],xmm0[1] movss -12(%rbp), %xmm0 movss -4(%rbp), %xmm2 unpcklps %xmm0, %xmm2 ## xmm2 = xmm2[0],xmm0[0],xmm2[1],xmm0[1] unpcklps %xmm1, %xmm2 ## xmm2 = xmm2[0],xmm1[0],xmm2[1],xmm1[1] movaps %xmm2, -32(%rbp) movaps %xmm2, -128(%rbp) movaps -112(%rbp), %xmm0 movaps %xmm0, -48(%rbp) movaps %xmm2, -64(%rbp) movaps -48(%rbp), %xmm0 addps -64(%rbp), %xmm0 movaps %xmm0, -144(%rbp) movq %rax, -152(%rbp) movq -152(%rbp), %rax cvtss2sd (%rax), %xmm0 movq -152(%rbp), %rax cvtss2sd 4(%rax), %xmm1 movq -152(%rbp), %rax cvtss2sd 8(%rax), %xmm2 movq -152(%rbp), %rax cvtss2sd 12(%rax), %xmm3 movb $4, %al callq _printf movl $0, %ecx movl %eax, -156(%rbp) ## 4-byte Spill movl %ecx, %eax addq $160, %rsp popq %rbp ret .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "%.2f, %.2f, %.2f, %.2f\n" .subsections_via_symbols
ちょっと読む体力がないけど、addpsのようなSSE命令が使われていることは確認できる。