Thread overview | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
February 06, 2014 Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Any time you hard code code something you are also creating constraints. If people build something on top of what you have built they inherit those constrains. Take building a house, once the foundation is laid and the framework is added it becomes next to impossible to go back and modify the foundation. One big problem with D is that the both the runtime and library are hard coded to use AGC. Switching these to use ARC or MMM are just building different foundations but all creating very serious constraints on the the users of D. Because the runtime and library are created and maintained by some dedicated and smart people, the "simplistic" way of memory management does not need to be taken. (I realize it seems like more work but it isn't given the n-fold benefit it creates for all those that use it) So, why not create a design pattern that allows one, at compile time, to use any specific memory management technique they want for the runtime and library(and user code)? e.g., switches could be used to recompile the source when a different pattern is desired or libraries could be created that use different methods. e.g., phobos64GC.lib. To make any of this work, and work efficiently, one must abstract what is common between all memory management techniques. Since allocation is always explicitly defined, it is easy to handle. Which every memory management technique is used, it would be called/notified on allocation. The deallocation is the tricky part. (reallocation is another potential issue) For manual memory management the deallocation is explicitly called by the user. This in and of itself is significant and either allows the compiler to know that manual memory management is used or to replace it with automatic management if desired. e.g., { auto a = new!strategy A; release a; // <- explicit deallocation: replaced with strategy.explicitDeallocation(a); } vs { auto a = new!strategy A; // implicit deallocation: compiler inserts strategy.scopeDeallocation(a); } implicit deallocation could necessarily be handled by the GC. Basically if the programmer forgets to deallocate a type then it is automatically handled(by say AGC or ARC or whatever). The programmer, of course, should be allowed to deal with how this actually behaves, such as make using a weak reference. The key point here is that if we abstract the memory allocation and deallocation schemes then many memory management techniques should be possible(and possibly simultaneously with no hard coded reliance on any specific technique. Both manual and automatic schemes could be used simultaneously in that if the user fails to release memory explicitly then automatic releasing is used(AGC and/or ARC). This allows more predicable memory management and in a perfect world, if the programmer always properly released memory then no automatic collections would occur. In the real world the programmer could disable automatic collections and create weak references or use automatic collections only on the debug build and log when they occur to fix(e.g., he forgot to create a weak reference or release memory explicitly). This could occur on production builds too, logged, and reported. (so the production code might not be ideal performance wise but be fixed in future builds). Different allocation strategies have different ways have handling how they deal with explicit and implicit calls. One strategy could report all cases of implicit scope deallocation for the programmer to look at. Another could completely ignore any implicit deallocations which would turn off any automatic collections = most performant but least safe. One could define a global strategy and then local strategies e.g., new A uses global strategy and new!myMMM A uses myMMM strategy for the variable A. The interactions of strategies would be relatively easy I think. If say, we have class A { B b; this() { b = new!strategy Y B; } } auto a = new!strategyX A; Then on release of A, strategy X would call strategy Y's deallocation for B. If it is "non-deterministic" then a notification would be setup so that when b is actually released a can be fully released. In some cases this could occur immediately but is meant more for circular references. e.g., if we have class B { A aa; } auto b = new!strategyZ B; b.aa = a; when we free a, b is subjected to freeing, which involves freeing aa, which involves freeing a. Since a is already in the state of being free'ed, it can be completely free'ed. Of course, such a system might be slow if not properly designed or may just be inherently slow. Regardless, it is a strategy and other strategies might not work this way(but there would still need to be some commonality between them and it would be up to the compiler to optimize things as much as possible. The benefit of such a method is that one could test various performance of different strategies and even, say, attempt to move one automatic management(implicit deallocations) to explicit to improve performance. Basically the compiler could insert implicit deallocations anywhere it things they are suppose to go... e.g., for local variables that never have outside references. The allocation strategy could do nothing(e.g., AGC) or immediately free(e.g., malloc) or reference count(e.g., ARC) or even throw an error(e.g., MMM: "oops! I forget to deallocate or use @weak). But what's great, is that by using a pattern for memory allocation and deallocation, regardless if the above method is the basis or not, D and phobos are no longer hard coded to one scheme or another and a proper foundation is laid instead one that is inflexible and limiting. There would no longer be arguments about which memory management method is best and more time with getting things done. Also, it would be very appealing to the outside world because, say, they need to write some code for the pic16 and need a very specific way of handling memory... they'll much more likely be able to do it efficiently in D than any other language... since not only does D offer complete abstinence of built in management, it would also allow "strategies" to be used. Anyways, just an idea... hopefully good enough to get the point across that we need a better way and we all can stop arguing over specific details that are somewhat moot. |
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Frustrated | On 2/6/14, 9:54 AM, Frustrated wrote: > Any time you hard code code something you are also creating > constraints. If people build something on top of what you have > built they inherit those constrains. Nice thought. > So, why not create a design pattern that allows one, at compile > time, to use any specific memory management technique they want > for the runtime and library(and user code)? We have a very powerful mechanism for what could be called "statically deferred committal": templates. Phobos did a great job at not committing to e.g. range representation or string representation. It seems to me we have a clean shot at applying the wealth of knowledge and experience we accumulated with that, to deferring approaches to allocation. > e.g., switches could be used to recompile the source when a > different pattern is desired or libraries could be created that > use different methods. e.g., phobos64GC.lib. That won't even be necessary with templates. It would be necessary if we have some sort of global flag dictating a fundamental fork. > To make any of this work, and work efficiently, one must abstract > what is common between all memory management techniques. > > Since allocation is always explicitly defined, it is easy to > handle. Which every memory management technique is used, it > would be called/notified on allocation. The deallocation is the > tricky part. (reallocation is another potential issue) Yah, std.allocator aims at formalizing that (in an untyped manner). > For manual memory management the deallocation is explicitly > called by the user. This in and of itself is significant and > either allows the compiler to know that manual memory management > is used or to replace it with automatic management if desired. > > e.g., > > { > auto a = new!strategy A; > > release a; // <- explicit deallocation: replaced with > strategy.explicitDeallocation(a); > } > > vs > > { > auto a = new!strategy A; > > // implicit deallocation: compiler inserts > strategy.scopeDeallocation(a); > } I don't think this works. The scope of "strategy" and "a" may be unrelated. More importantly a may be escaped (e.g. by passing it to functions etc) unbeknownst to "strategy", which in fact suggests that the strategy must be somehow known by "a" independently. (Stopped reading here.) Andrei |
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Frustrated | I like the idea, but instead of new!strategy I would prefer: use @strategy(RefCount) or use @strategy(GC) // Default We would have in druntime some functions which are tagged with @strategy(Whatever) (which are invoked if the use comes): ---- @strategy(C_Memory) T[] new(T : U[], U)(size_t count) { return (cast(U*) calloc(count, T.sizeof))[0 .. count]; } @strategy(C_Memory) void delete(T : U[], U)(ref U[] arr) { free(arr.ptr); arr = null; } ---- It's not perfect, just a thought. |
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On Thursday, 6 February 2014 at 18:18:49 UTC, Andrei Alexandrescu wrote:
> On 2/6/14, 9:54 AM, Frustrated wrote:
>> {
>> auto a = new!strategy A;
>>
>> // implicit deallocation: compiler inserts
>> strategy.scopeDeallocation(a);
>> }
>
> I don't think this works. The scope of "strategy" and "a" may be unrelated. More importantly a may be escaped (e.g. by passing it to functions etc) unbeknownst to "strategy", which in fact suggests that the strategy must be somehow known by "a" independently.
Perhaps he meant doing something like: this refence dies here, control of the object is now passed to the "Strategy Manager", that could be a reference counter that decreases its counter, the GC that takes care of everything, and so on.
|
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to fra | On Thursday, 6 February 2014 at 18:44:49 UTC, fra wrote:
> On Thursday, 6 February 2014 at 18:18:49 UTC, Andrei Alexandrescu wrote:
>> On 2/6/14, 9:54 AM, Frustrated wrote:
>>> {
>>> auto a = new!strategy A;
>>>
>>> // implicit deallocation: compiler inserts
>>> strategy.scopeDeallocation(a);
>>> }
>>
>> I don't think this works. The scope of "strategy" and "a" may be unrelated. More importantly a may be escaped (e.g. by passing it to functions etc) unbeknownst to "strategy", which in fact suggests that the strategy must be somehow known by "a" independently.
> Perhaps he meant doing something like: this refence dies here, control of the object is now passed to the "Strategy Manager", that could be a reference counter that decreases its counter, the GC that takes care of everything, and so on.
Yes,
ScopeDeallocation would either be nop for manual allocation or a
decrements of a reference counter.
For example, if you were using manual allocation but backed by
ARC or GC then scopeDeallocation would actually do something. If
you wanted completely control then it could be a nop or a warning.
Basically have the compiler fill in all the missing information
that it can in "implicit calls" to the strategy method. These
could be "overridden" to do whatever one wants depending on what
the goal is. If an explicit call is made then the compiler won't
add the implicit call.
|
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Namespace | On Thursday, 6 February 2014 at 18:41:25 UTC, Namespace wrote:
> I like the idea, but instead of new!strategy I would prefer:
> use @strategy(RefCount)
> or
> use @strategy(GC) // Default
>
> We would have in druntime some functions which are tagged with @strategy(Whatever) (which are invoked if the use comes):
> ----
> @strategy(C_Memory)
> T[] new(T : U[], U)(size_t count) {
> return (cast(U*) calloc(count, T.sizeof))[0 .. count];
> }
>
> @strategy(C_Memory)
> void delete(T : U[], U)(ref U[] arr) {
> free(arr.ptr);
> arr = null;
> }
> ----
>
> It's not perfect, just a thought.
I'm sure there are a lot of ways to do it. I think the hard part
is coming up with a cohesive and elegant solution that is
efficient. By providing an allocation strategy we can deal with
specific memory issues.
|
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On Thursday, 6 February 2014 at 18:18:49 UTC, Andrei Alexandrescu wrote: > On 2/6/14, 9:54 AM, Frustrated wrote: >> Any time you hard code code something you are also creating >> constraints. If people build something on top of what you have >> built they inherit those constrains. > > Nice thought. > >> So, why not create a design pattern that allows one, at compile >> time, to use any specific memory management technique they want >> for the runtime and library(and user code)? > > We have a very powerful mechanism for what could be called "statically deferred committal": templates. Phobos did a great job at not committing to e.g. range representation or string representation. It seems to me we have a clean shot at applying the wealth of knowledge and experience we accumulated with that, to deferring approaches to allocation. > >> e.g., switches could be used to recompile the source when a >> different pattern is desired or libraries could be created that >> use different methods. e.g., phobos64GC.lib. > > That won't even be necessary with templates. It would be necessary if we have some sort of global flag dictating a fundamental fork. Yeah, I was thinking templates could be used here to good effect. >> To make any of this work, and work efficiently, one must abstract >> what is common between all memory management techniques. >> >> Since allocation is always explicitly defined, it is easy to >> handle. Which every memory management technique is used, it >> would be called/notified on allocation. The deallocation is the >> tricky part. (reallocation is another potential issue) > > Yah, std.allocator aims at formalizing that (in an untyped manner). > yeah, and I think it is definitely a step in the right direction... Just have to get the core and library to be along those lines. >> For manual memory management the deallocation is explicitly >> called by the user. This in and of itself is significant and >> either allows the compiler to know that manual memory management >> is used or to replace it with automatic management if desired. >> >> e.g., >> >> { >> auto a = new!strategy A; >> >> release a; // <- explicit deallocation: replaced with >> strategy.explicitDeallocation(a); >> } >> >> vs >> >> { >> auto a = new!strategy A; >> >> // implicit deallocation: compiler inserts >> strategy.scopeDeallocation(a); >> } > > I don't think this works. The scope of "strategy" and "a" may be unrelated. More importantly a may be escaped (e.g. by passing it to functions etc) unbeknownst to "strategy", which in fact suggests that the strategy must be somehow known by "a" independently. > See the other post about this. scopeDeallocation meant to simply signal that the scope of a has ended but not necessarily there are no references to a. Would be used more in AGC and ARC than anything but could be used in other ways. The idea, I think, is to have the compiler insert as much implicit information as possible to handle stuff with the ability to override with explicit calls and to even override what the implicit calls do. It would sort of have the effect of adding "hooks" into the compiler to tell it what to do on allocations and deallocations. |
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Frustrated | On 2/6/14, 12:01 PM, Frustrated wrote:
> See the other post about this. scopeDeallocation meant to simply
> signal that the scope of a has ended but not necessarily there
> are no references to a.
So that's a struct destructor.
Andrei
|
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On Thursday, 6 February 2014 at 20:24:55 UTC, Andrei Alexandrescu
wrote:
> On 2/6/14, 12:01 PM, Frustrated wrote:
>> See the other post about this. scopeDeallocation meant to simply
>> signal that the scope of a has ended but not necessarily there
>> are no references to a.
>
> So that's a struct destructor.
>
> Andrei
Well, except it hooks into the memory allocation strategy.
I'm not saying that what I outlined above is perfect or the way
to go but just an idea ;)
If new had some way to pass an "interface" that contained the
allocation strategy details and one had the notation like
new!MyManualAllocator A;
then I suppose A's destructor could call MyManualAllocator's
"scopeDeallocator" method and you wouldn't need an implicit call
there.
In either case though, it is implicit and done behind the scenes
by the compiler.
But if it's done by the destructor then it is, in some sense,
"hidden" and it feels hard coded since we normally don't think of
the user being able to control how an object is able to destruct
itself. (usually this is done by the person that creates the
object type)
In some sense, destructors clean up their own mess they created
but not make assumptions about how they were created.
The problem then becomes, does new!MyManualAllocator A; flow
through and A uses MyManualAllocator internally or does it use
it's own or can we for A to use MyManualAllocator(or some other
allocator)?
|
February 06, 2014 Re: Disadvantages of building a compiler and library on top of a specific memory management scheme | ||||
---|---|---|---|---|
| ||||
Posted in reply to Frustrated | On 2/6/14, 12:51 PM, Frustrated wrote:
> On Thursday, 6 February 2014 at 20:24:55 UTC, Andrei Alexandrescu
> wrote:
>> On 2/6/14, 12:01 PM, Frustrated wrote:
>>> See the other post about this. scopeDeallocation meant to simply
>>> signal that the scope of a has ended but not necessarily there
>>> are no references to a.
>>
>> So that's a struct destructor.
>>
>> Andrei
>
> Well, except it hooks into the memory allocation strategy.
>
> I'm not saying that what I outlined above is perfect or the way
> to go but just an idea ;)
>
> If new had some way to pass an "interface" that contained the
> allocation strategy details and one had the notation like
>
> new!MyManualAllocator A;
>
> then I suppose A's destructor could call MyManualAllocator's
> "scopeDeallocator" method and you wouldn't need an implicit call
> there.
What is MyManualAllocator - type or value?
I should emphasize that obsessing over the syntax is counterproductive. Call a blessed function and call it a day.
Andrei
|
Copyright © 1999-2021 by the D Language Foundation