Jump to page: 1 216  
Page
Thread overview
std.allocator needs your help
Sep 23, 2013
Nicholas Londey
Sep 23, 2013
Manu
Sep 23, 2013
Brad Roberts
Sep 23, 2013
Manu
Sep 23, 2013
Walter Bright
Sep 23, 2013
Manu
Sep 23, 2013
Walter Bright
Sep 23, 2013
Walter Bright
Sep 23, 2013
Manu
Sep 23, 2013
Manu
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
Dmitry Olshansky
Sep 25, 2013
kraybit
Sep 25, 2013
kraybit
Sep 26, 2013
Jacob Carlborg
Sep 23, 2013
Johannes Pfau
Sep 23, 2013
Andrej Mitrovic
Sep 23, 2013
Johannes Pfau
Sep 23, 2013
QAston
Sep 23, 2013
Manu
Sep 23, 2013
Simen Kjaeraas
Sep 23, 2013
Manu
Sep 23, 2013
Manu
Sep 24, 2013
Manu
Sep 24, 2013
Jacob Carlborg
Sep 24, 2013
Paulo Pinto
Sep 24, 2013
Paulo Pinto
Sep 24, 2013
Nick Sabalausky
Sep 24, 2013
Robert
Sep 23, 2013
deadalnix
Sep 23, 2013
Walter Bright
Sep 24, 2013
Chad Joan
Sep 23, 2013
Benjamin Thaut
Sep 23, 2013
Andrej Mitrovic
Sep 23, 2013
Manu
Sep 23, 2013
Benjamin Thaut
Sep 23, 2013
Walter Bright
Sep 23, 2013
Walter Bright
Sep 24, 2013
deadalnix
Sep 23, 2013
Walter Bright
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
qznc
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
Manu
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
Dicebot
Sep 23, 2013
ponce
Sep 23, 2013
ponce
Oct 24, 2013
ponce
Sep 23, 2013
Adam D. Ruppe
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
Jacob Carlborg
Sep 23, 2013
Benjamin Thaut
Sep 24, 2013
Namespace
Sep 25, 2013
deadalnix
Sep 25, 2013
Namespace
Sep 25, 2013
Benjamin Thaut
Sep 25, 2013
Johannes Pfau
Sep 25, 2013
Benjamin Thaut
Sep 26, 2013
Jacob Carlborg
Sep 23, 2013
Nick Sabalausky
Sep 24, 2013
H. S. Teoh
Sep 24, 2013
Jacob Carlborg
Sep 23, 2013
deadalnix
Sep 23, 2013
Robert Schadek
Sep 23, 2013
Robert Schadek
Sep 24, 2013
deadalnix
Sep 23, 2013
BLM768
Sep 23, 2013
Timon Gehr
Sep 24, 2013
Jacob Carlborg
Sep 23, 2013
Peter Alexander
Sep 23, 2013
Peter Alexander
Sep 24, 2013
Manu
Sep 24, 2013
Manu
Sep 24, 2013
Peter Alexander
Sep 24, 2013
Peter Alexander
Sep 25, 2013
deadalnix
Sep 24, 2013
Dicebot
Sep 24, 2013
jerro
Sep 23, 2013
Nick Sabalausky
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Dmitry Olshansky
Sep 25, 2013
deadalnix
Sep 25, 2013
sclytrack
Sep 25, 2013
bearophile
Sep 24, 2013
Brad Anderson
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Rainer Schuetze
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Rainer Schuetze
Sep 24, 2013
Rainer Schuetze
Sep 24, 2013
Dmitry Olshansky
Sep 24, 2013
Dan Schatzberg
Sep 24, 2013
deadalnix
Sep 24, 2013
Dan Schatzberg
Sep 24, 2013
Dan Schatzberg
Sep 24, 2013
Dan Schatzberg
Sep 24, 2013
Bigsandwich
Sep 25, 2013
deadalnix
Sep 25, 2013
Peter Alexander
Sep 27, 2013
Yota
Sep 27, 2013
H. S. Teoh
Sep 27, 2013
Brian Schott
Sep 27, 2013
Craig Dillabaugh
Sep 27, 2013
Dicebot
Sep 27, 2013
H. S. Teoh
September 22, 2013
Hello,


