ocamloptの全体像

Posted on February 13, 2013 / Tags: ocaml, internal

Table of contents


発端

ocamloptを読むことになりました。 - Togetter

Camlp4だけはこわいんでゲソ… 逃げるしかないでゲソ。

考えてみると、確かにアラフラにはjhcを採用する予定でゲソが、 ひょっとしたらこのデザインに致命的な欠陥が見つかる可能性も高いでゲソ。 その場合、OCamlは代替案として大変有望でゲソ。 今のうちに調べておいて損はないんじゃなイカ?

簡単なプログラム を作ってみて、コンパイル結果とocamloptを比較すればだいたいわかるはずでゲソ。

$ uname -a
Linux casper 3.2.0-4-amd64 #1 SMP Debian 3.2.35-2 x86_64 GNU/Linux
$ pwd
/home/kiwamu/src/read_ocamlopt
$ ocamlopt -version
4.00.1
$ cat helloworld/helloworld.ml
let hello _ = print_endline "Hello world!";;
let _ = hello ();
$ make
ocamlopt -verbose -dstartup -o helloworld/helloworld helloworld/helloworld.ml
+ as -o 'helloworld/helloworld.o' '/tmp/camlasm034093.s'
+ as -o '/tmp/camlstartup43810e.o' 'helloworld/helloworld.startup.s'
+ gcc -o 'helloworld/helloworld'   '-L/usr/local/lib/ocaml'  '/tmp/camlstartup43810e.o' '/usr/local/lib/ocaml/std_exit.o' 'helloworld/helloworld.o' '/usr/local/lib/ocaml/stdlib.a' '/usr/local/lib/ocaml/libasmrun.a' -lm  -ldl

ということは以下のファイルについて調べればocamloptの全体像がわかるはずでゲソ。

起動プロセスについて

$ pwd
/home/kiwamu/src/read_ocamlopt/helloworld
$ gdb helloworld
(gdb) b camlHelloworld__hello_1030
Breakpoint 1 at 0x403560
(gdb) run
Starting program: /home/kiwamu/src/read_ocamlopt/helloworld/helloworld
warning: Could not load shared library symbols for linux-vdso.so.1.
Do you need "set solib-search-path" or "set sysroot"?

Breakpoint 1, 0x0000000000403560 in camlHelloworld__hello_1030 ()
(gdb) bt
#0  0x0000000000403560 in camlHelloworld__hello_1030 ()
#1  0x0000000000403591 in camlHelloworld__entry ()
#2  0x00000000000003e8 in ?? ()
#3  0x0000000000403229 in caml_program ()
#4  0x000000000002950d in ?? ()
#5  0x0000000000411a52 in caml_start_program ()
#6  0x0000000000000000 in ?? ()

スタックが小細工されているけれど、 caml_start_program関数を呼び出すのはasmrun/startup.cなので、 以下のようなコールグラフになるはずでゲソ。

caml_main (asmrun/startup.c)
=> caml_init_custom_operations(); /* これocamlopt nativeでも必要? */
   caml_register_custom_operations(&caml_int32_ops);
   caml_register_custom_operations(&caml_nativeint_ops);
   caml_register_custom_operations(&caml_int64_ops);
parse_camlrunparam(); /* OCAMLRUNPARAM環境変数の解析 */
=> caml_init_gc (minor_heap_init, heap_size_init, heap_chunk_init,
                 percent_free_init, max_percent_free_init);
   caml_page_table_initialize(Bsize_wsize(minor_size) + major_heap_size);
   caml_set_minor_heap_size (Bsize_wsize (norm_minsize (minor_size)));
   caml_major_heap_increment = Bsize_wsize (norm_heapincr (major_incr));
   caml_percent_free = norm_pfree (percent_fr);
   caml_percent_max = norm_pmax (percent_m);
   caml_init_major_heap (major_heap_size);
