(#JuliaLang) Julia Advent Calendar 2014
12月5日担当 kimrin(twitter:kimrin)
やっぴー♡ きむりんだよ♡ 小六女子のおっさん(40)だよ♡
(ここで読者おおいに混乱w)
今日はJulia言語に興味のある方だけでなく、組み込みとか低レベルのAPI/ABIに興味のある方にも見ていただきたい内容となっております。れっつちぇきらっちょーw
さて、結論から言います。
実はJulia言語、REPLで定義した関数の
LLVM IR(LLVMの中間表現)と
Native Code(マシン語)が
リアルタイムで観れますー!!!
すごいでしょ! (俺はすごくないけどw)
さて、興味湧きまくりんぐの視聴者のみなさんはここでJuliaの処理系をダウンロードするのです! そしてREPL(Julia本体)を起動するのです!!\(^o^)/
じゃ、準備はいいかなー? 起動してみましょう。
kimuraken-no-MacBook-Prox:~ kimrin$ julia
_
_ _ _(_)_ | A fresh approach to technical computing
(_) | (_) (_) | Documentation: http://docs.julialang.org
_ _ _| |_ __ _ | Type "help()" for help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 0.3.0 (2014-08-20 20:43 UTC)
_/ |\__'_|_|_|\__'_| | Official http://julialang.org/ release
|__/ | x86_64-apple-darwin13.3.0
julia>
julia>
ってプロンプトが出ればめでたしめでたし! ちなみにjuliaは言語そのもの、Juliaが具体的にいま使ってる処理系のことだからね!(これ豆なw)
じゃ、関数を定義してみましょー!! まずはInt64の二乗を返す、簡単な関数からいってみましょー
julia> function jijo(x::Int64)
x*x
end
jijo (generic function with 1 method)
関数名がダサいのはワザとさ、ふふっw(ぉぃ ちなみに文法のおさらいしとくと、function
キーワードが関数定義の開始で、end
までが関数本体。そしてLISPチックなのだが、最後のexpressionが返り値になりまする。ここでは引数はInt64にしましたー。
もひとつ豆だが、Julia, 32bitバージョンではIntがInt32、64bitバージョンではIntがInt64だから、覚えておくといいよ♡ まぁほとんどの人がMacかLinuxで64bitつこてるみたいだけどね。
さてお立ち会い。ここでおもむろにcode_llvm
関数にjijo
を突っ込んであげると。。。
julia> code_llvm(jijo,(Int64,))
define i64 @"julia_jijo;19948"(i64) {
top:
%1 = mul i64 %0, %0, !dbg !842
ret i64 %1, !dbg !842
}
julia>
やっべー、謎言語登場www ではないですが、なんだか不思議な表現が出てきました。
これ、Julia言語の処理系が使っているLLVMの中間表現(LLVM IR)なんです!
コンパイルしなくても中間表現が覗き見できちゃうんです!!! (というか、JITコンパイルされたLLVM IRを保存しているから見られる、が正解です)
読み解いてみると、引数i64, 返り値i64のjulia_jijo:...
関数が出来ていて、
%1
という変数(正確にはSSA変数)に引数*引数が入ってるみたいですね、でこれがret
で返されるみたいな
(LLVMに詳しい方向けですが、関数定義しただけなのでmainはなく、あの謎文字列のポインタの大きさとか描いた文字列もそのままでは出ません、ですー)
う〜ん、まーべらすw
ノリノリの紀子さん(ぉぃ)になってきたところで、もういっちょやってみましょー。
julia> function うんこw(x::Int64)
x = 1
y = x
z = x
w = x + y
w
end
うんこw (generic function with 1 method)
julia>
なんという才能の無駄遣いでしょう! なんとうんこw
という関数が定義できるじゃ、あ〜りませんかー!! www ほんとJuliaたんごめんね、ごめんねw
まぁあの、こんな感じで変数とか関数とか、Cでいうところのidentifier的なところにUnicode文字が使えます。これで御社のJulia難読化が捗るね♡ (違うw)
さて、LLVM IRみてみましょー
julia> code_llvm(うんこw,(Int64,))
define i64 @"julia_?\81\86?\82\93?\81\93w;19980"(i64) {
top:
ret i64 2, !dbg !941
}
julia>
えっ、えっ、これ、2
を返してるだけだよ!! 最適化、されちゃった♡
あれ、SSA変数の説明するところだったんだけど、おかしいな〜w
ごめん、あと1分私に時間をください(CV:鈴木健二w)
julia> function うんこww(x::Int64)
x = x*x
y = x
z = x
w = x + y
w
end
うんこww (generic function with 1 method)
julia> code_llvm(うんこww,(Int64,))
define i64 @"julia_?\81\86?\82\93?\81\93ww;20093"(i64) {
top:
%1 = shl i64 %0, 1, !dbg !1281
%2 = mul i64 %1, %0, !dbg !1282
ret i64 %2, !dbg !1283
}
julia>
えーっと、返り値が w = x + y = 2*x*x
になってるので shift left
で 1bit
だけ左にシフトして2
をx
に掛けて2x
にして、そいつを次の%2
でx(%0)
掛けて2*x*x
を返してるのですな。う〜ん、マンダムw
さて、流石に変数名が汚いwので、もうちょっと綺麗な例を出したいと思います♡
julia> function きれいなジャイアン(x::Int64)
if x % 2 == 0
y = 1
else
y = 2
end
y*x
end
きれいなジャイアン (generic function with 1 method)
文句なしに綺麗です(ぉぃ) さて、こいつのLLVM IR表現みてみましょー
julia> code_llvm(きれいなジャイアン,(Int64,))
define i64 @"julia_?\81\8D?\82\8C?\81\84?\81??\82??\83??\82??\82??\83?;20133"(i64) {
top:
%1 = and i64 %0, 1, !dbg !1403
%2 = icmp eq i64 %1, 0, !dbg !1403
br i1 %2, label %L1, label %L, !dbg !1403
L: ; preds = %top
br label %L1, !dbg !1404
L1: ; preds = %top, %L
%y.0 = phi i64 [ 2, %L ], [ 1, %top ]
%3 = mul i64 %y.0, %0, !dbg !1405
ret i64 %3, !dbg !1405
}
julia>
おおっ、なんかマシン語っぽいのでてきたw じゃぁ上から見ていきましょー\(^o^)/
%0
に引数のx
が入ってまーす。で、1
との論理積とります。これ、x % 2
っすね。
で次に、icmp
命令で%1
が0
かどうか確認します。bool値的なものが結果の%2
に入ってますです。
で、次、br
命令(ブランチ命令)です。あ、言い忘れましたが、!
以降はコメントと思ってください。
i1
(整数1bit)の%2
が非ゼロなら、すなわちx % 2 == 0
のthen partだったら、label %L1
に飛んでください、じゃなければ label %L
に飛んでください、というジャンプ文みたいな表現でーす。
じゃぁ次のL:
(x % 2 != 0
)の時をみてみましょー。あれ、%L1
に飛んで、とだけ書かれてるw このとき横に ; preds = %top
って書かれているの、わかるでしょーかー。これはBasic Block (ここではラベルの次に続く命令列の塊だと思ってください)のpredecessor(流入元)が、%top
(関数の先頭のBasic Block)からだよ♡ って;
でコメントされているのです!! 確かに、L:
には%top
からしか流入していませんね。で、L:
の中身は%L1
に飛んで、ですね、わかります。
おーっとここでー!! phi
関数の登場でーす!!
まぁあの、みなさんもうすうす気がついているかと思われまするが、分岐して、分岐の先でthen partとelse part内で違う値を同じ変数に突っ込むと、ごっちんがゴッチンこw ではないですが(ぉぃ)「俺(%y.0)
には何入れたらいいか、この時点で分からんじゃないかー!!!(怒)」となるわけです。そこで調停役(違うw)のphi
関数が登場します。
phi
関数は二つの流入する変数の定義(ここではwriteされた変数だと思ってください)をまとめる作用があります。で、[ 2, %L ], [ 1, %top ]
みると分かるように、L
から来たジャイアンだった場合は定数2
を(Juliaでのelse partですね)、top
から来たジャイアンには定数1
を(then part)代入して、i64
の phi
関数の結果としてこれを %y.0
に代入してね、てへっ、みたいな感じです。あとは一直線で、yにxを掛けて、最終的に結果の%3
をret
してあげます。いっちょあがり。
そういえばL1
のところに、; preds = %top, %L
って書いてあって、これはもしかしたら値がごっちんこ♡ するかもよ〜? というわけです。いえ、全部の場合で例外なくphi
関数が入るわけではなく、then part と else part で違う値を代入した時にだけ、phi
関数の出番となります。あ、phi
はφ
とのことです。
実は(言い忘れていたのですが)、このLLVM IRの表現形式のこと、SSA形式
といいます。左辺に注目してください。どの変数も一回だけしか出てきません。つまり代入(write)が一回だけなのです! そして先ほどのφ関数もSSA形式の特徴です。
一回だけしか代入しない、その場限りの恋、なんてロマンチックなんでしょー(www)
ここで素朴な疑問です。
じゃぁφ関数はどうやってマシン語に直すの?
きたー!! 君を待って居たんだ せにょりーたw
code_native
ってのがありまして、code_llvm
同様にこれに「きれいなジャイアン」を突っ込むと、
julia> code_native(きれいなジャイアン,(Int64,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 2
push RBP
mov RBP, RSP
mov EAX, 1
Source line: 2
test DIL, 1
je 5
mov EAX, 2
Source line: 7
imul RAX, RDI
pop RBP
ret
julia>
なんと、
関数のアッセンブリが表示されます!!やたー\(^o^)/
詳しくは追いませんが、test
で判定をしていて、je 5
が(おそらく)imul
に飛んでる(y=1のまま、then part)で、else partの時だけEAX
に2
を代入して、最後imul
でy*x
を計算してRAX
に結果を残したままret
ですー(多分)
そして注目すべきはphi
関数が消えてるという点です。
詳しい話すると、LLVM IRがコンパイラのフロントエンド(LLVM IRを出力)が出力したIRをmachine independent に最適化した状態で、それを受けたコンパイラのバックエンド(機械語命令生成するところ)がかりっかりに最適化して、x86のマシン語を出力しているのでーす。で、これをJIT compilationで実行時に一気にマシン語に直しちゃうとです。すげー。
で、SSA形式は最適化がし易い形式なんです。で、これを入力として様々な最適化をコンパイラが施して、phi
関数を消して、命令列を作り出すのです。ドラマッチックw
さて、LLVM IRとNative Codeの両方が手に取るようにわかりますね。
浮動小数点、いってみよー\(^o^)/
julia> function 限りなくfloatに近いfloat64(x::Float64)
x * x * x
end
限りなくfloatに近いfloat64 (generic function with 1 method)
julia> code_llvm(限りなくfloatに近いfloat64, (Float64,))
define double @"julia_?\99\90?\82\8A?\81??\81\8Ffloat?\81??\91?\81\84float64;20168"(double) {
top:
%1 = fmul double %0, %0, !dbg !1502
%2 = fmul double %1, %0, !dbg !1502
ret double %2, !dbg !1502
}
julia> code_native(限りなくfloatに近いfloat64, (Float64,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 2
push RBP
mov RBP, RSP
Source line: 2
movaps XMM1, XMM0
mulsd XMM1, XMM1
mulsd XMM1, XMM0
movaps XMM0, XMM1
pop RBP
ret
julia>
自分おっさんなので、関数名も古めです(どやっw)
LLVM IRみるとfmul
命令で2回掛け算してますね。おっすおっす。
native code みてみましょー。
XMM?
使って掛け算しているの、わかるでしょうかー。なんと、Julia言語は浮動小数点の計算にSSE命令
を使うのですー(>_<) きゃーかっこいー抱いてーwww(俺じゃなくてJuliaたんにw)
まぁあの、読者の方も気がついていると思いますが、この記事全体がパロディな訳でして、もうね、コンパイラ持ってけ泥棒状態ですw
あと、インストラクションはマシンによって異なりまーす。あれがあって、あれがなくて、とかあるでしょw
最後に、code_typed
をご紹介。
julia> code_typed(限りなくfloatに近いfloat64, (Float64,))
1-element Array{Any,1}:
:($(Expr(:lambda, {:x}, {{},{{:x,Float64,0}},{}}, :(begin # none, line 2:
return (top(box))(Float64,(top(mul_float))((top(box))(Float64,(top(mul_float))(x::Float64,x::Float64))::Float64,x::Float64))::Float64
end::Float64))))
julia>
僕も詳しくは分かりませんが(すまん)型付きのλ式みたいなのに、なっているのかなぁ、、、あ、みちゃいけないもの、みちゃった♡ って感じですw
さていかがだったでしょうか。みなさんも嫁(Juliaたん)片手に、バイナリハック、してみませんかーw
以上文責kimrinでした。ではねちゃおーw