I am making good progress on the design of std.allocator, and I am optimistic about the way it turns out. D's introspection capabilities really shine through, and in places the design does feel really archetypal - e.g. "this is the essence of a freelist allocator". It's a very good feeling. The overall inspiration comes from Berger's HeapLayers, but D's introspection takes that pattern to a whole new level.

Several details are still in flux, but at the top level it seems most natural to divide things in two tiers:

1. Typed allocator, i.e. every request for allocation comes with the exact type requested;

2. Untyped allocator - traffics exclusively in ubyte[].

Initially I thought it's impossible to separate the two, but it turns out there are profound generic realities about untyped allocators that make them the perfect foundation for typed allocators, without considerable abstraction costs.

I need to prove that before I'm sure, because so far I only have an archetype of untyped allocators, with typed allocators to follow. There's always the possibility that some hitch shows itself only when actually trying to implement various typed allocators.

Anyhow, here's the general interface of an untyped allocator. In fact I'll showcase the simplest allocator - the NullAllocator, which has zero memory to offer. (In spite of its do-absolutely-nothing stance, the NullAllocator is surprisingly useful as a "terminator" for certain layered allocators.) Anyhow, here it is (explanations follow):

struct NullAllocator
{
    enum alignment = real.alignof;
    enum size_t available = 0;
    ubyte[] allocate(size_t s) shared { return null; }
    bool expand(ref ubyte[] b, size_t minDelta, size_t maxDelta) shared
    { assert(b is null); return false; }
    bool reallocate(ref ubyte[] b, size_t) shared
    { assert(b is null); return false; }
    void deallocate(ubyte[] b) shared { assert(b is null); }
    void collect() shared { }
    void deallocateAll() shared { }
    static shared NullAllocator it;
}

Primitives:

* alignment is a compile-time constant yielding the alignment of allocated data. Many allocators offer the maximum alignment of the platform (for which I used real.alignof above). This constant is required for all allocators.

* available is a property that returns how many total (not necessarily contiguous) bytes are available for allocation. The NullAllocator knows statically it has 0 bytes available so it implements it as an enum. Generally allocators will implement it as a @property. This property is optional.

* allocate(s) returns a ubyte[] with length at least s, or null. (It does not throw on out-of-memory because that would hurt composability; it turns out many elemental allocators do naturally run out of memory.) This method is required for all allocators. In most allocators this method should be @safe.

* expand(b, minDelta, maxDelta) grows b's length by at least minDelta (and on a best-effort basis by at least maxDelta) and returns true, or does nothing and returns false. In most allocators this should be @safe. (One key insight is that expand() can be made @safe whereas shrink() or realloc() are most often not; such mini-epiphanies are very exciting because they put the design on a beam guide with few principles and many consequences.) The precondition is that b is null or has been previously returned by allocate(). This method is optional.

* reallocate(b, s) reallocates (like C's realloc) b to at least s bytes and returns true, or does nothing and returns false. It is possible that the memory is moved. This method is optional, and usually unsafe. (This was mostly introduced to accommodate C's realloc() within the allocator design.)

* deallocate(b) deallocates the memory allocated for b. b must have been previously allocated by the same allocator. This method is usually unsafe (but there are notable implementations that may offer safety, such as unbounded freelists.) This method is optional.

* deallocateAll() deallocates in one shot all memory previously allocated by this allocator. This method is optional, and when present is almost always unsafe (I see no meaningful @safe implementation.) Region allocators are notable examples of allocators that define this method.

* collect() frees unused memory that had been allocated with this allocator. This optional method is implemented by tracing collectors and is usually @safe.

