Upload
mitsunari-shigeo
View
6.860
Download
2
Embed Size (px)
DESCRIPTION
Citation preview
LLVM入門
2013/3/30 光成滋生(@herumi)
x86/x64最適化勉強会5(#x86opti)
目次
目標
LLVMで簡単な関数を作ってCから呼び出す
足し算関数を作ろう
比較と条件分岐
メモリアクセス
ループ
carryつき整数加算
注意 : 私はLLVM歴2週間の初心者です
つまり、私がLLVMに入門した話…
2013/3/30 #x86opti 5 /19 2
LLVM
プログラミング言語や実行環境に依存しない仮想機械をターゲットにした最適化支援コンパイラ基盤全般
LLVMアセンブラで書かれたプログラムの実行、最適化、ターゲット環境への変換などの機能がある
LLVMアセンブラ
SSA(Static Single Assignment)ベース
変数の再代入はできない
型安全
i32, float, doubleなどの型情報を持つ
レジスタは任意個
モジュール(翻訳単位に分かれたプログラム)を合成できる
http://llvm.org/docs/LangRef.html
2013/3/30 #x86opti 5 /19 3
ツール
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
足し算
二つの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 }
アセンブル(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
アセンブル(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
比較と分岐(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
比較と分岐(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
分岐(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
メモリアクセス(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
メモリアクセス(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
ループ
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
オーバーフロー(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 }
オーバーフロー(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
多倍長整数加算の実装(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 }
多倍長整数加算の実装(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 ...
多倍長整数加算の実装(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)
1週間ほど触った雑感
よくできている
ドキュメントが充実している
コマンドエラーが親切
他のCPUの勉強がしやすい
最適化機能はかなり頑張ってる
gccのインラインアセンブラよりずっと使いやすい
プログラムコードがきれい
何をやってるのか追いかけやすい
(私にとって)いまいちなところ
想像していたよりも抽象度が高い
LLVMアセンブラと実行環境のアセンブラとの乖離
もちろん利点なのだが、うーん、それを隠蔽するかみたいな
異なるアーキテクチャのCPUを同じコードでやることのしわ寄せ
2013/3/30 #x86opti 5 /19 19