www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - How to destruct class instances allocated by a Region-allocator over a

reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
As a follow-up to

https://forum.dlang.org/post/jfgpngdudtprzznrckwf forum.dlang.org

I managed to put together the benchmark

https://github.com/nordlow/phobos-next/blob/fa3526b15c746bda50a195f4e492ab2de9c15287/benchmarks/allocators/source/app.d

which run (via ldc)

dub run --build=release-nobounds

prints

DoubleNode Region allocator: 103 ms and 810 μs
DoubleNode new-allocation: 2 secs, 565 ms, and 566 μs
DoubleNode with global allocator: 2 secs, 680 ms, and 93 μs
...

proving the massive speedups possible when using, for instance, a 
Region allocator over a single continuous memory block, in this 
case allocated by the GC.

The code tested is in essence the performance of 10 million 
consecutive calls to the factory function `make` constructing 
instances of the class `DoubleNode`:

void benchmarkAllocatorsRegion()
{
     immutable nodeCount = 10_000_000; // number of `Nodes`s to 
allocate

     void[] buf = GCAllocator.instance.allocate(nodeCount * 
__traits(classInstanceSize, DoubleNode));
     auto allocator = Region!(NullAllocator, 
platformAlignment)(cast(ubyte[])buf);

     Type make(Type, Args...)(Args args) // TODO this should be 
pure
     {
         pragma(inline, true);
         return allocator.make!Type(args);
     }

     void[] allocate(size_t bytes)
     {
         return allocator.allocate(bytes); // TODO should be  safe 
pure
     }

     /* latest pointer here to prevent fast scoped non-GC 
allocation in LDC */
     void* latestPtr;

     void testRegionAllocator()
     {
         auto x = make!DoubleNode(42);
         assert(x);
         latestPtr = cast(void*)x;
     }

     void testNewAllocation()
     {
         auto x = new DoubleNode(42);
         latestPtr = cast(void*)x;
     }

     void testGlobalAllocator()
     {
         auto x = theAllocator.make!DoubleNode(42);
         latestPtr = cast(void*)x;
     }

     const results = benchmark!(testRegionAllocator,
                                testNewAllocation,
                                testGlobalAllocator)(nodeCount);
     writeln("DoubleNode Region allocator: ", results[0]);
     writeln("DoubleNode new-allocation: ", results[1]);
     writeln("DoubleNode with global allocator: ", results[2]);
}

However, I can't figure out how we can be sure that the 
destructors of `DoubleNode` are called for all the 10 million 
objects. Is there a way to tell the GC where the class instances 
that need to be destroyed lie (when nothing references them 
anymore).
Apr 02 2018
parent reply Alexandru Jercaianu <alex.jercaianu gmail.com> writes:
On Monday, 2 April 2018 at 14:52:34 UTC, Per Nordlöw wrote:
 As a follow-up to

 https://forum.dlang.org/post/jfgpngdudtprzznrckwf forum.dlang.org

 [...]
Hi, I am not completely sure how to solve this, but maybe we can find some clues here [1]. It seems like we should use addRoot on the buffer returned by GC.instance.allocate to keep it alive. Then, we can use addRange on each node after allocation and somehow use 'TypeInfo' to trigger destructors. I'll dig into this more tomorrow and come back with a better answer. Thanks, Alex [1] - https://dlang.org/phobos/core_memory.html
Apr 02 2018
parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Monday, 2 April 2018 at 20:43:01 UTC, Alexandru Jercaianu 
wrote:
 I am not completely sure how to solve this, but maybe we can 
 find some clues here [1].
 It seems like we should use addRoot on the buffer returned by 
 GC.instance.allocate to keep it alive.
 Then, we can use addRange on each node after allocation and 
 somehow use 'TypeInfo' to trigger destructors.
 I'll dig into this more tomorrow and come back with a better 
 answer.
How can there not be a documented answer for this question, given that std.experimental.allocator has been in Phobos for 2 years? Has std.experimental.allocator only been used for allocating `struct`s? Is the Region allocator especially misfit for constructing classes? Thanks, anyway.
Apr 02 2018
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 4/2/18 5:16 PM, Per Nordlöw wrote:
 On Monday, 2 April 2018 at 20:43:01 UTC, Alexandru Jercaianu wrote:
 I am not completely sure how to solve this, but maybe we can find some 
 clues here [1].
 It seems like we should use addRoot on the buffer returned by 
 GC.instance.allocate to keep it alive.
 Then, we can use addRange on each node after allocation and somehow 
 use 'TypeInfo' to trigger destructors.
 I'll dig into this more tomorrow and come back with a better answer.
