Jump to page: 1 2 3
Thread overview
Disadvantages of building a compiler and library on top of a specific memory management scheme
Feb 06, 2014
Frustrated
Feb 06, 2014
fra
Feb 06, 2014
Frustrated
Feb 06, 2014
Frustrated
Feb 06, 2014
Frustrated
Feb 06, 2014
Frustrated
Feb 07, 2014
Németh Péter
Feb 07, 2014
Frustrated
Feb 08, 2014
Marco Leise
Feb 08, 2014
logicchains
Feb 08, 2014
Daniel Murphy
Feb 08, 2014
Daniel Murphy
Feb 08, 2014
Paulo Pinto
Feb 06, 2014
Namespace
Feb 06, 2014
Frustrated
February 06, 2014
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
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
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
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
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
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
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
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
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
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
« First   ‹ Prev
1 2 3