init_atoms();
/* なんだろ?Initialize the atom table and the static data and code area limits. */
caml_init_signals();
/* スタックあふれ検出 */
sigsetjmp(caml_termination_jmpbuf.buf, 0); /* 例外かな? */
=> caml_start_program (asmrun/amd64.S)
   /* r12-r15はcallee-savedなので、レジスタで取りまわすことはないはず... */
   push caml_gc_regs
   push caml_last_return_address
   push caml_bottom_of_stack
   /* caml_young_ptrをr15にロード */
   /* caml_exception_pointerをr14にロード */
   /* Exception handlerをr13にロード */
   push r13
   push r14
   /* スタックポインタをr14にロード */
   /* r12にentry_pointを取りcall */
   => caml_program (helloworld.startup.s)
      => camlPervasives__entry
         => register_named_value "Pervasives.do_at_exit" do_at_exit
            => caml_register_named_value /* プロセス終了時にflushするように */
      caml_globals_inited += 1
      => camlHelloworld__entry /* <= OCamlコード */

ところで、上記のような解析をする際にはocamloptでコンパイルされた実行バイナリをobjdump -Sで見てみると良いでゲソ。 C言語の場合と同じように元のOCamlソースコードがまざって見えるので読みやすいでゲソ。 OCamlは本当に取り回しの良い、安心して使える言語でゲソ。 これが歴史の重みという奴じゃなイカ?

ランタイムについて

んー。やっぱり無茶苦茶多いじゃなイカ… でもこれ全部本当に必要なのカ? 機能を限定すれば削れそうなものもありそうでゲソ。 ちょっと今はocamloptを本格的に使うかわからないので、これらの詳細調査は遠慮したいでゲッソ。

もし使う時が来たら、また 簡約! λカ娘(4) - 参照透明な海を守る会 でやったようにランタイムのサイズ削減をやってみれば、自然に中身に手垢をつけられるはずでゲソ。 その時が楽しみでゲッソ!

$ size /usr/local/lib/ocaml/libasmrun.a|grep libasmrun.a|sort -k 6
   1696       0       0    1696     6a0 alloc.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1108      24       0    1132     46c amd64.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3656       0       0    3656     e48 array.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1342       8      16    1366     556 backtrace.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1150       0     104    1254     4e6 callback.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3165       0       8    3173     c65 compact.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2585      16    6144    8745    2229 compare.o (ex /usr/local/lib/ocaml/libasmrun.a)
    633       0      16     649     289 custom.o (ex /usr/local/lib/ocaml/libasmrun.a)
     94      12       4     110      6e debugger.o (ex /usr/local/lib/ocaml/libasmrun.a)
    421       0       0     421     1a5 dynlink.o (ex /usr/local/lib/ocaml/libasmrun.a)
   6569      24   20640   27233    6a61 extern.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1272      40      40    1352     548 fail.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1815       0      56    1871     74f finalise.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3836       0       0    3836     efc floats.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2791      64    8056   10911    2a9f freelist.o (ex /usr/local/lib/ocaml/libasmrun.a)
   4628       0      72    4700    125c gc_ctrl.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1524       0     476    2000     7d0 globroots.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2369       0      24    2393     959 hash.o (ex /usr/local/lib/ocaml/libasmrun.a)
   7196      16    6240   13452    348c intern.o (ex /usr/local/lib/ocaml/libasmrun.a)
   6343     184       0    6527    197f ints.o (ex /usr/local/lib/ocaml/libasmrun.a)
   7217      56      40    7313    1c91 io.o (ex /usr/local/lib/ocaml/libasmrun.a)
    938       0       0     938     3aa lexing.o (ex /usr/local/lib/ocaml/libasmrun.a)
     80       0       0      80      50 main.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3960       0      80    4040     fc8 major_gc.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3529       0       0    3529     dc9 md5.o (ex /usr/local/lib/ocaml/libasmrun.a)
   3256       0      40    3296     ce0 memory.o (ex /usr/local/lib/ocaml/libasmrun.a)
    310       0       0     310     136 meta.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2373       0     200    2573     a0d minor_gc.o (ex /usr/local/lib/ocaml/libasmrun.a)
    701       0       8     709     2c5 misc.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1721       0       0    1721     6b9 natdynlink.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1531       0       0    1531     5fb obj.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2220       0       4    2224     8b0 parsing.o (ex /usr/local/lib/ocaml/libasmrun.a)
    917       0       0     917     395 printexc.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2316       8      80    2404     964 roots.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1347      24      32    1403     57b signals.o (ex /usr/local/lib/ocaml/libasmrun.a)
    804       0    8200    9004    232c signals_asm.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1502      48     288    1838     72e startup.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1261       0       4    1265     4f1 str.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2706      36       8    2750     abe sys.o (ex /usr/local/lib/ocaml/libasmrun.a)
    202       0       0     202      ca terminfo.o (ex /usr/local/lib/ocaml/libasmrun.a)
   1532       0       0    1532     5fc unix.o (ex /usr/local/lib/ocaml/libasmrun.a)
   2181       8      16    2205     89d weak.o (ex /usr/local/lib/ocaml/libasmrun.a)

