Fix subtle bug in test file weaklifetime.ml - #10071
Conversation
|
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 |
gasche
left a comment
There was a problem hiding this comment.
I was curious about the "predict the future", which I guess might have turned it into a self-unfulfilling prediction.
| 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. *) |
There was a problem hiding this comment.
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;There was a problem hiding this comment.
(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.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I like @gasche's version. I'll copy it and re-test with my scaffolding to make sure it works.
ab89d1e to
1d88cfb
Compare
1d88cfb to
76fd739
Compare
xavierleroy
left a comment
There was a problem hiding this comment.
Looks reasonable to me.
A binding was keeping an object live longer than expected, but only in bytecode. (cherry picked from commit 0f629a0)
|
Cherry-picked to 4.12 in 1e990bb |
A binding was keeping an object live longer than expected, but only in bytecode.
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
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
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
A binding was keeping an object live longer than expected, but only in bytecode. (cherry picked from commit 0f629a0)
A binding was keeping an object live longer than expected, but only in bytecode. (cherry picked from commit 0f629a0)
This fixes the bug reported by @dra27 in #10055.
The bug is in the interaction of four things:
Gc.quick_statneeds to allocateweaklifetime.mlis writtenLooking at
weaklifetime.ml, the compilation of pattern-matching inserts a let-binding ofdata.(i).objs.(j)for the pattern-matching incheck_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 _, truecase. It does some random-number generation, some tests, some assignments, and a call togccount. It does a second call togccountafter 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 becauseGc.quick_statallocates 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 togccount, which happens after we erase the pointer from the data structure, is still in the scope of theletintroduced by pattern-matching that keeps the data alive. Hence we set the data toAbsent gc2while the cyclegc2+1can (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:I think the simplest fix is to get out of the scope of
matchbefore 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.