27
LLVM最適化のこつ x86/x64最適化勉強会7 サイボウズ・ラボ 光成滋生

LLVM最適化のこつ

Embed Size (px)

Citation preview

LLVM最適化のこつ

x86/x64最適化勉強会7

サイボウズ・ラボ 光成滋生

楽してARM向け最適化をしたい

ターゲットは簡単な固定サイズの多倍長演算

固定素数𝑝に対する𝑥 ± 𝑦 mod 𝑝とか𝑥𝑦 mod 𝑝とか

期待

x86/x64向け最適なコードを生成できるようにすればARM向けもよいコードが出力されるのでは?

一つのLLVMコード→x64/x86/arm/arm64...なら最高!

いろいろやってみた

目的

2/27

LLVMとは

仮想環境上の最適化支援コンパイラ基盤

LLVMアセンブラ

型安全、任意個のレジスタ、SSAベースのアセンブラ

http://llvm.org/docs/LangRef.html

仮想環境上のオブジェクトを生成

それから更にターゲットCPU向けアセンブラを生成

LLVM入門(2年前の自分の資料)

http://www.slideshare.net/herumi/llvm-17911004

LLVMの復習

3/27

CのコードをLLVMアセンブラで出力

clang -S -c -O3 -emit-llvm t.cでt.llを生成

defineで関数の定義:"@名前"が関数名

i32, i64, i128などが整数:"%名前"が変数名

ほぼ全ての命令に型がつく:ex. add i32, ret i32

整数の足し算関数

define i32 @add(i32 %x, i32 %y) { %ret = add i32 %x, %y ret i32 %ret }

uint32_t add(uint32_t x, uint32_t y) { return x + y; }

4/27

llc t.llでx64コードに

-march=x86でx86コードに変換

-march=arm, -march=aarch64なども

VC向け出力が見当たらない→llc.exeなら可能

ターゲットCPUへの変換

add: movl 4(%esp), %eax addl 8(%esp), %eax retl

add: leal (%rdi, %rsi), %eax ; x = %rdi, y = %rsi retq

add: r0, r0, r1 mov pc, lr

5/27

Cによるコード

add128の実装

