Skip to content

Fix subtle bug in test file weaklifetime.ml - #10071

Merged
xavierleroy merged 1 commit into
ocaml:trunkfrom
damiendoligez:fix-10055
Dec 8, 2020
Merged

Fix subtle bug in test file weaklifetime.ml#10071
xavierleroy merged 1 commit into
ocaml:trunkfrom
damiendoligez:fix-10055

Conversation

@damiendoligez

@damiendoligez damiendoligez commented Dec 4, 2020

Copy link
Copy Markdown
Member

This fixes the bug reported by @dra27 in #10055.

The bug is in the interaction of four things:

  1. the compilation of pattern-matching
  2. the bytecode compiler's management of scope
  3. the fact that Gc.quick_stat needs to allocate
  4. the way weaklifetime.ml is written

Looking at weaklifetime.ml, the compilation of pattern-matching inserts a let-binding of data.(i).objs.(j) for the pattern-matching in check_and_change. This let-binding's scope is the entire pattern-matching, including the actions. The bytecode compiler will thus keep it in a root until the end of the pattern-matching (and the function).

Now look at the Present _, true case. It does some random-number generation, some tests, some assignments, and a call to gccount. It does a second call to gccount after erasing the pointer, in an attempt to get the number of a cycle that started after the last (strong) pointer to the value was erased. It has to do that because Gc.quick_stat allocates and might thus start a new cycle and return the number of the previous cycle. But this is all for nought because the secoind call to gccount, which happens after we erase the pointer from the data structure, is still in the scope of the let introduced by pattern-matching that keeps the data alive. Hence we set the data to Absent gc2 while the cycle gc2+1 can (in rare circumstances) still see the data alive.

Note that the native-code compiler does scope minimization and removes the root as soon as it's not needed by the program, in this case when we enter the action, and avoids the problem entirely.

To make the problem easily reproducible (even on 64-bit Unix) just apply the following patch, which simply adds lots of allocations after the call to Gc.quick_stat:

--- weaklifetime.ml.orig	2020-12-04 14:29:20.000000000 +0100
+++ weaklifetime.ml	2020-12-04 14:42:35.000000000 +0100
@@ -1,7 +1,10 @@
 (* TEST
 *)
 
-Random.init 12345;;
+let seed = ref Random.(self_init(); bits());;
+if Array.length Sys.argv > 1 then seed := int_of_string Sys.argv.(1);;
+Printf.printf "seed=%d\n" !seed;;
+Random.init !seed;;
 
 let size = 1000;;
 
@@ -27,7 +30,11 @@
   )
 ;;
 
-let gccount () = (Gc.quick_stat ()).Gc.major_collections;;
+let gccount n =
+  let count = (Gc.quick_stat ()).Gc.major_collections in
+  for i = 0 to n do ignore (Sys.opaque_identity (Array.make 20 20)) done;
+  count
+;;
 
 (* Check the correctness condition on the data at (i,j):
    1. if the block is present, the weak pointer must be full
@@ -39,7 +46,7 @@
    2. if the block and weak pointer are present, randomly erase the block
 *)
 let check_and_change i j =
-  let gc1 = gccount () in
+  let gc1 = gccount 0 in
   match data.(i).objs.(j), Weak.check data.(i).wp j with
   | Present x, false -> assert false
   | Absent n, true -> assert (gc1 <= n+1)
@@ -50,14 +57,14 @@
   | Present _, true ->
     if Random.int 10 = 0 then begin
       data.(i).objs.(j) <- Absent gc1;
-      let gc2 = gccount () in
+      let gc2 = gccount 200 in
       if gc1 <> gc2 then data.(i).objs.(j) <- Absent gc2;
     end
 ;;
 
 let dummy = ref [||];;
 
-while gccount () < 20 do
+while gccount 0 < 20 do
   dummy := Array.make (Random.int 300) 0;
   let i = Random.int size in
   let j = Random.int (Array.length data.(i).objs) in

I think the simplest fix is to get out of the scope of match before we erase the pointer, by doing a tail-call to an auxiliary function.

Note: the Changes entry is my best attempt at predicting the future.

@dra27

dra27 commented Dec 4, 2020

Copy link
Copy Markdown
Member

Very nice: I'd got some of the way down the road, and had determined that the timing required the major GC to cycle, but I couldn't figure out why the gc2 part wasn't catching it (let alone why it was bytecode only)!