* "it" is an interesting artifact. Allocators may or may not hold per-instance state. Those that don't are required to define a global shared or thread-local singleton called "it" that will be used for all calls related to allocation. Of course, it is preferred for maximum flexibility that "it" is shared so as to clarify that the allocator is safe to use among different threads. In the case of the NullAllocator, this is obviously true, but non-trivial examples are the malloc-based allocator and the global garbage collector, both of which implement thread-safe allocation (at great effort no less).

There are quite a few more things to define more precisely, but this part of the design has become quite stable. To validate the design, I've defined some simple allocators (Mallocator, GCAllocator, Freelist, StackRegion, Region etc) but not the more involved ones, such as coalescing heaps, buddy system etc.

If you have an interest in memory allocators and would like to design (or have around) an allocator that fits the API above, please post it. For layering purposes, don't call malloc() or sbrk() for getting large blocks of memory from the system; instead, use another Allocator as your parent allocator in a pattern as follows:

struct MyAllocator(Allocator)
{
    static if (is(typeof(Allocator.it))) alias Allocator.it _parent;
    else Allocator _parent;
    ... use _parent for getting memory ...
}

This allows MyAllocator to use stateful and stateless allocators transparently by just referring to _parent. Of course, your allocator may take other generic parameters in addition to Allocator, or take none at all if it's a "root" allocator (sbrk() or Mallocator would be examples).

Destroy!


Andrei
September 23, 2013
> * allocate(s) returns a ubyte[] with length at least s, or null. (It does not throw on out-of-memory because that would hurt composability; it turns out many elemental allocators do naturally run out of memory.) This method is required for all allocators. In most allocators this method should be @safe.

Would an 'expected' be a possible alternative to returning null?

Am a big fan of this talk of yours.
http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C
September 23, 2013
On 23 September 2013 09:49, Andrei Alexandrescu < SeeWebsiteForEmail@erdani.org> wrote:

