Mais conteúdo relacionado
Mais de MITSUNARI Shigeo (20)
llvm入門
- 2. 目次
目標
LLVMで簡単な関数を作ってCから呼び出す
足し算関数を作ろう
比較と条件分岐
メモリアクセス
ループ
carryつき整数加算
注意 : 私はLLVM歴2週間の初心者です
つまり、私がLLVMに入門した話…
2013/3/30 #x86opti 5 2 /19
- 3. LLVM
プログラミング言語や実行環境に依存しない仮想機械
をターゲットにした最適化支援コンパイラ基盤全般
LLVMアセンブラで書かれたプログラムの実行、最適化、タ
ーゲット環境への変換などの機能がある
LLVMアセンブラ
SSA(Static Single Assignment)ベース
変数の再代入はできない
型安全
i32, float, doubleなどの型情報を持つ
レジスタは任意個
モジュール(翻訳単位に分かれたプログラム)を合成できる
http://llvm.org/docs/LangRef.html
2013/3/30 #x86opti 5 3 /19
- 4. ツール
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 4 /19
- 5. 足し算
二つのuint32_t変数を足して返す関数を作る
define(関数定義) define i32 @add1(i32 %x, i32 %y) {
entry:
関数名:@なんとか
%ret = add i32 %x, %y
レジスタ名:%なんとか ret i32 %ret
}
i32(32bitレジスタ)
符号は特に無い(使う命令で決める)
i1なら1bitのレジスタ(フラグ)
i128なら128bitのレジスタ
entry(ラベル)
とりあえず一つラベルがいる
add(加算命令), ret(関数から返る命令)
各命令にも型情報が必要
2013/3/30 #x86opti 5 5 /19
- 6. アセンブル(1/3)
アセンブルして標準出力に出す
llc add.ll –o – // コメント削除
add1:
leal (%rdi,%rsi), %eax
ret
Linuxの64bit環境ではrdiが第一引数, rsiが第二引数
C/C++の呼び出し規約にしたがって処理される
lealで eax ← rdi + rsiを実行
LLVMのaddが単純にx64のaddになるわけではない
x86用に出力してみる
llc add.ll –o – -march=x86
add1:
movl 4(%esp), %eax ; 一つ目の引数
addl 8(%esp), %eax ; 二つ目の引数
ret
2013/3/30 #x86opti 5 6 /19
- 7. アセンブル(2/3)
Intel形式で出してみる
llc add.ll –o – -march=x86 -x86-asm-syntax=intel
add1:
mov EAX, DWORD PTR [ESP + 4]
add EAX, DWORD PTR [ESP + 8]
ret
arm用に出力
llc add.ll –o – -march=arm
add1:
add r0, r0, r1
mov pc, lr
二項演算としては他にsub, mul, udiv(符号なし),
sdiv(符号あり), urem, srem, fadd(浮動小数)など
2013/3/30 #x86opti 5 7 /19
- 8. 比較と分岐(1/3)
二つの値の大きい方 define i32 @my_max(i32 %x, i32 %y) {
比較命令はicmp entry:
%r = icmp ugt i32 %x, %y
icmpの戻り値は br i1 %r, label %gt, label %else
1bitの変数 gt:
ugt → 符号なしgt ret i32 %x
else:
他にeq, ne, sltなど ret i32 %y
brでラベルに飛ぶ }
elseは予約語ではない
なんでもいい my_max:
cmpl %esi, %edi
jbe .LBB4_2
movl %edi, %eax
ret
.LBB4_2:
movl %esi, %eax
ret
2013/3/30 #x86opti 5 8 /19
- 9. 比較と分岐(2/3)
絶対値の場合
0より小さいかを見るにはslt(signed less than)
y = sub 0, xで-xを作る
nsw(no signed wrap)
制御の合流 define i32 @my_abs(i32 %x) {
phi命令を使う entry:
%cmp = icmp slt i32 %x, 0
br i1 %cmp, label %lt, label %else
my_abs: lt:
test edi, edi %neg = sub nsw i32 0, %x
jns else br label %exit
neg edi else:
else: br label %exit
mov eax, edi exit:
ret %ret = phi i32 [%neg,%lt], [%x,%else]
ret i32 %ret
}
2013/3/30 #x86opti 5 9 /19
- 10. 分岐(3/3)
selectを使う
cmpにしたがって値を選択
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
}
x86ではcmov cmp edi, esi
cmova esi, edi ; edi > esiならesi ← edi
mov eax, esi
ret
cmovを使わせないとジャンプ命令が使われる
-march=x86 –mattr=-cmov
2013/3/30 #x86opti 5 10 /19
- 11. メモリアクセス(1/2)
次の関数を作ってみる
void add(int *z, const int *x, const int *y) {
*z = *x + *y;
}
loadとstore命令
alignを指定するとそのalignが仮定される
armでalign 1にするとバイト単位で読むコードに展開された
x86/x64では気にしないw
define void @add(i32* %z,i32* %x,i32* %y){
entry:
%0 = load i32* %x, align 32
add: %1 = load i32* %y, align 32
mov eax,dword [rsi] %ret = add nsw i32 %0, %1
add eax,dword [rdx] store i32 %ret, i32* %z, align 32
mov dword [rdi],eax ret void
ret }
2013/3/30 #x86opti 5 11 /19
- 12. メモリアクセス(2/2)
uint128_tの足し算を作ってみる
i128を使う
そんなレジスタが無い環境(たいていの環境)でも使える
i64*をi128*にして値を読む
型変換にはbitcastを使う
define void @add(i64* %z,i64* %x,i64* %y){
entry:
%0 = bitcast i64* %x to i128*
add: %1 = bitcast i64* %y to i128*
mov rax, [rsi] %2 = load i128* %0, align 64
mov rcx, [rsi + 8] %3 = load i128* %1, align 64
add rax, [rdx] %4 = add i128 %2, %3
adc rcx, [rdx + 8] %5 = bitcast i64* %z to i128*
mov [rdi + 8], rcx store i128 %4, i128* %5, align 64
mov [rdi], rax ret void
ret }
2013/3/30 #x86opti 5 12 /19
- 13. ループ
uint64_tの配列の総和を求める
ループの更新では値の上書きができないのでphiを使う
getelementptr 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:
sum: %ip = phi i64 [0,%entry],[%i,%lp]
xor eax, eax %retp = phi i64 [0,%entry],[%ret,%lp]
test rsi, rsi %xi = getelementptr i64* %x, i64 %ip
je exit %v = load i64* %xi
lp: %ret = add i64 %retp, %v
add rax, qword [rdi] %i = add i64 %ip, 1
add rdi, 8 %i_eq_n = icmp eq i64 %i, %n
dec rsi br i1 %i_eq_n,label %exit,label %lp
jne lp exit:
exit: %r = phi i64 [0,%entry],[%ret,%lp]
ret ret i64 %r }
2013/3/30 #x86opti 5 13 /19
- 14. オーバーフロー(1/2)
多倍長演算のためにcarryを使う
組み込み関数llvm.uadd.with.overflow
使うにはdeclareが必要
戻り値は値とフラグのペア
そこから値を取り出すにはextractvalueを使う
// *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 }
2013/3/30 #x86opti 5 14 /19
- 15. オーバーフロー(2/2)
前ページのコードの出力
// *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
小さい幅のレジスタから大きい幅のレジスタへの拡張
zext(符号なし)やsext(符号あり)を使う
困った
carryをaddに加える命令が無い!
LLVMのソースコードを見ると内部的にはz=ADDE(x, y, carry)
というのがあるようだが、それを呼べない…
2013/3/30 #x86opti 5 15 /19
- 16. 多倍長整数加算の実装(1/3)
疑似コード
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); }
add_with_carryは二つのレジスタとcarryを入力とし
て加算の結果と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 }
2013/3/30 #x86opti 5 16 /19
- 17. 多倍長整数加算の実装(2/3)
作ったadd_with_carryを使って実装する
ループの一部
%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
llc uint.ll –o –
.lp:
movq (%r15), %rsi
movq (%r12), %rdi
movzbl %dl, %edx
callq add_with_carry
...
あれ、関数呼び出しのまま
2013/3/30 #x86opti 5 17 /19
- 18. 多倍長整数加算の実装(3/3)
optコマンドを使って最適化する
一度bc(ビットコード)に変換して逆アセンブルしてllcを適用
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)
関数が展開されて埋め込まれた
すばらしい!
性能については後半に続く
2013/3/30 #x86opti 5 18 /19
- 19. 1週間ほど触った雑感
よくできている
ドキュメントが充実している
コマンドエラーが親切
他のCPUの勉強がしやすい
最適化機能はかなり頑張ってる
gccのインラインアセンブラよりずっと使いやすい
プログラムコードがきれい
何をやってるのか追いかけやすい
(私にとって)いまいちなところ
想像していたよりも抽象度が高い
LLVMアセンブラと実行環境のアセンブラとの乖離
もちろん利点なのだが、うーん、それを隠蔽するかみたいな
異なるアーキテクチャのCPUを同じコードでやることのしわ寄せ
2013/3/30 #x86opti 5 19 /19