@dra27

dra27 commented Dec 4, 2020

Copy link
Copy Markdown
Member

@stedolan and I had also spotted that it was happening on 64-bit Linux system, just requiring more patience. The problem, as it happens, became more apparent with 6027c9e before 4.10 was branched!

@gasche gasche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious about the "predict the future", which I guess might have turned it into a self-unfulfilling prediction.

Comment thread testsuite/tests/misc/weaklifetime.ml Outdated
end
if Random.int 10 = 0 then (erase [@ocaml.tailcall]) i j gc1
(* Must be a tail-call to get out of the scope of the binding of
[data.(i).objs.(j)] that is introduced by the pattern-matching. *)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the aim is to ensure that the previous value of data.(i).objs.(j) is not retained on the bytecode stack, would the following suffice?

type change = No_change | Set_present | Maybe_set_absent

let check_and_change i j =
  let gc1 = gccount () in
  let change =
    (* we only read data.(i).objs.(j) in this local binding to ensure
        that it does not remain reachable on the bytecode stack
        in the rest of the function below, when we overwrite the value
        and try to observe its collection.  *)
    match data.(i).objs.(j), Weak.check data.(i).wp j with
    | Present x, false -> assert false
    | Absent n, true -> assert (gc1 <= n+1); No_change
    | Absent _, false -> Make_present
    | Present _, true ->
      if Random.int 10 = 0 then Make_absent else No_change
  in
  match change with
  | No_change -> ()
  | Make_present ->
      let x = Array.make (1 + Random.int 10) 42 in
      data.(i).objs.(j) <- Present x;
      Weak.set data.(i).wp j (Some x);
  | Make_absent ->
      data.(i).objs.(j) <- Absent gc1;
      let gc2 = gccount () in
      if gc1 <> gc2 then data.(i).objs.(j) <- Absent gc2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(The point, I guess, is that I find it more natural to use lexical scoping to scope lifetimes, rather than to use tail-recursion to break out of the lexical-scoping-related lifetime.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @gasche's point about scoping, but I'm also eager to have this test fixed, as it is ruining our CI right now. Either @damiendoligez rewrites as suggested very soon, or the fix goes in unchanged.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @gasche's version. I'll copy it and re-test with my scaffolding to make sure it works.

@xavierleroy xavierleroy added the bug label Dec 5, 2020
@xavierleroy xavierleroy added this to the 4.12 milestone Dec 5, 2020

@xavierleroy xavierleroy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable to me.

@xavierleroy
xavierleroy merged commit 0f629a0 into ocaml:trunk Dec 8, 2020
@damiendoligez
damiendoligez deleted the fix-10055 branch December 8, 2020 13:24
Octachron pushed a commit that referenced this pull request Dec 9, 2020
A binding was keeping an object live longer than expected, but only in bytecode.

(cherry picked from commit 0f629a0)
@Octachron

Copy link
Copy Markdown
Member

Cherry-picked to 4.12 in 1e990bb

dbuenzli pushed a commit to dbuenzli/ocaml that referenced this pull request Mar 25, 2021
A binding was keeping an object live longer than expected, but only in bytecode.
shym added a commit to shym/ocaml-containers that referenced this pull request Dec 16, 2022
Rewrite a test to shorten the lexical scope of the string it builds
because, in the bytecode backend, a variable is deemed live at least as
long as its lexical scope.

Reference: ocaml/ocaml#10071
shym added a commit to shym/ocaml-containers that referenced this pull request Dec 19, 2022
Rewrite a test to shorten the lexical scope of the string it builds
because, in the bytecode backend, a variable is deemed live at least as
long as its lexical scope.

Reference: ocaml/ocaml#10071
FardaleM pushed a commit to c-cube/ocaml-containers that referenced this pull request Dec 22, 2022
Rewrite a test to shorten the lexical scope of the string it builds
because, in the bytecode backend, a variable is deemed live at least as
long as its lexical scope.

Reference: ocaml/ocaml#10071
tbrugere pushed a commit to tbrugere/ocaml that referenced this pull request May 17, 2026
A binding was keeping an object live longer than expected, but only in bytecode.

(cherry picked from commit 0f629a0)
rboudrouss pushed a commit to rboudfork/ocaml that referenced this pull request Jul 12, 2026
A binding was keeping an object live longer than expected, but only in bytecode.

(cherry picked from commit 0f629a0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants