疑似オブジェクトの比較
OCamlでは、あるまとまった関数とか変数とかをまとめるとき、大きく分けて3つのやり方があると思います。
- レコード
- モジュール
- クラス
クラスはちょっとあれですが、レコードとモジュールは大変広く使われていますし、ほとんどの用はそれで足りると思います。
クラスが必要そうな場面で思い浮かぶのが、ゲームとかのオブジェクトたちでしょうか。例えば共通のインターフェースを持つけれども、実装がそれぞれ異なるようなものです。また、内部で利用するデータとかもそれぞれであるため、そのあたりの共通化が必要になったりする、と思います。
そうなると、クラスを作成して継承して〜というのが、オブジェクト指向言語的なやりかただと思いますが、OCamlではそれは時に自殺行為になりそうです。
こういうオブジェクトを作成するとき、他の関数型言語、例えばHaskellでは、クロージャにして、データを内包してやって、作成した関数をレコードとかにして返したりしているようです。当然、関数がファーストクラスであるOCamlでもクロージャが作れます。
しかし、こうやった場合、クラスとかと比較してどれくらいパフォーマンスが違うんだべ?というどうでもいい疑問が浮かびました。多分こんなもの比較する必要は無いと思いますが、今迄使ったことのない一級モジュールとかも使ってみたかったので、簡単にですがやってみました。
速度比較
比較する条件は簡単に以下の通りです。
というわけで、こんな感じのプログラムで測ってみました。正確じゃないとかはとりあえず気にしない。
type test = { func : unit -> unit; get: unit -> int } let make_record init = let init_ref = ref init in {func = (fun () -> init_ref := succ !init_ref); get = fun () -> !init_ref} module type T = sig val func : unit -> unit val get : unit -> int end let make_module init : (module T) = let init_ref = ref init in (module struct let func () = init_ref := succ !init_ref let get () = !init_ref end : T) class clazz init = object val mutable init_ref = init method func = init_ref <- succ init_ref method get = init_ref end let () = let time = Unix.gettimeofday () in let record = make_record 0 in begin for i = 0 to 10000000 do record.func () done; Printf.printf "function call from record : %f : %d\n" ((Unix.gettimeofday ()) -. time) (record.get ()) end; let time = Unix.gettimeofday () in let module M = (val (make_module 1) : T) in begin for i = 0 to 10000000 do M.func () done; Printf.printf "function call from module : %f : %d\n" ((Unix.gettimeofday ()) -. time) (M.get ()) end; let time = Unix.gettimeofday () in let c = new clazz 2 in begin for i = 0 to 10000000 do c#func done; Printf.printf "function call from class : %f : %d\n" ((Unix.gettimeofday ()) -. time) c#get end
これをocamlc/ ocamloptでコンパイルして実行してみた結果が以下です。
# ocamlc % ./a.out function call from record : 0.240140 : 10000001 function call from module : 0.238692 : 10000002 function call from class : 0.273343 : 10000003 % ./a.out function call from record : 0.245692 : 10000001 function call from module : 0.243844 : 10000002 function call from class : 0.277076 : 10000003 % ./a.out function call from record : 0.244651 : 10000001 function call from module : 0.238968 : 10000002 function call from class : 0.271432 : 10000003 % ./a.out function call from record : 0.244047 : 10000001 function call from module : 0.243378 : 10000002 function call from class : 0.274625 : 10000003 % ./a.out function call from record : 0.244622 : 10000001 function call from module : 0.239441 : 10000002 function call from class : 0.273458 : 10000003 #ocamlopt % ./a.out function call from record : 0.030798 : 10000001 function call from module : 0.030720 : 10000002 function call from class : 0.095447 : 10000003 % ./a.out function call from record : 0.031522 : 10000001 function call from module : 0.031542 : 10000002 function call from class : 0.095843 : 10000003 % ./a.out function call from record : 0.031546 : 10000001 function call from module : 0.031213 : 10000002 function call from class : 0.095048 : 10000003 % ./a.out function call from record : 0.030902 : 10000001 function call from module : 0.030727 : 10000002 function call from class : 0.095413 : 10000003 % ./a.out function call from record : 0.031276 : 10000001 function call from module : 0.030987 : 10000002 function call from class : 0.096431 : 10000003
結果としては、どちらの場合でも 、一級モジュール >= レコード > オブジェクト の順でした。特にocamloptでは、オブジェクトの実行速度が他の3倍くらいになってしまってますが、なんでなのかはわかりません。
これを見ると、手軽さとかを考えたとしても、共通インターフェースをレコードに切り出して、クロージャを含むレコードを返すようにした方が手っ取り早そうです。また、一級モジュールも初めて使ってみましたが、こちらの速度も中々です。こちらは、モジュールから別の型を返すようなものも作れそうですが、いかんせん煩雑なので、まだ私では使いどころが今一、いや今二くらい掴めません。
結果を見る限り、やはりOCamlではクラスを積極的に利用する機会というのはそんなに無い、としてもいい感じみたいです。
メモリ比較
どうせなんでやってみました。正確でもなんでもなく、こんなもんか、という感じで御覧下さい。条件は以下の通り
- 速度比較で使ったレコード/モジュール/クラスをリストに格納
- 件数は1,000,000件
- メモリは、Gc.allocated_bytesで見る(どれを使えばより正確になるのか・・・?)
使ったプログラムはこちら。
type test = { func : unit -> unit; get: unit -> int } let make_record init = let init_ref = ref init in {func = (fun () -> init_ref := succ !init_ref); get = fun () -> !init_ref} module type T = sig val func : unit -> unit val get : unit -> int end let make_module init : (module T) = let init_ref = ref init in (module struct let func () = init_ref := succ !init_ref let get () = !init_ref end : T) class clazz init = object val mutable init_ref = init method func = init_ref <- succ init_ref method get = init_ref end let () = (* レコード *) let list = ref [] in begin for i = 0 to 1000000 do list := (make_record i) :: !list done; Printf.printf "allocated size in record : %f MiBs\n" ((Gc.allocated_bytes ()) /. 1024.0 /. 1024.0) end; Thread.delay 10.0 (* モジュール *) (* let list = ref [] in *) (* begin *) (* for i = 0 to 1000000 do *) (* list := (make_module i) :: !list *) (* done; *) (* Printf.printf "allocated size in module : %f\n" (Gc.allocated_bytes ()) *) (* end; *) (* オブジェクト *) (* let list = ref [] in *) (* begin *) (* for i = 0 to 1000000 do *) (* list := (new clazz i) :: !list *) (* done; *) (* Printf.printf "allocated size in module : %f\n" (Gc.allocated_bytes ()) *) (* end *)
別々に作るのもあれだったし、そんなに手間がかかるわけでもないので、コンパイルしなおして実行しました。実行結果はこちらです。すべてocamloptでコンパイルした後です。
# レコード allocated size in record : 122.076904MiB # モジュール allocated size in record : 122.076904MiB # クラス allocated size in record : 91.560501 MiB
クラスだけかなり少ない値になっています。何回実行してもこれだったので、そもそもの構造がレコード/モジュールと全く違うとしか思えません。実際違うんでしょうけど。