Gc.finalise and let open struct .. end

Dear All,

I have the following two tests, with the test output in comments after each test:

let test2 () =
  Printf.printf "Test 2\n%!";
  let x = ref "hello" in 
  let _ = Gc.finalise (fun _ -> Printf.printf "x has been GC'ed\n") x in
  let _ = Gc.full_major () in
  let _ = Printf.printf "This should be printed after the 'x has been GC'ed' message\n" in
  ()


(*
Test 2
x has been GC'ed
This should be printed after the 'x has been GC'ed' message
*)


let test3 () = 
  let open struct
    let _ = Printf.printf "Test 3\n%!"
    let x = ref "hello" 
    let _ = Gc.finalise (fun _ -> Printf.printf "x has been GC'ed\n") x 
    let _ = Gc.full_major () 
    let _ = Printf.printf "This should be printed after the 'x has been GC'ed' message\n"
  end 
  in
  ()

(*
Test 3
This should be printed after the 'x has been GC'ed' message

NOTE: the finaliser is not run, and x is not GC'ed
*)

For test3, I naively expected it might behave the same as test2. But it doesn’t (the heap value x is not GC’ed by the full_major). Can someone explain what is happening? Thanks

1 Like

I guess one possible interpretation is: all values in a structure are reachable during the structure initialization phase (i.e., when the toplevel bindings in the structure are evaluated).

Can someone confirm this is correct (if it is)?

I don’t observe any difference between your two tests (with a flambda-enable compiler)?
Notably, even your second test seems to depend on some native backend optimization: it fails for me with the bytecode compiler.

I assume that you’re using the native compiler (in bytecode, let-bound variables are kept alive as long as they’re in scope).
The difference between the two tests is that struct (* ... *) let x = ref "hello" (* ... *) end evaluates to a structure that contains x. So x needs to stay alive for the whole duration of the structure.
But you can get around it by constraining the signature of your module so that it does not include x:

let test3 () = 
  let open (struct
    let _ = Printf.printf "Test 3\n%!"
    let x = ref "hello" 
    let _ = Gc.finalise (fun _ -> Printf.printf "x has been GC'ed\n") x 
    let _ = Gc.full_major () 
    let _ = Printf.printf "This should be printed after the 'x has been GC'ed' message\n"
  end : sig end)
  in
  ()

This should give you the same results as test2.

Flambda is better at removing unused allocations. In this case the module being opened is not used anywhere, so Flambda will remove the corresponding allocation. Without this allocation, nothing keeps x alive so it can be collected during the Gc.full_major call.

3 Likes

Yes indeed, I was using the native compiler. Your explanation makes sense, and highlights that there may be differences in behaviour with Flambda, in these edge cases. Thanks very much!