How can there not be a documented answer for this question, given that std.experimental.allocator has been in Phobos for 2 years? Has std.experimental.allocator only been used for allocating `struct`s? Is the Region allocator especially misfit for constructing classes?
Since a while, the GC also calls struct destructors, so it's likely to be a problem for both. Note, addRoot and addRange will NOT call the destructors appropriately. It will just prevent those memory areas from getting collected. The memory shouldn't be collected anyway because RegionAllocator should have a reference to it. The only way it will get destroyed is removing the root/range, and then it will get collected just like any other GC block -- same as it is now. It looks like std.experimental.allocator assumes you will manually destroy items (possibly via dispose), it has no mechanism to say "here's how to destroy this memory I'm allocating if you happen to collect it". -Steve
Apr 02 2018
parent reply Eduard Staniloiu <edi33416 gmail.com> writes:
On Monday, 2 April 2018 at 21:32:47 UTC, Steven Schveighoffer 
wrote:
 On 4/2/18 5:16 PM, Per Nordlöw wrote:
 On Monday, 2 April 2018 at 20:43:01 UTC, Alexandru Jercaianu 
 wrote:
 I am not completely sure how to solve this, but maybe we can 
 find some clues here [1].
 It seems like we should use addRoot on the buffer returned by 
 GC.instance.allocate to keep it alive.
 Then, we can use addRange on each node after allocation and 
 somehow use 'TypeInfo' to trigger destructors.
 I'll dig into this more tomorrow and come back with a better 
 answer.
How can there not be a documented answer for this question, given that std.experimental.allocator has been in Phobos for 2 years? Has std.experimental.allocator only been used for allocating `struct`s? Is the Region allocator especially misfit for constructing classes?
Since a while, the GC also calls struct destructors, so it's likely to be a problem for both. Note, addRoot and addRange will NOT call the destructors appropriately. It will just prevent those memory areas from getting collected. The memory shouldn't be collected anyway because RegionAllocator should have a reference to it. The only way it will get destroyed is removing the root/range, and then it will get collected just like any other GC block -- same as it is now. It looks like std.experimental.allocator assumes you will manually destroy items (possibly via dispose), it has no mechanism to say "here's how to destroy this memory I'm allocating if you happen to collect it". -Steve
The GCAllocator from std.experimental uses the druntime core.memory.GC, and allocates with a call to GC.malloc [1] The GC doesn't know how you are using the memory chunk that he provided you with. He only keeps a track of this chunk and will collect it when there are no more references to it; you could also manually free it, if you wish so, with a call to `GCAllocator.instance.deallocate`. As Steve has said, you will have to manually destroy the items. I recommend using dispose as it checks if the destroyed object has an explicit destructor, which it calls, before deallocating the memory. So, say `reg` is your allocator, your workflow would be auto obj = reg.make!Type(args); /* do stuff */ reg.dispose(obj); // If Type has a __dtor, it will call obj.__dtor // and then reg.deallocate(obj) Hope this helps. Cheers, Edi [1] - https://dlang.org/library/core/memory/gc.malloc.html
Apr 03 2018
parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Tuesday, 3 April 2018 at 09:14:28 UTC, Eduard Staniloiu wrote:
 So, say `reg` is your allocator, your workflow would be

 auto obj = reg.make!Type(args);
 /* do stuff */
 reg.dispose(obj); // If Type has a __dtor, it will call 
 obj.__dtor
                   // and then reg.deallocate(obj)
If I do sucessive calls to reg.make!X where X are different kinds of classes of different sizes how does reg.dispose(obj) figure out at which address(es) (where emplace filled in the data) the objects reside?
Apr 06 2018
parent reply Eduard Staniloiu <edi33416 gmail.com> writes:
On Friday, 6 April 2018 at 21:49:37 UTC, Per Nordlöw wrote:
 On Tuesday, 3 April 2018 at 09:14:28 UTC, Eduard Staniloiu 
 wrote:
 So, say `reg` is your allocator, your workflow would be

 auto obj = reg.make!Type(args);
 /* do stuff */
 reg.dispose(obj); // If Type has a __dtor, it will call 
 obj.__dtor
                   // and then reg.deallocate(obj)
If I do sucessive calls to reg.make!X where X are different kinds of classes of different sizes how does reg.dispose(obj) figure out at which address(es) (where emplace filled in the data) the objects reside?
It can't figure out. With custom allocators you have to manually do the memory management, so the responsibility of when and which object needs to be destroyed falls on the user of the custom allocator.
Apr 07 2018
parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Saturday, 7 April 2018 at 07:50:37 UTC, Eduard Staniloiu wrote:
 On Friday, 6 April 2018 at 21:49:37 UTC, Per Nordlöw wrote:
 On Tuesday, 3 April 2018 at 09:14:28 UTC, Eduard Staniloiu 
 wrote:
 So, say `reg` is your allocator, your workflow would be

 auto obj = reg.make!Type(args);
 /* do stuff */
 reg.dispose(obj); // If Type has a __dtor, it will call 
 obj.__dtor
                   // and then reg.deallocate(obj)
