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);

Reply via email to