Jump to page: 1 210  
Page
Thread overview
What should happen here?
Sep 20, 2021
Johan
Sep 20, 2021
Johan
Sep 20, 2021
IGotD-
Sep 20, 2021
IGotD-
Sep 20, 2021
IGotD-
Sep 20, 2021
Elronnd
Sep 21, 2021
Johan
Sep 21, 2021
bauss
Sep 21, 2021
Paulo Pinto
Sep 21, 2021
Johan
Sep 23, 2021
Johan
Sep 23, 2021
Johan
Sep 23, 2021
max haughton
Sep 23, 2021
H. S. Teoh
Sep 23, 2021
Daniel N
Sep 23, 2021
IGotD-
Sep 24, 2021
deadalnix
Sep 24, 2021
max haughton
Sep 24, 2021
H. S. Teoh
Sep 26, 2021
deadalnix
Sep 26, 2021
max haughton
Sep 26, 2021
max haughton
Sep 27, 2021
deadalnix
Sep 27, 2021
Max Samukha
Sep 27, 2021
jfondren
Sep 27, 2021
Elronnd
Sep 28, 2021
Max Samukha
Sep 27, 2021
deadalnix
Sep 28, 2021
Elronnd
Sep 28, 2021
deadalnix
Sep 29, 2021
John Colvin
Sep 29, 2021
Elronnd
Sep 30, 2021
deadalnix
Sep 23, 2021
Paulo Pinto
Sep 24, 2021
Kagamin
Sep 25, 2021
Walter Bright
Sep 25, 2021
Walter Bright
Sep 22, 2021
Kagamin
Sep 22, 2021
Johan
Sep 22, 2021
Ali Çehreli
Sep 22, 2021
IGotD-
Sep 22, 2021
Adam D Ruppe
Sep 22, 2021
Adam D Ruppe
Sep 22, 2021
Paul Backus
Sep 22, 2021
IGotD-
Sep 22, 2021
jfondren
Sep 22, 2021
jfondren
Sep 22, 2021
Kagamin
Sep 22, 2021
Kagamin
Sep 20, 2021
Paul Backus
Sep 21, 2021
Ali Çehreli
Sep 21, 2021
Dukc
Sep 21, 2021
Adam D Ruppe
Sep 23, 2021
deadalnix
Sep 24, 2021
Ali Çehreli
Sep 25, 2021
Walter Bright
Sep 25, 2021
Ali Çehreli
Sep 25, 2021
Elronnd
Sep 26, 2021
Walter Bright
Sep 26, 2021
Daniel N
Sep 26, 2021
Paul Backus
Sep 26, 2021
Paul Backus
Sep 25, 2021
Walter Bright
Sep 25, 2021
jfondren
Sep 25, 2021
jfondren
Sep 26, 2021
Walter Bright
Sep 26, 2021
Ali Çehreli
Sep 26, 2021
Adam D Ruppe
Sep 26, 2021
Walter Bright
Sep 26, 2021
H. S. Teoh
September 20, 2021

Without any context, what do you think should happen here?

import std.stdio;
import core.memory;
class C
{
   ~this() { writeln("dtor"); }
}

void main()
{
   auto c = new C;
   foreach(i; 0 .. 10000) GC.collect;
   writeln("end of main");
}

Option 1:

end of main
dtor

Option 2:

dtor
end of main

Option 3:

end of main

Option 4:
Option 1 or 2, depending on entropy.

I'll post a response with what I've observed, and further discussion. I just want people to consider the above only with their initial expectations.

-Steve

September 20, 2021

On Monday, 20 September 2021 at 18:26:59 UTC, Steven Schveighoffer wrote:

>

Without any context, what do you think should happen here?

import std.stdio;
import core.memory;
class C
{
   ~this() { writeln("dtor"); }
}

void main()
{
   auto c = new C;
   foreach(i; 0 .. 10000) GC.collect;
   writeln("end of main");
}

