Arantium Maestum

プログラミング、囲碁、読書の話題

OCamlのlambda IRをいじる(関数)

前回に続いて、ローカルとモジュールレベルでの関数定義がどうlambda IRにコンパイルされるかを見ていく。

ローカル関数

まずはこんな感じの簡単な関数:

let () =
  let f x = x + 1 in
  print_int @@ f 5

ocamlopt -c -dlambda localfunc.mlなどとすると:

(seq
  (let
    (*match*/84 =
       (apply (field 43 (global Stdlib!)) (let (x/85 =[int] 5) (+ x/85 1))))
    0a)
  0a)

見てのとおり、関数定義が消え、呼び出しがletに変換される、というインライン化が行われている。

ちなみにlambda IR変換ではいくつかの最適化も行われる。そんな最適化をする前の「変換しただけのlambda IR」をみたい場合は-dlambdaではなく-drawlambdaフラグを渡す。

ocamlopt -c -drawlambda localfunc.mlの結果:

(seq
  (let
    (*match*/84 =
       (let (f/80 = (function x/82[int] : int (+ x/82 1)))
         (dirapply (field 43 (global Stdlib!)) (apply f/80 5))))
    0a)
  0a)

しっかり関数定義・呼び出しが残っているのがわかる。興味深いポイントとしては:

  • (function x/82[int] : int (+ x/82 1))のように整数を受け取る・返す場合は型アノテーションが残っている。返り値が文字列などの場合はこのような型アノテーションはないので、=[int]と同じくboxing省略の関係なのだと思うがまだよくわからない
  • 見慣れないdirapplyというキーワードが(print_int呼び出しのために)出てくる。applyとの違いはよくわからない。direct applyの意味だと思うが・・・

引数2のローカル関数

引数2の場合も前回と同様:

let () =
  let f x y = x + y in
  print_int @@ f 1 2

-dlambdaだと:

(seq
  (let
    (*match*/85 =
       (apply (field 43 (global Stdlib!))
         (let (x/86 =[int] 1 y/87 =[int] 2) (+ x/86 y/87))))
    0a)
  0a)

やはり関数定義・呼び出しが変換で消えている。

-drawlambdaだと残っている&型アノテーションつき:

(seq
  (let
    (*match*/85 =
       (let (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83)))
         (dirapply (field 43 (global Stdlib!)) (apply f/80 1 2))))
    0a)
  0a)

ローカル関数の部分適用

先ほどのローカル関数を部分適用したものを別の変数に束縛してみる:

let () =
  let f x y = x + y in
  let g = f 1 in
  print_int @@ g 2

すると-dlambdaでも関数定義・呼び出しのまま残る:

(seq
  (let
    (*match*/86 =
       (let
         (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83))
          g/84 = (apply f/80 1))
         (apply (field 43 (global Stdlib!)) (apply g/84 2))))
    0a)
  0a)

別のIR変換ステップ(例えばclosureつきのclambdaへの変換)で最適化される可能性はあるが、少なくともlambda IRの段階ではこのような簡単な部分適用でも最適化が行われなくなる、ということは面白い。

引数が関数適用の結果

関数の引数部分に別の関数適用の式を入れてみる:

let () =
  let f x y = x + y in
  let g x = x - 1 in
  print_int @@ f 1 (g 2)

この場合は-dlambdaだと普通に関数がインライン化されてletに置き換わる:

(seq
  (let
    (*match*/88 =
       (apply (field 43 (global Stdlib!))
         (let (x/90 =[int] 1 y/91 =[int] (let (x/89 =[int] 2) (- x/89 1)))
           (+ x/90 y/91))))
    0a)
  0a)

fの適用とgの適用で別々のネストしたlet式になっている。(よく考えると自然な変換ではある)

関数定義内で別のローカル関数を使う

まずローカル関数gを定義し、それをローカル関数fの定義内で使う:

let () =
  let g x = x + 1 in
  let f x y = x + (g y) in
  print_int @@ f 1 2

-dlambdaコンパイルすると関数定義は両方消える:

(seq
  (let
    (*match*/88 =
       (apply (field 43 (global Stdlib!))
         (let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1)))))
    0a)
  0a)

思ったよりシンプルな形になっている。

(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1)))

の部分が

(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (let (x/92 =[int] y/91) (+ x/92 1))))

になるかと思っていた。

変数を別の変数に束縛

関数関係なくこのようなコードを試してみたら:

let () =
  let x = 1 in
  let y = x in
  print_int @@ x + y

-dlambdaだと:

(seq
  (let
    (*match*/83 =
       (let (x/80 =[int] 1)
         (apply (field 43 (global Stdlib!)) (+ x/80 x/80))))
    0a)
  0a)

yが消えている。

-drawlambdaだと:

(seq
  (let
    (*match*/83 =
       (let (x/80 =[int] 1 y/81 =[int] x/80)
         (dirapply (field 43 (global Stdlib!)) (+ x/80 y/81))))
    0a)
  0a)

yが残っているので、lambda IR内での最適化として意味のないletによる変数束縛は省略されるようだ。

モジュールトップレベルでの関数定義

let f x = x + 1

let () = print_int @@ f 5

-dlambda

(seq
  (let (f/80 = (function x/82[int] : int (+ x/82 1)))
    (setfield_ptr(root-init) 0 (global Func!) f/80))
  (let
    (*match*/85 =
       (apply (field 43 (global Stdlib!)) (apply (field 0 (global Func!)) 5)))
    0a)
  0a)

正直あまり面白みはない。トップレベルにある関数に関してはlambda IRの段階ではインライン化は効かない。

次回

次はタプルがどうコンパイルされるかをみていく。