Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Advantage of `let rec` bindings vs regular `let` bindings in OCaml

Tags:

ocaml

This question is sort-of a follow-up question to this SO question: How to make a covariant observable in OCaml

The accepted answer's author has (casually) noted that using let rec to bind two independent values would be more "economical" than two separate let bindings.

let make x =
  let queue = Queue.create () in
  let obj = x in
  let watch f = Queue.add f queue in
  let notify () = Queue.iter (fun f -> f x) queue in
  { obj; watch; notify; }

vs

let make x =
  let queue = Queue.create () in
  let obj = x in
  let rec watch f = Queue.add f queue
  and notify () = Queue.iter (fun f -> f x) queue in
  { obj; watch; notify; }

Was his statement correct? If so, why is the second version more "economical"?

like image 655
Ramon Snir Avatar asked Feb 01 '14 16:02

Ramon Snir


1 Answers

As I stated already in my comment, it seems that by using let rec, you avoid creating more closures. To inspect this, I created two slightly different files shown here:

test1.ml has the "usual" way without let rec:

let test1 x =
    let x = 5 in
    let w () = x + 1 in
    let n () = x + 1 in
    w () + n ()

On the other hand, test2.ml uses let rec:

let test2 x =
    let x = 5 in
    let rec w () = x + 1 
    and n () = x + 1 in
    w () + n ()

I then ocamlc -dinstr'd both files (i.e. I generated bytecode for both files) and obtained the following:

For test1.ml, I got:

    branch L2
L3: envacc 1
    offsetint 1
    return 1
L4: envacc 1
    offsetint 1
    return 1
L1: const 5
    push
    acc 0
    closure L4, 1
    push
    acc 1
    closure L3, 1
    push
    const 0a
    push
    acc 1
    apply 1
    push
    const 0a
    push
    acc 3
    apply 1
    addint
    return 4
L2: closure L1, 0
    push
    acc 0
    makeblock 1, 0
    pop 1
    setglobal Closuretest!

The file test2.ml resulted in the following:

    branch L2
L3: envacc 3
    offsetint 1
    return 1
L4: envacc 1
    offsetint 1
    return 1
L1: const 5
    push
    acc 0
    closurerec 3 4, 1
    const 0a
    push
    acc 1
    apply 1
    push
    const 0a
    push
    acc 3
    apply 1
    addint
    return 4
L2: closure L1, 0
    push
    acc 0
    makeblock 1, 0
    pop 1
    setglobal Closuretest2!

From a Caml Virtual Machine — Instruction set Document (unfortunately, I don't know if it even is the official one, but it looks reasonable), it seems that the instructions closure and closurerec generate a closure on the stack. As you can see, the bytecode for test1.ml generates 3 closures in total, while test2.ml only generates two closures (one via closurerec).

I am no assembly guru, but you can test ocamlopt -S filename.ml, so that the compiler leaves (and does not delete) the assembly (then in filename.s), where you can possibly spot analogous differences, as well.

like image 195
phimuemue Avatar answered Sep 24 '22 02:09

phimuemue