September 25, 2021
On Saturday, 25 September 2021 at 22:19:30 UTC, Walter Bright wrote:
> Maybe the real problem is the user is expecting the destructor to run at a specific point in the execution.

In the case that spawned this thread, the problem was a segfault after a hidden reference was restored to the collected object, and the coder was certain the object should still be alive because the scope of its auto variable hadn't ended yet. The destructor by this point just had some debugging output so that we could see it was being collected before the end of it scope.

The hidden reference was hidden by Linux's epoll facility, which keeps 64 bits of arbitrary user data in kernel space, and returns it on an event. A bit like writing a pointer address to a file and then reading it back later.

(There's *also* meaningful work that must happen at a specific time in destructors in this code base, and other problems. It's pretty elaborate code by someone new to D.)

I don't think this is a problem with D, but I shared the intuition that this object should still be alive after the point the GC collected it, and that wrong intuition made troubleshooting this case a lot harder.
September 25, 2021
On Saturday, 25 September 2021 at 23:14:54 UTC, jfondren wrote:
> I don't think this is a problem with D,

Although I'd still like an efficient GC.keepAlive. My worry is the optimizer removing some measure I'm taking specifically to keep a reference alive.

September 25, 2021
On 9/25/21 3:19 PM, Walter Bright wrote:
> On 9/25/2021 10:46 AM, Steven Schveighoffer wrote:
>> Right, but the optimizer is working against that.
>>
>> For example:
>>
>> ```d
>> auto c = new C;
>> .... // a whole bunch of other code
>>
>> c.method; // not necessarily still live here.
>> ```
>>
>> Why would it not be live there? Because the method might be inlined,
>> and the compiler might determine at that point that none of the data
>> inside the method is needed, and therefore the object is no longer
>> needed.
>>
>> So technically, it's not live after the original allocation. But this
>> is really hard for a person to determine. Imagine having the GC
>> collect your object when the object is clearly "used" later?
>
> I understand your point. It's value is never used again, so there is no
> reason for the GC to hold on to it.

I must be misunderstanding Steven's example. You say "its value is never used again" yet his example has

  c.method

which to me is clearly "using" it again. So, I don't understand Steven's "not necessarily still live here" comment either. Is the case today?

> After the point when the value is
> never used again, when the destructor is run is indeterminate.

I accept that part.

The to-me-new concept of non-lexical scoping scares me if the non-lexical scope is shorter than lexical scope. And especially when the value is clearly used by `c.method`. (If I heard non-lexical scoping, I must have taken it as "destructor may be executed later than the lexical scope".)

Ali

September 26, 2021
On Sunday, 26 September 2021 at 00:23:19 UTC, Ali Çehreli wrote:
>   c.method
>
> which to me is clearly "using" it again.

If it is a final method that doesn't actually use any class variables then it doesn't actually use the `this` pointer. The optimizer sees this and can let the object go.

That code can also run without crashing if c is null too btw for the same reason.
September 25, 2021

On 9/25/21 8:23 PM, Ali Çehreli wrote:

>

On 9/25/21 3:19 PM, Walter Bright wrote:

>

On 9/25/2021 10:46 AM, Steven Schveighoffer wrote:

>

Right, but the optimizer is working against that.

For example:

auto c = new C;
.... // a whole bunch of other code

c.method; // not necessarily still live here.

Why would it not be live there? Because the method might be inlined,
and the compiler might determine at that point that none of the data
inside the method is needed, and therefore the object is no longer
needed.

So technically, it's not live after the original allocation. But this
is really hard for a person to determine. Imagine having the GC
collect your object when the object is clearly "used" later?

I understand your point. It's value is never used again, so there is no
reason for the GC to hold on to it.

I must be misunderstanding Steven's example. You say "its value is never used again" yet his example has

  c.method

which to me is clearly "using" it again. So, I don't understand Steven's "not necessarily still live here" comment either. Is the case today?

Yes, that is the case today.

As an example, c.method might not use any data inside c. Maybe c.method is void method() {}

But you might say "it has to pass c into it, so aha, it's live!". Except that it could be inlined, which effectively inlines a completely empty function.

But what if it's virtual? "Aha!" you might say, "now it has to have the pointer live, because it has to read the vtable". But not if your compiler is ldc -- it can inline virtual functions if it can figure out that there's no way you could have created a derived object.

As I said, the optimizer is fighting you the entire way.

Someone on Beerconf suggested destroying the object at the end of the struct. That actually is a reasonable solution that I don't think the compiler can elide. If that's what you want.

But I still am wishing for a simple "keep this alive" mechanism that doesn't add too much cruft, and is guaranteed to keep it alive.

-Steve

September 25, 2021
On 9/25/2021 6:02 PM, Steven Schveighoffer wrote:
> As I said, the optimizer is fighting you the entire way.

I'd reframe it as the user is trying to impose his own semantics on the optimizer :-)

Selecting semantics that enable aggressive optimizations is always a dance between user predictability and high performance. High performance usually wins.

Disabling dead assignment elimination would have a catastrophic deleterious effect on optimizations. A lot of template bloat would remain.


> But I still am wishing for a simple "keep this alive" mechanism that doesn't add too much cruft, and is guaranteed to keep it alive.

That's exactly what addRoot() is for.
September 25, 2021
On 9/25/2021 4:22 PM, jfondren wrote:
> Although I'd still like an efficient GC.keepAlive. My worry is the optimizer removing some measure I'm taking specifically to keep a reference alive.

Use GC.addRoot() to keep a reference alive. That's what it's for.
September 25, 2021
On 9/25/2021 12:28 PM, Elronnd wrote:
> Indeed; additionally, 'scope c = new Class()' _does_ follow RAII.

That's more of an optimization than a semantic shift.
September 26, 2021
On Sunday, 26 September 2021 at 02:15:53 UTC, Walter Bright wrote:
> On 9/25/2021 12:28 PM, Elronnd wrote:
>> Indeed; additionally, 'scope c = new Class()' _does_ follow RAII.
>
> That's more of an optimization than a semantic shift.

// This works fine...
scope c = new Class()

// I hope this works, otherwise it violates the 'law of least surprise'
auto c = new Class()
scope(exit) destroy(c);

If scope(exit) also works, then I don't think there is any problem...

September 26, 2021
On Sunday, 26 September 2021 at 07:49:46 UTC, Daniel N wrote:
> // I hope this works, otherwise it violates the 'law of least surprise'
> auto c = new Class()
> scope(exit) destroy(c);
>
> If scope(exit) also works, then I don't think there is any problem...

Destroy on a class reference doesn't call the destructor; it just sets the reference to null.

If the optimizer sees that the reference isn't used again after that, it can remove the assignment to null as a dead store. And *then* if it sees that the reference isn't used after its initialization, it can remove the initialization as a dead store.

Basically, any clever shortcut you can come up with that "uses" the reference but doesn't actually do anything will fall apart as soon as the optimizer figures out that it doesn't actually do anything.

Better to use addRoot/removeRoot, which have well-defined semantics and will never be optimized away