コンパイルパイプラインについて

まずはコールグラフを書いてみたでゲソ。

main
Arg.parse (Arch.command_line_options @ Options.list) anonymous usage;
=> process_file Format.err_formatter
   => process_implementation_file ppf name
      => Optcompile.implementation ppf name opref (* <= コンパイルパイプライン本体 *)
         let (++) x f = f x in
         let (+++) (x, y) f = (x, f y) in
         let inputfile = Pparse.preprocess sourcefile in
         Pparse.file ppf inputfile Parse.implementation ast_impl_magic_number
             (* :: Parsetree.structure *)
         ++ print_if ppf Clflags.dump_parsetree Printast.implementation
             (* :: Parsetree.structure -> Parsetree.structure *)
         ++ Typemod.type_implementation sourcefile outputprefix modulename env
             (* :: Parsetree.structure -> Typedtree.structure * Typedtree.module_coercion *)
         ++ Translmod.transl_store_implementation modulename
             (* :: Typedtree.structure * Typedtree.module_coercion -> int * Lambda.lambda *)
         +++ print_if ppf Clflags.dump_rawlambda Printlambda.lambda
             (* :: Lambda.lambda -> Lambda.lambda *)
         +++ Simplif.simplify_lambda
             (* :: Lambda.lambda -> Lambda.lambda *)
         +++ print_if ppf Clflags.dump_lambda Printlambda.lambda
             (* :: Lambda.lambda -> Lambda.lambda *)
         ++ Asmgen.compile_implementation outputprefix ppf;
             (* :: int * Lambda.lambda -> unit *)
         Compilenv.save_unit_info cmxfile; (* <= コンパイル済みcurrent_unitをファイル書き込み *)
=> Asmlink.link ppf (List.rev !objfiles) target;
   make_startup_file ppf startup units_tolink; (* <= helloworld.startup.sファイル生成 *)
   => Proc.assemble_file startup startup_obj
      Ccomp.command (Config.asm ^ " -o " ^
        Filename.quote outfile ^ " " ^ Filename.quote infile)
      (* gasでオブジェクトファイルに *)
   call_linker (List.map object_file_name objfiles) startup_obj output_name;
   => Ccomp.call_linker mode output_name files c_lib;

少し混乱しやすいでゲソが、 Argモジュールは単にコマンド引数を解析するだけでなく、 標準ライブラリArgモジュール にある通り、anonymous関数を引数を適用するんでゲソ。

Optcompile.implementation関数がコンパイルパイプラインの本体で、 Haskellのdoと比較すると、 ++ が >> で、 +++ が >>= みたいな感じでゲソ。 あくまで感じであってモナドとかそんな高尚なもんじゃないでゲソ。 つまり内側から外側に向かって処理が進むようでゲソ。 最初ギョっとするでゲソが、慣れれば読みやすいでゲソ。 print_ifは無視できるので、コンパイルパイプラインを抜き出すと…