If I do sucessive calls to reg.make!X where X are different kinds of classes of different sizes how does reg.dispose(obj) figure out at which address(es) (where emplace filled in the data) the objects reside?
It can't figure out. With custom allocators you have to manually do the memory management, so the responsibility of when and which object needs to be destroyed falls on the user of the custom allocator.
IMHO, such a complexity should be wrapped in a typed allocation layer. Have Andrei spoken anything about `TypedAllocator`(s) to wrap this complexity?
Apr 07 2018
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 4/7/18 10:57 AM, Per Nordlöw wrote:
 On Saturday, 7 April 2018 at 07:50:37 UTC, Eduard Staniloiu wrote:
 On Friday, 6 April 2018 at 21:49:37 UTC, Per Nordlöw wrote:
 On Tuesday, 3 April 2018 at 09:14:28 UTC, Eduard Staniloiu wrote:
 So, say `reg` is your allocator, your workflow would be

 auto obj = reg.make!Type(args);
 /* do stuff */
 reg.dispose(obj); // If Type has a __dtor, it will call obj.__dtor
                   // and then reg.deallocate(obj)
If I do sucessive calls to reg.make!X where X are different kinds of classes of different sizes how does reg.dispose(obj) figure out at which address(es) (where emplace filled in the data) the objects reside?
It can't figure out. With custom allocators you have to manually do the memory management, so the responsibility of when and which object needs to be destroyed falls on the user of the custom allocator.
IMHO, such a complexity should be wrapped in a typed allocation layer. Have Andrei spoken anything about `TypedAllocator`(s) to wrap this complexity?
Well, you know the type, because make returned it no? The contract is, you call obj = make!X(args), then you have to call dispose(obj), where obj is of the type X. That's how it knows. If you are thinking you want to destroy the whole block at once (typed as void[]), that's not how it works. stdx.allocator is not going to help you with GC collection, it's not geared towards that purpose. -Steve
Apr 09 2018
parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Monday, 9 April 2018 at 13:51:47 UTC, Steven Schveighoffer 
wrote:
 Well, you know the type, because make returned it no? The 
 contract is, you call obj = make!X(args), then you have to call 
 dispose(obj), where obj is of the type X. That's how it knows.

 If you are thinking you want to destroy the whole block at once 
 (typed as void[]), that's not how it works.

 stdx.allocator is not going to help you with GC collection, 
 it's not geared towards that purpose.
Ok, thanks!
Apr 09 2018
parent reply Eduard Staniloiu <edi33416 gmail.com> writes:
On Monday, 9 April 2018 at 14:51:24 UTC, Per Nordlöw wrote:
 On Monday, 9 April 2018 at 13:51:47 UTC, Steven Schveighoffer 
 wrote:
 Well, you know the type, because make returned it no? The 
 contract is, you call obj = make!X(args), then you have to 
 call dispose(obj), where obj is of the type X. That's how it 
 knows.

 If you are thinking you want to destroy the whole block at 
 once (typed as void[]), that's not how it works.

 stdx.allocator is not going to help you with GC collection, 
 it's not geared towards that purpose.
Ok, thanks!
If you are using a custom allocator to allocate memory then you also have to manually dispose of the memory when it is no longer needed. You can't have a custom allocator to supply you with the memory and then have the GC track and dispose of it. I get the feeling that this is what you were looking for?
Apr 10 2018
parent rikki cattermole <rikki cattermole.co.nz> writes:
On 11/04/2018 1:56 AM, Eduard Staniloiu wrote:
 On Monday, 9 April 2018 at 14:51:24 UTC, Per Nordlöw wrote:
 On Monday, 9 April 2018 at 13:51:47 UTC, Steven Schveighoffer wrote:
 Well, you know the type, because make returned it no? The contract 
 is, you call obj = make!X(args), then you have to call dispose(obj), 
 where obj is of the type X. That's how it knows.

 If you are thinking you want to destroy the whole block at once 
 (typed as void[]), that's not how it works.

 stdx.allocator is not going to help you with GC collection, it's not 
 geared towards that purpose.
Ok, thanks!
If you are using a custom allocator to allocate memory then you also have to manually dispose of the memory when it is no longer needed. You can't have a custom allocator to supply you with the memory and then have the GC track and dispose of it. I get the feeling that this is what you were looking for?
As long as you can use libc's free, sure you can :)
Apr 10 2018