疑似オブジェクトの比較

OCamlでは、あるまとまった関数とか変数とかをまとめるとき、大きく分けて3つのやり方があると思います。

  • レコード
  • モジュール
  • クラス

クラスはちょっとあれですが、レコードとモジュールは大変広く使われていますし、ほとんどの用はそれで足りると思います。

クラスが必要そうな場面で思い浮かぶのが、ゲームとかのオブジェクトたちでしょうか。例えば共通のインターフェースを持つけれども、実装がそれぞれ異なるようなものです。また、内部で利用するデータとかもそれぞれであるため、そのあたりの共通化が必要になったりする、と思います。
そうなると、クラスを作成して継承して〜というのが、オブジェクト指向言語的なやりかただと思いますが、OCamlではそれは時に自殺行為になりそうです。

こういうオブジェクトを作成するとき、他の関数型言語、例えばHaskellでは、クロージャにして、データを内包してやって、作成した関数をレコードとかにして返したりしているようです。当然、関数がファーストクラスであるOCamlでもクロージャが作れます。
しかし、こうやった場合、クラスとかと比較してどれくらいパフォーマンスが違うんだべ?というどうでもいい疑問が浮かびました。多分こんなもの比較する必要は無いと思いますが、今迄使ったことのない一級モジュールとかも使ってみたかったので、簡単にですがやってみました。

速度比較

比較する条件は簡単に以下の通りです。

  • それぞれ10,000,000回ずつクロージャ/メソッドを呼び出す
  • ちゃんと求められている値が結果としてクロージャ/メソッドから取得できる

というわけで、こんな感じのプログラムで測ってみました。正確じゃないとかはとりあえず気にしない。

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

クラスだけかなり少ない値になっています。何回実行してもこれだったので、そもそもの構造がレコード/モジュールと全く違うとしか思えません。実際違うんでしょうけど。

まとめ
  • 無理してクラス作るよりも、レコードとか一級モジュールとクロージャを併せた方がいいかも。
  • クラスのメモリ消費が予想以上に少ない

何げない調査でしたが、以外と得るものがあったと思います。まずはクロージャを使いこなせるように頑張ります。s