> Hello,
>
>
> I am making good progress on the design of std.allocator, and I am optimistic about the way it turns out. D's introspection capabilities really shine through, and in places the design does feel really archetypal - e.g. "this is the essence of a freelist allocator". It's a very good feeling. The overall inspiration comes from Berger's HeapLayers, but D's introspection takes that pattern to a whole new level.
>
> Several details are still in flux, but at the top level it seems most natural to divide things in two tiers:
>
> 1. Typed allocator, i.e. every request for allocation comes with the exact type requested;
>
> 2. Untyped allocator - traffics exclusively in ubyte[].
>
> Initially I thought it's impossible to separate the two, but it turns out there are profound generic realities about untyped allocators that make them the perfect foundation for typed allocators, without considerable abstraction costs.
>
> I need to prove that before I'm sure, because so far I only have an archetype of untyped allocators, with typed allocators to follow. There's always the possibility that some hitch shows itself only when actually trying to implement various typed allocators.
>
> Anyhow, here's the general interface of an untyped allocator. In fact I'll showcase the simplest allocator - the NullAllocator, which has zero memory to offer. (In spite of its do-absolutely-nothing stance, the NullAllocator is surprisingly useful as a "terminator" for certain layered allocators.) Anyhow, here it is (explanations follow):
>
> struct NullAllocator
> {
>     enum alignment = real.alignof;
>     enum size_t available = 0;
>     ubyte[] allocate(size_t s) shared { return null; }
>     bool expand(ref ubyte[] b, size_t minDelta, size_t maxDelta) shared
>     { assert(b is null); return false; }
>     bool reallocate(ref ubyte[] b, size_t) shared
>     { assert(b is null); return false; }
>     void deallocate(ubyte[] b) shared { assert(b is null); }
>     void collect() shared { }
>     void deallocateAll() shared { }
>     static shared NullAllocator it;
> }
>
> Primitives:
>
> * alignment is a compile-time constant yielding the alignment of allocated data. Many allocators offer the maximum alignment of the platform (for which I used real.alignof above). This constant is required for all allocators.
>
> * available is a property that returns how many total (not necessarily contiguous) bytes are available for allocation. The NullAllocator knows statically it has 0 bytes available so it implements it as an enum. Generally allocators will implement it as a @property. This property is optional.
>
> * allocate(s) returns a ubyte[] with length at least s, or null. (It does not throw on out-of-memory because that would hurt composability; it turns out many elemental allocators do naturally run out of memory.) This method is required for all allocators. In most allocators this method should be @safe.
>
> * expand(b, minDelta, maxDelta) grows b's length by at least minDelta (and
> on a best-effort basis by at least maxDelta) and returns true, or does
> nothing and returns false. In most allocators this should be @safe. (One
> key insight is that expand() can be made @safe whereas shrink() or
> realloc() are most often not; such mini-epiphanies are very exciting
> because they put the design on a beam guide with few principles and many
> consequences.) The precondition is that b is null or has been previously
> returned by allocate(). This method is optional.
>
> * reallocate(b, s) reallocates (like C's realloc) b to at least s bytes
> and returns true, or does nothing and returns false. It is possible that
> the memory is moved. This method is optional, and usually unsafe. (This was
> mostly introduced to accommodate C's realloc() within the allocator design.)
>
> * deallocate(b) deallocates the memory allocated for b. b must have been previously allocated by the same allocator. This method is usually unsafe (but there are notable implementations that may offer safety, such as unbounded freelists.) This method is optional.
>
> * deallocateAll() deallocates in one shot all memory previously allocated by this allocator. This method is optional, and when present is almost always unsafe (I see no meaningful @safe implementation.) Region allocators are notable examples of allocators that define this method.
>
> * collect() frees unused memory that had been allocated with this allocator. This optional method is implemented by tracing collectors and is usually @safe.
>
> * "it" is an interesting artifact. Allocators may or may not hold per-instance state. Those that don't are required to define a global shared or thread-local singleton called "it" that will be used for all calls related to allocation. Of course, it is preferred for maximum flexibility that "it" is shared so as to clarify that the allocator is safe to use among different threads. In the case of the NullAllocator, this is obviously true, but non-trivial examples are the malloc-based allocator and the global garbage collector, both of which implement thread-safe allocation (at great effort no less).
>
> There are quite a few more things to define more precisely, but this part of the design has become quite stable. To validate the design, I've defined some simple allocators (Mallocator, GCAllocator, Freelist, StackRegion, Region etc) but not the more involved ones, such as coalescing heaps, buddy system etc.
>
> If you have an interest in memory allocators and would like to design (or have around) an allocator that fits the API above, please post it. For layering purposes, don't call malloc() or sbrk() for getting large blocks of memory from the system; instead, use another Allocator as your parent allocator in a pattern as follows:
>
> struct MyAllocator(Allocator)
> {
>     static if (is(typeof(Allocator.it))) alias Allocator.it _parent;
>     else Allocator _parent;
>     ... use _parent for getting memory ...
> }
>
> This allows MyAllocator to use stateful and stateless allocators transparently by just referring to _parent. Of course, your allocator may take other generic parameters in addition to Allocator, or take none at all if it's a "root" allocator (sbrk() or Mallocator would be examples).
>
> Destroy!


Well it looks like a good start, but the thing I'm left wondering after
reading this is still... how is it actually used?
I think the greatest challenge is finding a simple, clean and correct way
to actually specify which allocator should be used for making allocations
throughout your code, and perhaps more troublesome; within generic code,
and then layers of generic code.

Are you intending to be able to associate a particular allocator with a
class declaration?
What about a struct declaration?
What about a region of code (ie, a call tree/branch).
What if the given allocator should be used for most of the tree, except for
a couple of things beneath that always want to use their explicit allocator?

What if I want to associate an allocator instance, not just an allocator type (ie, I don't want multiple instances of the same type(/s) of allocators in my code, how are they shared?

It wasn't clear to me from your demonstration, but 'collect()' implies that GC becomes allocator-aware; how does that work?

deallocateAll() and collect() may each free a whole lot of memory, but it seems to me that they may not actually be aware of the individual allocations they are freeing; how do the appropriate destructors for the sub-allocations get called?

I have a suspicion you're going to answer most of these questions with the concept of allocator layering, but I just don't completely see it. It's quite an additional burden of resources and management to manage the individual allocations with a range allocator above what is supposed to be a performance critical allocator to begin with.

C++'s design seems reasonable in some ways, but history has demonstrated that it's a total failure, which is almost never actually used (I've certainly never seen anyone use it).

Some allocators that I use regularly to think about:

A ring-buffer:
  * Like a region allocator I guess, but the circular nature adds some
minor details, and requires the user to mark the heap from time to time,
freeing all allocations between the old mark and the new mark.

A pool:
  * Same as a free-list, but with a maximum size, ie, finite pool of
objects pre-allocated and pre-populating the freelist.

A pool-group:
  * Allocate from a group of pools allocating differently sized objects.
(this is a good test for allocator layering, supporting a group of pools
above, and fallback to the malloc heap for large objects)


September 23, 2013
On 9/22/13 6:34 PM, Nicholas Londey wrote:
>> * allocate(s) returns a ubyte[] with length at least s, or null. (It
>> does not throw on out-of-memory because that would hurt composability;
>> it turns out many elemental allocators do naturally run out of
>> memory.) This method is required for all allocators. In most
>> allocators this method should be @safe.
>
> Would an 'expected' be a possible alternative to returning null?
>
> Am a big fan of this talk of yours.
> http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C

Thanks! I think expected would be appropriate for the high-level allocator.

Andrei

September 23, 2013
On 9/22/13 6:35 PM, Manu wrote:
> Well it looks like a good start, but the thing I'm left wondering after
> reading this is still... how is it actually used?
> I think the greatest challenge is finding a simple, clean

Oxford comma please :o)

> and correct
> way to actually specify which allocator should be used for making
> allocations throughout your code, and perhaps more troublesome; within
> generic code, and then layers of generic code.

My design makes it very easy to experiment by allowing one to define complex allocators out of a few simple building blocks. It is not a general-purpose allocator, but it allows one to define any number of such.

> Are you intending to be able to associate a particular allocator with a
> class declaration?

No, or at least not at this level.

> What about a struct declaration?

Same answer.

> What about a region of code (ie, a call tree/branch).
> What if the given allocator should be used for most of the tree, except
> for a couple of things beneath that always want to use their explicit
> allocator?

The proposed design makes it easy to create allocator objects. How they are used and combined is left to the application.

> What if I want to associate an allocator instance, not just an allocator
> type (ie, I don't want multiple instances of the same type(/s) of
> allocators in my code, how are they shared?

An allocator instance is a variable like any other. So you use the classic techniques (shared globals, thread-local globals, passing around as parameter) for using the same allocator object from multiple places.

> It wasn't clear to me from your demonstration, but 'collect()' implies
> that GC becomes allocator-aware; how does that work?

No, each allocator has its own means of dealing with memory. One could define a tracing allocator independent of the global GC.

> deallocateAll() and collect() may each free a whole lot of memory, but
> it seems to me that they may not actually be aware of the individual
> allocations they are freeing; how do the appropriate destructors for the
> sub-allocations get called?

No destructors are called at this level. Again, all these allocators understand is ubyte[].

> I have a suspicion you're going to answer most of these questions with
> the concept of allocator layering, but I just don't completely see it.

No, it's just that at this level some of these questions don't even have an appropriate answer - like we discuss atoms and molecules and you ask about building floors and beams and pillars.

> It's quite an additional burden of resources and management to manage
> the individual allocations with a range allocator above what is supposed
> to be a performance critical allocator to begin with.

I don't understand this.

> C++'s design seems reasonable in some ways, but history has demonstrated
> that it's a total failure, which is almost never actually used (I've
> certainly never seen anyone use it).

Agreed. I've seen some uses of it that quite fall within the notion of the proverbial exception that prove the rule.

> Some allocators that I use regularly to think about:
>
> A ring-buffer:
>    * Like a region allocator I guess, but the circular nature adds some
> minor details, and requires the user to mark the heap from time to time,
> freeing all allocations between the old mark and the new mark.
>
> A pool:
>    * Same as a free-list, but with a maximum size, ie, finite pool of
> objects pre-allocated and pre-populating the freelist.

I implemented the finite size for a freelist.

> A pool-group:
>    * Allocate from a group of pools allocating differently sized
> objects. (this is a good test for allocator layering, supporting a group
> of pools above, and fallback to the malloc heap for large objects)

I implemented that as well, it's one of the best designs I've defined in my life.


Andrei

September 23, 2013
On 9/22/13 7:28 PM, Andrei Alexandrescu wrote:
> On 9/22/13 6:35 PM, Manu wrote:
>
>> Are you intending to be able to associate a particular allocator with a
>> class declaration?
>
> No, or at least not at this level.
>
>> What about a struct declaration?
>
> Same answer.
>
>> What about a region of code (ie, a call tree/branch).
>> What if the given allocator should be used for most of the tree, except
>> for a couple of things beneath that always want to use their explicit
>> allocator?
>
> The proposed design makes it easy to create allocator objects. How they are used and combined is
> left to the application.

I think the question he's asking, which a lot of people are anxiously anticipating is... how does the intersect with the parts of the language and core libraries that have been blocked (for a loose definition of blocked) waiting for the allocator design.  Primarily, but far from exclusively, the container library.

Yes, the allocator design is clearly a lower level component, but it's also far easier than the integration with the language and libraries.
September 23, 2013
On 9/22/13 7:45 PM, Brad Roberts wrote:
> I think the question he's asking, which a lot of people are anxiously
> anticipating is... how does the intersect with the parts of the language
> and core libraries that have been blocked (for a loose definition of
> blocked) waiting for the allocator design.  Primarily, but far from
> exclusively, the container library.
>
> Yes, the allocator design is clearly a lower level component, but it's
> also far easier than the integration with the language and libraries.

Containers and other objects that want to allow customization of their allocation would be parameterized during compilation with an allocator type. Functions that need to allocate memory may similarly accept a parameter of allocator type.

One possibility I'm keeping in mind is that of defining a dynamic interface (i.e. in the OOP sense) for a global allocator. Then people can globally change what allocator must be used for operator new invocations.

The work at the level described so far is quite orthogonal on these high level choices. Right now it's all about a statically-typed allocator that is good at allocating and deallocating octets.


Andrei

September 23, 2013
On 9/22/2013 4:49 PM, Andrei Alexandrescu wrote:
> Destroy!

It should be compatible with:

http://wiki.dlang.org/DIP46

or the DIP should be adaptable to work with std.allocator.
September 23, 2013
On 23 September 2013 12:28, Andrei Alexandrescu < SeeWebsiteForEmail@erdani.org> wrote:

> On 9/22/13 6:35 PM, Manu wrote:
>
>> Well it looks like a good start, but the thing I'm left wondering after
>> reading this is still... how is it actually used?
>> I think the greatest challenge is finding a simple, clean
>>
>
> Oxford comma please :o)
>
>
>  and correct
>> way to actually specify which allocator should be used for making allocations throughout your code, and perhaps more troublesome; within generic code, and then layers of generic code.
>>
>
> My design makes it very easy to experiment by allowing one to define complex allocators out of a few simple building blocks. It is not a general-purpose allocator, but it allows one to define any number of such.


Oh okay, so this isn't really intended as a system then, so much a
suggested API?
That makes almost all my questions redundant. I'm interested in the system,
not the API of a single allocator (although your API looks fine to me).
I already have allocators I use in my own code. Naturally, they don't
inter-operate with anything, and that's what I thought std.allocator was
meant to address.

 Are you intending to be able to associate a particular allocator with a
>> class declaration?
>>
>
> No, or at least not at this level.
>
>
>  What about a struct declaration?
>>
>
> Same answer.
>
>
>  What about a region of code (ie, a call tree/branch).
>> What if the given allocator should be used for most of the tree, except for a couple of things beneath that always want to use their explicit allocator?
>>
>
> The proposed design makes it easy to create allocator objects. How they are used and combined is left to the application.


Is that the intended limit of std.allocator's responsibility, or will patterns come later?

Leaving the usage up to the application means we've gained nothing.
I already have more than enough allocators which I use throughout my code.
The problem is that they don't inter-operate, and certainly not with
foreign code/libraries.
This is what I hoped std.allocator would address.

 What if I want to associate an allocator instance, not just an allocator
>> type (ie, I don't want multiple instances of the same type(/s) of
>> allocators in my code, how are they shared?
>>
>
> An allocator instance is a variable like any other. So you use the classic techniques (shared globals, thread-local globals, passing around as parameter) for using the same allocator object from multiple places.


Okay, that's fine... but this sort of manual management implies that I'm using it explicitly. That's where it all falls down for me.

Eg, I want to use a library, it's allocation patterns are incompatible with
my application; I need to provide it with an allocator.
What now? Is every library responsible for presenting the user with a
mechanism for providing allocators? What if the author forgets? (a problem
I've frequently had to chase up in the past when dealing with 3rd party
libraries)

Once a library is designed to expect a user to supply an allocator, what
happens if the user doesn't? Fall-back logic/boilerplate exists in every
library I guess...
And does that mean that applications+libraries are required to ALWAYS
allocate through given allocator objects?
That effectively makes the new keyword redundant. And what about the GC?

I can't really consider std.allocator intil it presents some usage patterns.

 It wasn't clear to me from your demonstration, but 'collect()' implies
>> that GC becomes allocator-aware; how does that work?
>>
>
> No, each allocator has its own means of dealing with memory. One could define a tracing allocator independent of the global GC.


I'm not sure what this means. Other than I gather that the GC and
allocators are fundamentally separate?
Is it possible to create a tracing allocator without language support? Does
the current language insert any runtime calls to support the GC?

I want a ref-counting GC for instance to replace the existing GC, but it's impossible to implement one of them nicely without support from the language, to insert implicit inc/dec ref calls all over the place, and to optimise away redundant inc/dec sequences.

 deallocateAll() and collect() may each free a whole lot of memory, but
>> it seems to me that they may not actually be aware of the individual allocations they are freeing; how do the appropriate destructors for the sub-allocations get called?
>>
>
> No destructors are called at this level. Again, all these allocators understand is ubyte[].


Fair enough. That's a problem for another time then.

 I have a suspicion you're going to answer most of these questions with
>> the concept of allocator layering, but I just don't completely see it.
>>
>
> No, it's just that at this level some of these questions don't even have an appropriate answer - like we discuss atoms and molecules and you ask about building floors and beams and pillars.


Yep, fair enough.
I just saw "I am making good progress on the design of std.allocator" and
presumed you had some thoughts about actual usage semantics of the system.
I can easily define an allocator to use in my own code if it's entirely up
to me how I use it, but that completely defeats the purpose of this
exercise.
Until there aren't standard usage patterns, practises, conventions that ALL
code follows, then we have nothing. I was hoping to hear your thoughts
about those details.

 It's quite an additional burden of resources and management to manage
>> the individual allocations with a range allocator above what is supposed to be a performance critical allocator to begin with.
>>
>
> I don't understand this.


It's irrelevant here.
But fwiw, in relation to the prior point about block-freeing a range
allocation; there will be many *typed* allocations within these ranges, but
a typical range allocator doesn't keep track of the allocations within.
This seems like a common problem that may or may not want to be addressed
in std.allocator.
If the answer is simply "your range allocator should keep track of the
offsets of allocations, and their types", then fine. But that seems like
boilerplate that could be automated, or maybe there is a different/separate
system for such tracking?

 C++'s design seems reasonable in some ways, but history has demonstrated
>> that it's a total failure, which is almost never actually used (I've certainly never seen anyone use it).
>>
>
> Agreed. I've seen some uses of it that quite fall within the notion of the proverbial exception that prove the rule.


I think the main fail of C++'s design is that it mangles the type.
I don't think a type should be defined by the way it's memory is allocated,
especially since that could change from application to application, or even
call to call. For my money, that's the fundamental flaw in C++'s design.

 Some allocators that I use regularly to think about:
>>
>> A ring-buffer:
>>    * Like a region allocator I guess, but the circular nature adds some
>> minor details, and requires the user to mark the heap from time to time,
>> freeing all allocations between the old mark and the new mark.
>>
>> A pool:
>>    * Same as a free-list, but with a maximum size, ie, finite pool of
>> objects pre-allocated and pre-populating the freelist.
>>
>
> I implemented the finite size for a freelist.
>
>
>  A pool-group:
>>    * Allocate from a group of pools allocating differently sized
>> objects. (this is a good test for allocator layering, supporting a group
>> of pools above, and fallback to the malloc heap for large objects)
>>
>
> I implemented that as well, it's one of the best designs I've defined in my life.


Well as an atom, as you say, it seems like a good first step.
I can't see any obvious issues, although I don't think I quite understand
the collect() function if it has no relation to the GC. What is it's
purpose?
If the idea is that you might implement some sort of tracking heap which is
able to perform a collect, how is that actually practical without language
support?

I had imagined going into this that, like the range interface which the
_language_ understands and interacts with, the allocator interface would be
the same, ie, the language would understand this API and integrate it with
'new', and the GC... somehow.
If allocators are just an object like in C++ that people may or may not
use, I don't think it'll succeed as a system. I reckon it needs deep
language integration to be truly useful.

The key problem to solve is the friction between different libraries, and
different moments within a single application its self.
I feel almost like the 'current' allocator needs to be managed as some sort
of state-machine. Passing them manually down the callstack is no good. And
'hard' binding objects to their allocators like C++ is no good either.


September 23, 2013
On 23 September 2013 13:01, Andrei Alexandrescu < SeeWebsiteForEmail@erdani.org> wrote:

> On 9/22/13 7:45 PM, Brad Roberts wrote:
>
>> I think the question he's asking, which a lot of people are anxiously anticipating is... how does the intersect with the parts of the language and core libraries that have been blocked (for a loose definition of blocked) waiting for the allocator design.  Primarily, but far from exclusively, the container library.
>>
>> Yes, the allocator design is clearly a lower level component, but it's also far easier than the integration with the language and libraries.
>>
>
> Containers and other objects that want to allow customization of their allocation would be parameterized during compilation with an allocator type. Functions that need to allocate memory may similarly accept a parameter of allocator type.
>

You've just described C++ verbatim.
And you've previously agreed that C++'s approach totally failed. Can you
explain how this is different from C++, and how it solves the issues that
defeated that design?

One possibility I'm keeping in mind is that of defining a dynamic interface
> (i.e. in the OOP sense) for a global allocator. Then people can globally change what allocator must be used for operator new invocations.
>

Right, this is getting warmer. It's a stark contrast to what you suggest
above though, and when I try and think this through, it gets very complex,
very fast.
I can't imagine how such a system could ever be declared safe. However,
this is more or less what I want. I don't know how to reconcile the 2
requirements.

How much have you thought on this? This is where I think some serious creativity will need to be applied...

Following this train of thought, I can imagine a really nice end goal would be that the existing GC is effectively plugged in as a library, and people can easily substitute it for their own GC if they want/need to.

The work at the level described so far is quite orthogonal on these high
> level choices. Right now it's all about a statically-typed allocator that is good at allocating and deallocating octets.
>
>
> Andrei
>
>


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