19
LLVM入門 2013/3/30 光成滋生(@herumi) x86/x64最適化勉強会5(#x86opti)

llvm入門

Embed Size (px)

DESCRIPTION

 

Citation preview

Page 1: llvm入門

LLVM入門

2013/3/30 光成滋生(@herumi)

x86/x64最適化勉強会5(#x86opti)

Page 2: llvm入門

目次

目標

LLVMで簡単な関数を作ってCから呼び出す

足し算関数を作ろう

比較と条件分岐

メモリアクセス

ループ

carryつき整数加算

注意 : 私はLLVM歴2週間の初心者です

つまり、私がLLVMに入門した話…

2013/3/30 #x86opti 5 /19 2

Page 3: llvm入門

LLVM

プログラミング言語や実行環境に依存しない仮想機械をターゲットにした最適化支援コンパイラ基盤全般

LLVMアセンブラで書かれたプログラムの実行、最適化、ターゲット環境への変換などの機能がある

LLVMアセンブラ

SSA(Static Single Assignment)ベース

変数の再代入はできない

型安全

i32, float, doubleなどの型情報を持つ

レジスタは任意個

モジュール(翻訳単位に分かれたプログラム)を合成できる

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

2013/3/30 #x86opti 5 /19 3

Page 4: llvm入門

ツール

clang –S –emit-llvm <C/C++ソース>.c

C/C++からLLVMアセンブラ(以下LLVMと略)を生成

llc <LLVMアセンブラ>.ll

LLVMアセンブラからターゲットCPUのアセンブラを生成

-marchオプションでターゲットCPUを指定

x86, arm, mips, sparc, etc.

llc –versionでサポートターゲット一覧表示

llc –mattr=helpでより詳細な設定一覧表示

lli <LLVMアセンブラ>.ll

LLVMアセンブラを仮想マシン上で実行する

当然リンカや逆アセンブラ、最適化ツールなどもある

2013/3/30 #x86opti 5 /19 4

Page 5: llvm入門

足し算

二つのuint32_t変数を足して返す関数を作る

define(関数定義) 関数名:@なんとか

レジスタ名:%なんとか

i32(32bitレジスタ) 符号は特に無い(使う命令で決める)

i1なら1bitのレジスタ(フラグ)

i128なら128bitのレジスタ

entry(ラベル)

とりあえず一つラベルがいる

add(加算命令), ret(関数から返る命令)

各命令にも型情報が必要

2013/3/30 #x86opti 5 /19 5

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

Page 6: llvm入門

アセンブル(1/3)

アセンブルして標準出力に出す

Linuxの64bit環境ではrdiが第一引数, rsiが第二引数

C/C++の呼び出し規約にしたがって処理される

lealで eax ← rdi + rsiを実行

LLVMのaddが単純にx64のaddになるわけではない

x86用に出力してみる

2013/3/30 #x86opti 5 /19 6

llc add.ll –o – // コメント削除 add1: leal (%rdi,%rsi), %eax ret

llc add.ll –o – -march=x86 add1: movl 4(%esp), %eax ; 一つ目の引数 addl 8(%esp), %eax ; 二つ目の引数 ret

Page 7: llvm入門

アセンブル(2/3)

Intel形式で出してみる

arm用に出力

二項演算としては他にsub, mul, udiv(符号なし), sdiv(符号あり), urem, srem, fadd(浮動小数)など

2013/3/30 #x86opti 5 /19 7

llc add.ll –o – -march=x86 -x86-asm-syntax=intel add1: mov EAX, DWORD PTR [ESP + 4] add EAX, DWORD PTR [ESP + 8] ret

llc add.ll –o – -march=arm add1: add r0, r0, r1 mov pc, lr

Page 8: llvm入門

比較と分岐(1/3)

二つの値の大きい方

比較命令はicmp

icmpの戻り値は 1bitの変数

ugt → 符号なしgt

他にeq, ne, sltなど

brでラベルに飛ぶ

elseは予約語ではない

なんでもいい

2013/3/30 #x86opti 5 /19 8

define i32 @my_max(i32 %x, i32 %y) { entry: %r = icmp ugt i32 %x, %y br i1 %r, label %gt, label %else gt: ret i32 %x else: ret i32 %y }

my_max: cmpl %esi, %edi jbe .LBB4_2 movl %edi, %eax ret .LBB4_2: movl %esi, %eax ret

Page 9: llvm入門

比較と分岐(2/3)

絶対値の場合

0より小さいかを見るにはslt(signed less than)

y = sub 0, xで-xを作る

nsw(no signed wrap)

制御の合流

phi命令を使う

2013/3/30 #x86opti 5 /19 9

define i32 @my_abs(i32 %x) { entry: %cmp = icmp slt i32 %x, 0 br i1 %cmp, label %lt, label %else lt: %neg = sub nsw i32 0, %x br label %exit else: br label %exit exit: %ret = phi i32 [%neg,%lt], [%x,%else] ret i32 %ret }

my_abs: test edi, edi jns else neg edi else: mov eax, edi ret

Page 10: llvm入門

分岐(3/3)

selectを使う

cmpにしたがって値を選択

x86ではcmov

cmovを使わせないとジャンプ命令が使われる

-march=x86 –mattr=-cmov

2013/3/30 #x86opti 5 /19 10

define i32 @my_max3(i32 %x, i32 %y) { entry: %cmp = icmp ugt i32 %x, %y %cond = select i1 %cmp, i32 %x, i32 %y ret i32 %cond }

cmp edi, esi cmova esi, edi ; edi > esiならesi ← edi mov eax, esi ret

Page 11: llvm入門

メモリアクセス(1/2)

次の関数を作ってみる

loadとstore命令

alignを指定するとそのalignが仮定される

armでalign 1にするとバイト単位で読むコードに展開された

x86/x64では気にしないw

2013/3/30 #x86opti 5 /19 11

define void @add(i32* %z,i32* %x,i32* %y){ entry: %0 = load i32* %x, align 32 %1 = load i32* %y, align 32 %ret = add nsw i32 %0, %1 store i32 %ret, i32* %z, align 32 ret void }

void add(int *z, const int *x, const int *y) { *z = *x + *y; }

add: mov eax,dword [rsi] add eax,dword [rdx] mov dword [rdi],eax ret

Page 12: llvm入門

メモリアクセス(2/2)

uint128_tの足し算を作ってみる

i128を使う

そんなレジスタが無い環境(たいていの環境)でも使える

i64*をi128*にして値を読む

型変換にはbitcastを使う

2013/3/30 #x86opti 5 /19 12

define void @add(i64* %z,i64* %x,i64* %y){ entry: %0 = bitcast i64* %x to i128* %1 = bitcast i64* %y to i128* %2 = load i128* %0, align 64 %3 = load i128* %1, align 64 %4 = add i128 %2, %3 %5 = bitcast i64* %z to i128* store i128 %4, i128* %5, align 64 ret void }

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

Page 13: llvm入門

ループ

uint64_tの配列の総和を求める

ループの更新では値の上書きができないのでphiを使う

getelementptr

ポインタの計算に使う

ループ変数が減る方向!

2013/3/30 #x86opti 5 /19 13

define i64 @sum(i64* %x,i64 %n) { entry: %n_is_0 = icmp eq i64 %n, 0 br i1 %n_is_0, label %exit,label %lp lp: %ip = phi i64 [0,%entry],[%i,%lp] %retp = phi i64 [0,%entry],[%ret,%lp] %xi = getelementptr i64* %x, i64 %ip %v = load i64* %xi %ret = add i64 %retp, %v %i = add i64 %ip, 1 %i_eq_n = icmp eq i64 %i, %n br i1 %i_eq_n,label %exit,label %lp exit: %r = phi i64 [0,%entry],[%ret,%lp] ret i64 %r }

sum: xor eax, eax test rsi, rsi je exit lp: add rax, qword [rdi] add rdi, 8 dec rsi jne lp exit: ret

Page 14: llvm入門

オーバーフロー(1/2)

多倍長演算のためにcarryを使う

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

使うにはdeclareが必要

戻り値は値とフラグのペア

そこから値を取り出すにはextractvalueを使う

2013/3/30 #x86opti 5 /19 14

// *z = x + y, return true if overflow // bool add_over(uint32_t *z, uint32_t x, uint32_t y); declare {i32, i1} @llvm.uadd.with.overflow.i32(i32, i32) define zeroext i1 @add_over(i32* %z, i32 %x, i32 %y) { entry: %0 = call {i32, i1} @llvm.uadd.with.overflow.i32(i32 %x,i32 %y) %ret = extractvalue {i32, i1} %0, 0 store i32 %ret, i32* %z %flag = extractvalue {i32, i1} %0, 1 ret i1 %flag }

Page 15: llvm入門

オーバーフロー(2/2)

前ページのコードの出力

小さい幅のレジスタから大きい幅のレジスタへの拡張

zext(符号なし)やsext(符号あり)を使う

困った

carryをaddに加える命令が無い!

LLVMのソースコードを見ると内部的にはz=ADDE(x, y, carry) というのがあるようだが、それを呼べない…

2013/3/30 #x86opti 5 /19 15

// *z = x + y, return true if overflow // bool add_over(uint32_t *z, uint32_t x, uint32_t y); add_over: addl %edx, %esi movl %esi, (%rdi) setb %al ret ; al ← set 1 if overflow

Page 16: llvm入門

多倍長整数加算の実装(1/3)

疑似コード

add_with_carryは二つのレジスタとcarryを入力として加算の結果とCFのペアを返す

2013/3/30 #x86opti 5 /19 16

addn(uint64_t *pz,const uint64_t *px,const uint64_t *py,size_t n){ bool CF = 0; for (size_t i = 0; i < n; i++) (pz[i],CF)=add_with_carry(px[i], py[i], CF); }

define {i64, i1} @add_with_carry(i64 %x, i64 %y, i1 %c) { %vc1 = call {i64, i1}@llvm.uadd.with.overflow.i64(i64 %x,i64 %y) %v1 = extractvalue {i64, i1} %vc1, 0 %c1 = extractvalue {i64, i1} %vc1, 1 %zc = zext i1 %c to i64 %v2 = add i64 %v1, %zc %r1 = insertvalue {i64, i1} undef, i64 %v2, 0 %r2 = insertvalue {i64, i1} %r1, i1 %c1, 1 ret { i64, i1 } %r2 }

Page 17: llvm入門

多倍長整数加算の実装(2/3)

作ったadd_with_carryを使って実装する

ループの一部

llc uint.ll –o –

あれ、関数呼び出しのまま

2013/3/30 #x86opti 5 /19 17

%x = load i64* %px_i, align 64 %y = load i64* %py_i, align 64 %rc1 = call {i64, i1} @add_with_carry(i64 %x, i64 %y, i1 %c_p) %r2 = extractvalue {i64, i1} %rc1, 0 %c = extractvalue {i64, i1} %rc1, 1

.lp: movq (%r15), %rsi movq (%r12), %rdi movzbl %dl, %edx callq add_with_carry ...

Page 18: llvm入門

多倍長整数加算の実装(3/3)

optコマンドを使って最適化する

一度bc(ビットコード)に変換して逆アセンブルしてllcを適用

関数が展開されて埋め込まれた

すばらしい!

性能については後半に続く

2013/3/30 #x86opti 5 /19 18

opt uint.ll -o - -std-compile-opts | llvm-dis –o - | llc –o -

.lp: movq (%rsi), %r9 addq (%rdx), %r9 setb %al movzbl %r8b, %r8d andq $1, %r8 addq %r9, %r8 movq %r8, (%rdi)

Page 19: llvm入門

1週間ほど触った雑感

よくできている

ドキュメントが充実している

コマンドエラーが親切

他のCPUの勉強がしやすい

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

gccのインラインアセンブラよりずっと使いやすい

プログラムコードがきれい

何をやってるのか追いかけやすい

(私にとって)いまいちなところ

想像していたよりも抽象度が高い

LLVMアセンブラと実行環境のアセンブラとの乖離

もちろん利点なのだが、うーん、それを隠蔽するかみたいな

異なるアーキテクチャのCPUを同じコードでやることのしわ寄せ

2013/3/30 #x86opti 5 /19 19