let inputfile = Pparse.preprocess sourcefile in
Pparse.file ppf inputfile Parse.implementation ast_impl_magic_number
    (* :: Parsetree.structure *)
++ Typemod.type_implementation sourcefile outputprefix modulename env
    (* :: Parsetree.structure -> Typedtree.structure * Typedtree.module_coercion *)
++ Translmod.transl_store_implementation modulename
    (* :: Typedtree.structure * Typedtree.module_coercion -> int * Lambda.lambda *)
+++ Simplif.simplify_lambda
    (* :: Lambda.lambda -> Lambda.lambda *)
++ Asmgen.compile_implementation outputprefix ppf;
    (* :: int * Lambda.lambda -> unit *)

それでは、このコンパイルパイプラインに注目して再度コールグラフを書いてみようじゃなイカ。

Optcompile.implementation ppf name opref
=> let inputfile = Pparse.preprocess sourcefile in
   let comm = Printf.sprintf "%s %s > %s" pp (Filename.quote sourcefile) tmpfile in
   Ccomp.command comm (* 指定されたプリプロセッサコマンドにかける *)
=> Pparse.file ppf inputfile Parse.implementation ast_impl_magic_number
   Location.input_name := inputfile;
   let lexbuf = Lexing.from_channel ic in
   Location.init lexbuf inputfile;
   => Parse.implementation lexbuf
      => wrap Parser.implementation (* パーサ本体 *)
=> ++ Typemod.type_implementation sourcefile outputprefix modulename env
   let (str, sg, finalenv) = type_structure initial_env ast Location.none in
   let simple_sg = simplify_signature sg in
   check_nongen_schemes finalenv str.str_items;
   normalize_signature finalenv simple_sg;
   let coercion = Includemod.compunit sourcefile sg "(inferred signature)" simple_sg in
   Typecore.force_delayed_checks (); (* 型推論の本体 *)
   (str, coercion) (* :: Typedtree.structure * Typedtree.module_coercion *)
=> ++ Translmod.transl_store_implementation modulename
   => transl_store_gen module_name (str, restr) false
      let (map, prims, size) = build_ident_map restr (defined_idents str) in
      let f = function
        | [ { str_desc = Tstr_eval expr } ] when topl ->
            assert (size = 0);
            subst_lambda !transl_store_subst (transl_exp expr)
        | str -> transl_store_structure module_id map prims str in
      transl_store_label_init module_id size f str
      (* λ項に変換してるらしい、返値のintはサイズみたい *)
=> +++ Simplif.simplify_lambda
   => let lam = simplify_exits lam (* なんだろう? *)
   => simplify_lets lam
      (* 最適化。だいたい以下?
         * コンパイル時β簡約
         * 未使用のletを削除。またlet参照を値にする *)
=> ++ Asmgen.compile_implementation outputprefix ppf;
   (* λ項からアセンブリコードへ *)
   Emit.begin_assembly(); (* アセンブラファイルのヘッダを書き込む *)
   => Closure.intro size lam (* :: Clambda.ulambda *)
      let (ulam, approx) = close Tbl.empty Tbl.empty lam in ulam
      (* アンカリー化、直接呼び出し *)
   ++ Cmmgen.compunit size (* :: Clambda.ulambda -> Cmm.phrase list *)
   (* λ項からC--へ *)
   ++ List.iter (compile_phrase ppf) (* :: Cmm.phrase list -> unit *)
   compile_phrase ppf
     (Cmmgen.reference_symbols
        (List.filter (fun s -> s <> "" && s.[0] <> '%')
           (List.map Primitive.native_name !Translmod.primitive_declarations))
     ); (* 関数とデータをアセンブラファイルに書き込む *)
   Emit.end_assembly(); (* アセンブラファイルのフッタを書き込む *)
   Proc.assemble_file asmfile (prefixname ^ ext_obj) (* gasでオブジェクトファイルに *)

このコンパイルパイプラインが実際には一瞬で完了するのだから、すごいでゲソ。

blog comments powered by Disqus