void add128(uint64_t *z, const uint64_t *x, const uint64_t *y) { uint64_t x0 = x[0]; uint64_t z0 = x0 + y[0]; pz[0] = z0; uint64_t x1 = px[1]; uint64_t y1 = py[1]; if (z0 >= x0) { // 繰り上がり無し pz[1] = x1 + y1; } else { pz[1] = x1 + y1 + 1; // 繰り上がりあり } }

6/27

組み込み関数llvm.uadd.with.overflow

%xと%yを足して結果のi64とcarryのi1の組を返す

入力にcarryが無い???

なかなか...なかなか使い勝手が悪い

i1をzext(ゼロ拡張)して足してみる

carryの扱い

declare {i64, i1} @llvm.uadd.with.overflow.i64(i64 %x, i64 %y)

7/27

zextでi1をi64に変換して足す

zextでadd128を実装(1)

define void @add128zext(i64* %pz, i64* %px, i64* %py) { %x0 = load i64* %px %y0 = load i64* %py %vc0 = call{i64,i1}@llvm.uadd.with.overflow.i64(i64 %x0, i64 %y0) %v0 = extractvalue { i64, i1 } %vc0, 0 %c0 = extractvalue { i64, i1 } %vc0, 1 store i64 %v0, i64* %pz %c = zext i1 %c0 to i64 %px1 = getelementptr i64* %px, i64 1 %py1 = getelementptr i64* %py, i64 1 %x1 = load i64* %px1 %y1 = load i64* %py1 %z = add i64 %x1, %y1 %t = add i64 %z, %c %pz1 = getelementptr i64* %pz, i64 1 store i64 %t, i64* %pz1 ret void }

8/27

zextでi1をi64に変換して足す

carryを先に足してみる

zextでadd128を実装(2)

add128zext: mov rax, qword ptr [rsi] ; rax = x[0] mov r8, qword ptr [rdx] ; r8 = y[0] lea rcx, qword ptr [rax + r8] ; z0 = x[0] + y[0] mov qword ptr [rdi], rcx ; z[0] = z0 mov rcx, qword ptr [rdx + 8] ; rcx = x[1] add rcx, qword ptr [rsi + 8] ; rcx = y[1] add rax, r8 ; え、もう一度足すの adc rcx, 0 mov qword ptr [rdi + 8], rcx ret

%z = add i64 %x1, %y1 %t = add i64 %z, %c

%z = add i64 %x1, %c %t = add i64 %z, %y1

9/27

うーむ微妙

zextでadd128を実装(3)

add128zext2: mov rax, qword ptr [rsi] ; rax = x[0] add rax, qword ptr [rdx] ; rax += x[0] + y[0] mov qword ptr [rdi], rax ; z[0] = x[0] + y[0] sbb rax, rax ; rax = carry ? -1 : 0 and rax, 1 ; rax = carry ? 1 : 0 add rax, qword ptr [rsi + 8] ; rax = x[1] + carry add rax, qword ptr [rdx + 8] ; rax += y[1] mov qword ptr [rdi + 8], rax ; z[1] = rax ret

10/27

select i1 flag, v1, v2 ; (flag ? v1 : v0)

zextと同じ結果

まあそうかも

LLVMでの正解は

selectを使ってみる

%z = add i64, %x1, %y1 %t0 = select i1 %c0, i64 1, i64 0 %t1 = add i64 %z, %t0

11/27

整数型のサイズに制約が無い

その出力(x64とaarch64)

i128を使う

define void @add128(i128* %pz, i128* %px, i128* %py) { %x = load i128* %px %y = load i128* %py %z = add i128 %x, %y store i128 %z, i128* %pz ret void }

mov rax, [rdx] mov rcx, [rdx + 8] add rax, [rsi] adc rcx, [rsi + 8] mov [rdi + 8], rcx mov [rdi], rax ret

ldp x8, x9, [x1] ldp x10, x11, [x2] adds x8, x8, x10 adcs x9, x9, x11 stp x8, x9, [x0] ret

12/27

1回のaddと3回のadcなのでよさそう

carryは明示的に扱わないのがよさげ

x86やarmをターゲットにすると

; レジスタ退避して ; メモリから値を読み込む部分は省略 add edi, dword ptr [ecx] adc ebx, dword ptr [ecx + 4] adc esi, dword ptr [ecx + 8] adc ebp, dword ptr [edx + 12] mov dword ptr [eax + 8], esi mov dword ptr [eax + 4], ebx mov dword ptr [eax], edi mov dword ptr [eax + 12], ebp ...

13/27

uint8_t _addcarry_u64(CF, x, y, *out)がある

こっちのほうがいい

Visual Studioのintrinsic

#include <intrin.h> void add128(uint64_t* z, const uint64_t* x, const uint64_t* y) { uint8_t c = _addcarry_u64(0, x[0], y[0], &z[0]); _addcarry_u64(c, x[1], y[1], &z[1]); }

mov rax, [rdx] add rax, [r8] mov [rcx], rax mov rax, [rdx+8] adc rax, [r8+8] mov [rcx+8], rax ret

14/27

Cの擬似コード

比較と引き算は同じ処理

結果を捨てるか捨てないかの差

とりあえず引いて負なら(carryが立てば)捨てる

addModの実装

void addMod(INT *z, const INT *x, const INT *y, const INT *p) { INT t = *x + y if (t >= *p) t -= *p *z = t }

void addMod(INT *z, const INT *x, const INT *y, const INT *p) { INT t0 = *x + y (t1, CF) = t0 - *p *z = CF ? t0 : t1 }

15/27

こんな感じ(N=128, NP=192とか)

LLVMでの実装

declare {iNP, i1 } @llvm.usub.with.overflow.iNP(iNP, iNP) define void @addMod(iN* %pz, iN* %px, iN* %py, iN* %pp) { %x = load iN* %px %y = load iN* %py %p = load iN* %pp %x1 = zext iN %x to iNP %y1 = zext iN %y to iNP %p1 = zext iN %p to iNP %t = add iNP %x1, %y1 ; t = x + y %vc = call{iNP,i1} @llvm.usub.with.overflow.iNP(iNP %t, iNP %p1) %v = extractvalue { iNP, i1 } %vc, 0 ; v = x + y - p %c = extractvalue { iNP, i1 } %vc, 1 %z = select i1 %c, iNP %t, iNP %v ; z = CF ? t : v %z1 = trunc iNP %z to iN store iN %z1, iN* %pz ret void

16/27

まあまあ? mov r9, qword ptr [rdx] mov r10, qword ptr [rdx + 8] add r9, qword ptr [rsi] adc r10, qword ptr [rsi + 8] sbb rsi, rsi and rsi, 1 ; [rsi:r10:r9] = x + y xor r8d, r8d mov rax, r9 sub rax, qword ptr [rcx] mov rdx, r10 sbb rdx, qword ptr [rcx + 8] ; [rdx:rax] = x + y - p sbb rsi, 0 sbb r8, 0 cmovne rdx, r10 cmovne rax, r9 mov qword ptr [rdi + 8], rdx mov qword ptr [rdi], rax

これが無駄

ここはよい

17/27

usbのcarryとi1の冗長性が無駄なのか

いまいち... 模索中

usubを使わない

%t0 = add iNP %x1, %y1 ; x + y %t1 = sub iNP %t0, %p1 ; x + y - p %t2 = lshr iNP %t1, N ; 右シフトして上位ビット %t3 = trunc iNP %t2 to i1 ; を取り出して %t4 = select i1 %t3, iNP %t0, iNP %t1 ; 選択

; [r9:r8] = x + y sub rax, [rcx] mov rdx, r9 sbb rdx, [rcx + 8] ; [rdx:eax] = x + y - p sbb rsi, 0 ; 不要 and esi, 1 ; 不要 cmovne rax, r8 test sil, sil ; 不要 cmovne rdx, r9 ; z = [rdx:rax]

18/27

N bit x N bit → 2N bit みたいなの

出力が2倍になるのでzext N to 2Nしてからmulしよう

乗算は

define void @mul128(i256* %pz, i128* %px, i128* %py) { %x = load i128* %px %y = load i128* %py %x1 = zext i128 %x to i256 %y1 = zext i128 %y to i256 %z = mul i256 %x1, %y1 store i256 %z, i256* %pz ret void }

19/27

Segmentation fault

nativeに命令を持ってないと駄目のようだ

出力結果

0 libLLVM-3.5.so.1 0x00007fa53e015cd2 llvm::sys::PrintStackTrace(_IO_FILE*) + 34 1 libLLVM-3.5.so.1 0x00007fa53e015ac4 2 libc.so.6 0x00007fa53c85ad40 3 libc.so.6 0x00007fa53c8acaea strlen + 42 4 libLLVM-3.5.so.1 0x00007fa53df39d3c llvm::SelectionDAG::getExternalSymbol(char const*, llvm::EVT) + 44 5 libLLVM-3.5.so.1 0x00007fa53dfa1b78 llvm::TargetLowering::makeLibCall(llvm::SelectionDAG&, llvm::RTLIB::Libcall, llvm::EVT, llvm::SDValue const*, unsigned int, bool, llvm::SDLoc, bool, bool) const + 552 6 libLLVM-3.5.so.1 0x00007fa53ded57f0 7 libLLVM-3.5.so.1 0x00007fa53dedd0cb 8 libLLVM-3.5.so.1 0x00007fa53deea61e 9 libLLVM-3.5.so.1 0x00007fa53deeacdf llvm::SelectionDAG::LegalizeTypes() + 943 10 libLLVM-3.5.so.1 0x00007fa53df8c154 llvm::SelectionDAGISel::CodeGenAndEmitDAG() + 164 11 libLLVM-3.5.so.1 0x00007fa53df8f242 llvm::SelectionDAGISel::SelectAllBasicBlocks(llvm::Function const&) + 1154 12 libLLVM-3.5.so.1 0x00007fa53df90551 llvm::SelectionDAGISel::runOnMachineFunction(llvm::MachineFunction&) + 929 13 libLLVM-3.5.so.1 0x00007fa53e18e238 14 libLLVM-3.5.so.1 0x00007fa53d92fbb7 llvm::FPPassManager::runOnFunction(llvm::Function&) + 471 15 libLLVM-3.5.so.1 0x00007fa53d92ff3b llvm::FPPassManager::runOnModule(llvm::Module&) + 43 16 libLLVM-3.5.so.1 0x00007fa53d930215 llvm::legacy::PassManagerImpl::run(llvm::Module&) + 693 17 llc-3.5 0x0000000000412962 18 llc-3.5 0x000000000040d0f0 main + 368 19 libc.so.6 0x00007fa53c845ec5 __libc_start_main + 245 20 llc-3.5 0x000000000040d149 Stack dump: 0. Program arguments: llc-3.5 uint.ll 1. Running pass 'Function Pass Manager' on module 'uint.ll'. 2. Running pass 'X86 DAG->DAG Instruction Selection' on function '@mul128' Segmentation fault

20/27

N bit x 64 bit → N + 64bit

64bit x 64bit → 128bitを作ってそれを組み合わせる

このコードはちゃんとコンパイルできる

変数の上位bitが0であることを知っている

-mattr=bmi2つきで実行するとHaswellの命令を使う

まず64bit倍するのを作る

define i128 @mul64x64(i64 %x, i64 %y) { %x0 = zext i64 %x to i128 %y0 = zext i64 %y to i128 %z = mul i128 %x0, %y0 ret i128 %z }

mul64x64: mov rdx, rsi mulx rdx, rax, rdi ret

21/27

64bit x 64bitを2回して足す

後の拡張のためxからHやLを取り出すextractを作る

mul128x64

define i192 @mul128x64(i128 %x, i64 %y) { ; x = [x1:x0] %x0 = call i64 @extract128(i128 %x, i128 0) %x0y = call i128 @mul64x64(i64 %x0, i64 %y) ; x0 * y %x0y0 = zext i128 %x0y to i192 %x1 = call i64 @extract128(i128 %x, i128 64) %x1y = call i128 @mul64x64(i64 %x1, i64 %y) ; x1 * y %x1y0 = zext i128 %x1y to i192 %x1y1 = shl i192 %x1y0, 64 ; シフトして %t0 = add i192 %x0y0, %x1y1 ; 足す ret i192 %t0 }

define i64 @extract128(i128 %x, i128 %shift) { %t0 = lshr i128 %x, %shift %t1 = trunc i128 %t0 to i64 ret i64 %t1 }

22/27

mul128x64を2回呼び出して加算

そんなに細かく関数に分けて大丈夫か

問題ない

最適化 + ターゲットに変換

乗算4回、加算7回の最適なコードを出力

mul128x128

opt -O3 –o - | llc –O3 -

mulx r9, r10, r8 mov rdx, rbx mov rdx, rax mulx rbx, rdx, r11 mulx rsi, rax, r11 add rdx, r8 add rax, r9 adc rbx, 0 adc rsi, 0 add rcx, rax mov qword ptr [rdi], r10 adc rdx, rsi mov rdx, rbx adc rbx, 0 mulx r8, rcx, r8

23/27

64bit x 64bit→128bitが無いため

先に32bit x 32bit→64bitでラッパーを作る?

そうするとトータルの加算回数が増えてしまう

4N x 2Nの場合

"4N x N ; 4回" x 2回. 5N + 5Nで5回の計13回

2Nずつすると"2N x 2N ; 7回" x 2回. 後4回の計18回

32bit用の乗算コードが必要

64bitと32bitの2種類用意する必要がある

テンプレートのコードから両方を自動生成するはめに

当初の野望

LLVM一つでx86/x64/arm/arm64全部

現実:32bitと64bit必要

32bitだとやはりSEGV

24/27

p=0xfffffffffffffffffffffffffffffffeffffffffffffffffで割る

殆ど2^192に近い素数(NIST_P192)

余りを簡単に求められる

例:123123 % 999 = (123000 % 999)+123 = 123 + 123=246

こんなコードで

32/64bit対応

Nexus7で試すと

GMPに比べて

add 188→60nsec

mul 990→480nsec

うまくいったケース(特殊な剰余)

define void @modNIST_P192(i192* %out, i192 %H192, i192 %L) { %H192 = load i192* %pH %H = zext i192 %H192 to i256 %H10_ = shl i192 %H192, 64 %H10 = zext i192 %H10_ to i256 %H2_ = call i64 @extract192to64(i192 %H192, i192 128) %H2 = zext i64 %H2_ to i256 %H102 = or i256 %H10, %H2 %H2s = shl i256 %H2, 64 %t0 = add i256 %L, %H %t1 = add i256 %t0, %H102 %t2 = add i256 %t1, %H2s %e = lshr i256 %t2, 192 %t3 = trunc i256 %t2 to i192 %e1 = trunc i256 %e to i192 %t4 = add i192 %t3, %e1 %e2 = shl i192 %e1, 64 %t5 = add i192 %t4, %e2 store i192 %t5, i192* %out ret void }

25/27

i192などの変数のどの部分が0かを

きちんと追跡している

シフトしなくていい部分は一切しない

メモリアクセスも追跡している

先ほどのmul128x128でpx = pyとして最適化すると

乗算回数が3回に減る

読み込み先と書き込み先が同じ可能性を考慮

noaliasをつけるか、値が確定したものは早い段階で

メモリに書き落とすとテンポラリが減る

例:乗算の途中結果

所感

26/27

最適化はかなり頑張ってる

レジスタサイズの制約が無いように見えて

実は結構きつい

乗算をするとき。加減算のみならほぼ大丈夫

ただし書き下せればかなりよいコード生成

フラグの扱いがやや難

3/16加筆(Windows呼び出し規約に従う方法)

https://github.com/CRogers/LLVM-Windows-Binaries

llc.exeで-march=x86-64 -x86-asm-syntax=intel

まとめ

27/27