On Wednesday, 29 August 2012 at 15:07:42 UTC, F i L wrote:
Actually, now that I think about it, there's an potentially
better way. Simply have static analysis do the work for us:
class A
{
int a;
this new() {
// if 'this = ...' is found before 'this.whatever' then
// the automatic allocation is overriden. So we have no need
// for any kind of @noalloc/@alloc() distinction.
// More importantly, because allocation is type specific, we
// strip this out when calling it from a derived class (see
B)
this = GC.alloc(A); // this stripped when called from
B.new()
this.a = ...;
}
}
class B : A
{
int b;
this new() {
super.new(); // use A.new() except for allocation
this.b = ...;
}
}
Basically what's happening is two functions are built out for
each class constructor which defines a 'this = ...': one with
the allocation stuff, and one without. When a derived class
calls the super classes constructor, it's calling the one built
without the allocation stuff.
There could also be some kind of cool tricks involved. For
instance of you use 'typeof(this)' with 'GC.alloc()' (instead
of 'A'), then it could keep the allocation stuff and the
super.new() constructor and use the allocation logic, but still
allocate the size appropriate for type 'B' when it's called:
class A
{
this new() {
if (condition) {
this = GC.alloc(typeof(this));
}
else {
this = malloc(typeof(this));
}
...
}
}
class B
{
this new() {
super.new(); // same allocation rules as A
...
}
}
However, that last part's just a side thought, and I'm not sure
if it would really work, or what the implementation costs would
be.
By this form of definition that's all suggested, you'd be mixing
if it was heap or non-heap allocated. I'm not talking about Class
A & B, I'm talking about things they contain.
Assume class B was defined instead.
class B : A {
C something;
this new() {
super.new(); // same allocation rules as A
...
something = new C(); //and however it's made
}
}
Now you have the following:
1) Sometimes A/B is heap and sometimes not
2) Class C may or may not be heap allocated we don't know (It's
an implementation detail)
If A/B happens to be stack allocated then when it leaves the
frame (or abandoned), no harm done (C is abandoned and will be
picked up by the GC later safely)
Let's reverse it so C is the outer class, and let's assume A is
defined to use something like... alloca (stack). Now you have:
class C {
B mysteryNew;
this new() {
mysteryNew = new B();
}
}
Oops! Now leaving new (or the constructor, or whatever)
mysteryNew is now an invalid object! So if there's another new
option you can decide 'maybe' which one you may want to use.
int currentCounter;
B[10] globalReservedB;
class C {
B mysteryNew;
this new1() @alloc {
if (currentCounter < globalReservedB) {
//because it's faster? And we all know faster is better!
globalReservedB[currentCounter] = new1 B();
mysteryNew = globalReservedB[currentCounter++];
} else
assert(0);
}
}
Whew! Wait! No!!!! globalReservedB still is just a reference
pointer and not preallocated space, meaning you'd have to do
fancy low level magic to actually store stuff there. mysteryNew
now points to a global reference holder that holds an invalid
pointer.
Say the authors of zlib make a D class that does compression at
a low level because D is better than C. By default they used the
standard new.
LATER they decide that zlib compression should only happen on
the stack because you only need to compress very very small
buffers (for something like chat text where you only have maybe
4k to worry about), so they override new with their own that uses
alloca. They don't want to change it to a struct because it's
already a class.
Now what?? There's no guarantees anymore at all! If you update a
library you'd need to read every implementation detail to make
sure the updates don't break current code and maybe add checks
everywhere. What a headache!
interface IZlib {
void compress(string input);
string decompress(string input);
ubyte[] flush();
void empty();
}
//intended use.
ubyte[] compressText(string text) {
Zlib zText = new Zlib();
zText.compress(text);
return zText.flush();
}
//our fancy function wants to do multiple reads/writes.
Zlib compressText(Zlib zText, string text) {
if (zText is null)
zText = new Zlib();
zText.compress(text);
return zText;
}
Assuming it allocates on the heap and where we assumed it would
always go, the function probably would work fine. If Zlib changed
to alloca since it should never be used otherwise (and they even
make an explicit note), but it could break previously compiled
code.
class Zlib : IZlib {
...
}
//could contain mystery allocator
IZlib ztext = compressText(null, "Hello world");
//could crash, if it was pre-compiled code to call a library
//this is now unavoidable and was shipped to millions of
customers
compressText(ztext, " Hello Hello!");
Now let's assume you aren't allocating on the stack (because
they think the stack is a stupid place to store stuff) but we
don't want to use the GC. Let's assume Zlib was used with it's
own new to use malloc so it's compatible with their C
interface(s).
class Something {
Zlib zText;
void clear() {zText = null;}
}
Something s = new Something();
s.zText = new Zlib();
So far so good. Later..
//s isn't needed anymore
//but we don't know if s was still used by something else
//no destructor is called on zText, but we assume the GC
//can pick up the abandoned object and cleanly handle it later.
s.clear();
s = null;
Now zText is abandoned, and will never call it's destructor
since it wasn't registered in the GC. If it is registered, then
it MIGHT call the destructor, or it may only search the area and
manage to free the ubyte[] buffer that it contained still leaving
you with a floating leaked block of memory (no matter how small).
(May be ranting, feel free to ignore me from here on)
Let's assume you have space limitations and you have to do
space efficiently. So we decide to do something Apple/Mac people
did and use a two level management for memory (Or sort of,
pointerless pointers, I read about this so...)
struct sizeBlock{
void* ptr;
int size;
}
void* rawMemory;
sizeBlock[1_024] blockRef; //1k item limit! Maybe we have 64k
only, embedded devices
int allocate(int size); //suspiciously similar to malloc somehow
//simple, but more aptly used for arrays like strings
void deallocate(int index) {
blockRef[index].size = 0;
}
Object isObject(int index) {
return cast(Object) *blockRef[index].ptr;
}
All allocators now need to register their pointers in blockRef.
Should memory need to be shifted, only blockRef is updated and we
can save as many bytes as we need.
class Managed {
int new() @alloc {
int block = allocate(Managed.sizeof); //or however the size
is calculated
this = isObject(block);
//initialize
return block;
}
}
Now if allocate cannot find a block of appropriate size, it can
just shift everything over until the end has room and none of the
classes or references (as long as it honors blockRef) is safe;
Even if rawMemory gets resized/moved later it should be able to
handle it.
int someObject = new Managed(); //so far so good right?
Object o = isObject(someObject); //actively work on/with it
//safe until we decide to allocate something.
//Then we need to refresh our instance of o.
int somethingElse = new Managed();
o = isObject(someObject); //may have moved so we refresh o
//destructor never called! and memory in jeopardy! Worse is any
contents
//are likely abandoned and a larger memory leak issue has
appeared!
deallocate(someObject);