Program crash: GC destroys an object unexpectedly

Steven Schveighoffer schveiguy at gmail.com
Wed Sep 22 11:44:16 UTC 2021


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
> 
> ```d
> 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](https://en.wikipedia.org/wiki/Dead_store).

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:
> 
> ```d
> 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](https://forum.dlang.org/post/sichju$2gth$1@digitalmars.com). 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


More information about the Digitalmars-d-learn mailing list