On 9/21/21 4:17 PM, eugene wrote:
> On Tuesday, 21 September 2021 at 19:42:48 UTC, jfondren wrote:
> On Monday, 13 September 2021 at 17:18:30 UTC, eugene wrote:
There's nothing special about sg0 and sg1, except that they're part of Stopper. The Stopper in main() is collected before the end of main() because it's not used later in the function
Okay, but how could you explain this then
void main(string[] args) {
auto Main = new Main();
Main.run();
auto stopper = new Stopper();
stopper.run();
Here is what is happening. The compiler keeps track of how long it needs to keep stopper
around.
In assembly, the new Stopper()
call is a function which returns in a register.
On the very next instruction, you are calling the function stopper.run
where it needs the value of the register (either pushed into an argument register, or put on the call stack, depending on the ABI). Either way, this is the last time in the function the value stopper
is needed. Therefore, it does not store it on the stack frame of main
. This is an optimization, but one that is taken even without optimizations enabled in some compilers. It's called dead store elimination.
Since the register is overwritten by subsequent function calls, there no longer exists a reference to stopper
, and it gets collected (along with the members that are only referenced via stopper
).
> Now, change operation order in the main like this:
void main(string[] args) {
auto Main = new Main();
auto stopper = new Stopper();
Main.run();
stopper.run();
d-lang/edsm-in-d-simple-example-2 $ ./test | grep STOPPER
'STOPPER' registered 5 (esrc.Signal)
'STOPPER' registered 6 (esrc.Signal)
'STOPPER @ INIT' got 'M0' from 'SELF'
'STOPPER' enabled 5 (esrc.Signal)
'STOPPER' enabled 6 (esrc.Signal)
Everything is Ok now, stopper is not collected soon after start.
So the question is how this innocent looking change
can affect GC behavior so much?...
In this case, at the point you call Main.run
, stopper
is only in a register. Yet, it's needed later, so the compiler has no choice but to put stopper
on the stack so it has access to it to call stopper.run
. If it didn't, it's likely that Main.run
will overwrite that register.
Once it's on the stack, the GC can see it for the full run of main
. This is why this case is different.
Note that Java is even more aggressive, and might still collect it, because it could legitimately set stopper
to null after the last use to signify that it's no longer needed. I don't anticipate D doing this though.
I recommend you read the blog post, it has details on how this is happening. How do you fix it? I have proposed a possible solution, but I'm not sure if it's completely sound, see here. It may be that this works today, but a future more clever compiler can potentially see through this trick and still not store the pinned value.
I think the spec is wrong to say that just storing something as a local variable should solve the problem. We should follow the lead of other GC-supporting languages, and provide a mechanism to ensure a pointer is scannable by the GC through the entire scope.
-Steve