Option 1:

end of main
dtor

Option 2:

dtor
end of main

Option 3:

end of main

Option 4:
Option 1 or 2, depending on entropy.

I believe all options are valid, because there is no guarantee if and when class destructors are called.
I'm guessing without optimization, option 3 will happen most.
With optimization turned on, option 2. Or maybe the object is put on the stack (scope or LDC's heap->stack optimization) and so option 1 happens?

-Johan

September 20, 2021

On Monday, 20 September 2021 at 18:26:59 UTC, Steven Schveighoffer wrote:

>

Without any context, what do you think should happen here?

Is this a trick question?

In a perfect world I think option 1 should happen.

However doesn't it depend on how the compiler decide what to do with variable c. It can be in a register or it can be on stack. If it is on stack, then the GC believe it is still in use. If it is in a register, then the compiler is likely to optimize c away before the loop and the GC destroys it.

In reality, I guess option 4.

September 20, 2021

On Monday, 20 September 2021 at 18:38:03 UTC, Johan wrote:

>

On Monday, 20 September 2021 at 18:26:59 UTC, Steven Schveighoffer wrote:

>

Without any context, what do you think should happen here?

import std.stdio;
import core.memory;
class C
{
   ~this() { writeln("dtor"); }
}

void main()
{
   auto c = new C;
   foreach(i; 0 .. 10000) GC.collect;
   writeln("end of main");
}

Option 1:

end of main
dtor

Option 2:

dtor
end of main

Option 3:

end of main

Option 4:
Option 1 or 2, depending on entropy.

I believe all options are valid, because there is no guarantee if and when class destructors are called.
I'm guessing without optimization, option 3 will happen most.
With optimization turned on, option 2. Or maybe the object is put on the stack (scope or LDC's heap->stack optimization) and so option 1 happens?

FWIW I believe option 1 should only happen with this "to stack" optimization (object lifetime provably confined to the function), because we are not obliged by the language spec to collect garbage after main ends (so it's best for performance to not collect).

-Johan

September 20, 2021

OK, so here is what Actually happens consistently (all this is with dmd):

  1. Macos 64-bit: Option 2
  2. Windows 64-bit: Option 1
  3. Windows 32-bit: Option 2
  4. Windows 32-bit with -g: Option 1
  5. Linux 64-bit: Option 1
  6. Linux 32-bit: Option 2

I'm sure other compilers would have varying degrees of options.

What is happening?

What happens is that the variable c is sometimes not stored on the stack, but in a register. after a call to GC.collect, that register is overwritten, and there is no longer any reference to the object, it gets collected.

In this simple example, we are doing nothing with c afterwards. But there is a real case of trouble happening to someone here. They are passing the class reference into a C function that registers the class in a place the GC can't see. The GC collects that information, and by the time that C library uses that (all before main exits), the data is invalid.

I feel like this might not necessarily be an issue, because technically, you aren't using c any more, so it can be deallocated immediately. But right in our documentation here it lists ways to alleviate this:

If pointers to D garbage collector allocated memory are passed to C functions, it's critical to ensure that the memory will not be collected by the garbage collector before the C function is done with it. This is accomplished by:

* Making a copy of the data using core.stdc.stdlib.malloc() and passing the copy instead.
* Leaving a pointer to it on the stack (as a parameter or automatic variable), as the garbage collector will scan the stack.
* Leaving a pointer to it in the static data segment, as the garbage collector will scan the static data segment.
* Registering the pointer with the garbage collector with the std.gc.addRoot() or std.gc.addRange() calls.

This to me seems like "leaving a pointer to it on the stack". I'm not sure how else I would do that specifically? Plus, this option is the only "free" one -- the others all require much more complication. Adding a pointer to the stack is free. It's just, I don't know how to tell the compiler to do that besides declaring it.

Note that I don't think any other data types allocated on the heap do this. I tried to change C to a struct, and it is not collected. Change the line to auto c = [new C] and it works fine.

It's just class references that the compiler seems to not care about ensuring stack references stay alive.

Should it be this way?

-Steve

September 20, 2021

On 9/20/21 2:39 PM, IGotD- wrote:

>

On Monday, 20 September 2021 at 18:26:59 UTC, Steven Schveighoffer wrote:

>

Without any context, what do you think should happen here?

Is this a trick question?

Of course it's a trick question! No normal questions are asked this way ;)

>

In a perfect world I think option 1 should happen.

However doesn't it depend on how the compiler decide what to do with variable c. It can be in a register or it can be on stack. If it is on stack, then the GC believe it is still in use. If it is in a register, then the compiler is likely to optimize c away before the loop and the GC destroys it.

In reality, I guess option 4.

You are kind of right. The question I bring up, just posted, is how do you solve passing a class reference into a C library that should only be used during the function?

-Steve

September 20, 2021

On Monday, 20 September 2021 at 18:26:59 UTC, Steven Schveighoffer wrote:

>

Without any context, what do you think should happen here?

import std.stdio;
import core.memory;
class C
{
   ~this() { writeln("dtor"); }
}

void main()
{
   auto c = new C;
   foreach(i; 0 .. 10000) GC.collect;
   writeln("end of main");
}

Option 1:

end of main
dtor

Option 2:

dtor
end of main

Option 3:

end of main

Option 4:
Option 1 or 2, depending on entropy.

IMO all of the options are valid.

The GC gives no guarantees about when or if it will finalize GC-allocated objects, so both option 1 and 3 are valid.

Option 2 at first seems like it should be invalid, but since c is never accessed after initialization, the compiler is free to remove the initialization as a dead store, which would allow the GC to collect the new C object prior to the end of c's lifetime.

I would expect to see either 1 or 3 with optimizations disabled, and possibly 2 with optimizations enabled. I would be surprised to see 4, since dead-store elimination shouldn't depend on entropy at runtime.

September 20, 2021

On Monday, 20 September 2021 at 18:49:12 UTC, Steven Schveighoffer wrote:

>

Should it be this way?

I think regardless, two worlds shouldn't mix. If you have a C function that also stores the allocated data then that data should also be allocated in "C world". Typically you have an additional C function that creates the data/object that you then pass to the C functions. I find it more a design pattern/API problem.

I'm sure the there are exceptions to what I described where it cannot be used.

Also, leaving the variable on stack option should be removed in the documentation because how can we know what the compiler decides to do with it.

September 20, 2021

On Monday, 20 September 2021 at 19:00:38 UTC, IGotD- wrote:

>

I also think this doesn't have much to do with the GC of D but more a general life time behaviour. In Rust (feel free to kick me in butt every time I mention Rust), there the free call might happen after the variable is last being used and not necessarily at the end of scope. That behaviour would yield the same result that C functions would access invalid memory.

The only thing that saves Rust is that when you send a variables to C world, the compiler gives up, consider it gone and does nothing.

The next question would be, would the upcoming borrow checker in D solve any of this?

September 20, 2021

On 9/20/21 3:00 PM, IGotD- wrote:

>

On Monday, 20 September 2021 at 18:49:12 UTC, Steven Schveighoffer wrote:

>

Should it be this way?

I think regardless, two worlds shouldn't mix. If you have a C function that also stores the allocated data then that data should also be allocated in "C world". Typically you have an additional C function that creates the data/object that you then pass to the C functions. I find it more a design pattern/API problem.

I'm sure the there are exceptions to what I described where it cannot be used.

Also, leaving the variable on stack option should be removed in the documentation because how can we know what the compiler decides to do with it.

Then why are pointers to structs, arrays, structs containing class references not treated the same?

I'm not sure why class references are singled out, but they are for some reason.

-Steve

« First   ‹ Prev
1 2 3 4 5 6 7 8 9 10