March 03, 2015
On Tuesday, 3 March 2015 at 01:23:24 UTC, Zach the Mystic wrote:
> Which makes me think about a bigger problem... when you opAssign, don't you redirect the variable to a different instance? Won't the destructor then destroy *that* instance (or not destroy it, since it just got a +1 count) instead of the one most recently decremented? How does it hold onto the instance to be destroyed?

auto y = new RcStruct;
y = null;

y's old RcStruct gets decremented to zero, but who owns it now? Whose destructor ever gets run on it?
March 03, 2015
On 3/2/2015 4:40 PM, deadalnix wrote:
> After moving resources, the previous owner can no longer be used.

How does that work with the example presented by Marc?

March 03, 2015
On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
> On 3/2/2015 4:40 PM, deadalnix wrote:
>> After moving resources, the previous owner can no longer be used.
>
> How does that work with the example presented by Marc?

He couldn't pass s and a member of s because s is borrowed as mutable.
He would have to pass both as immutable.
March 03, 2015
On Tuesday, 3 March 2015 at 02:04:15 UTC, weaselcat wrote:
> On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
>> On 3/2/2015 4:40 PM, deadalnix wrote:
>>> After moving resources, the previous owner can no longer be used.
>>
>> How does that work with the example presented by Marc?
>
> He couldn't pass s and a member of s because s is borrowed as mutable.
> He would have to pass both as immutable.

Er, I was speaking in terms of Rust of course(it has no distinction between const and immutable like D afaik)
March 03, 2015
On Tuesday, 3 March 2015 at 02:10:23 UTC, weaselcat wrote:
> On Tuesday, 3 March 2015 at 02:04:15 UTC, weaselcat wrote:
>> On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
>>> On 3/2/2015 4:40 PM, deadalnix wrote:
>>>> After moving resources, the previous owner can no longer be used.
>>>
>>> How does that work with the example presented by Marc?
>>
>> He couldn't pass s and a member of s because s is borrowed as mutable.
>> He would have to pass both as immutable.
>
> Er, I was speaking in terms of Rust of course(it has no distinction between const and immutable like D afaik)

http://dpaste.com/30WAMVQ

I hope this helps explain it and I didn't butcher it too bad : )
Sorry for the triple reply.
March 03, 2015
On 3/2/2015 6:04 PM, weaselcat wrote:
> On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
>> On 3/2/2015 4:40 PM, deadalnix wrote:
>>> After moving resources, the previous owner can no longer be used.
>>
>> How does that work with the example presented by Marc?
>
> He couldn't pass s and a member of s because s is borrowed as mutable.
> He would have to pass both as immutable.

A pointer to s could be obtained otherwise and passed.
March 03, 2015
On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
> On 3/2/2015 4:40 PM, deadalnix wrote:
>> After moving resources, the previous owner can no longer be used.
>
> How does that work with the example presented by Marc?

I'm not sure what you don't understand. The comment in the sample code seems clear to me.

// "Move" `a` into `b`
// Here's what happens under the hood: the pointer `a` gets copied (*not*
// the data on the heap, just its address) into `b`. Now both are pointers
// to the *same* heap allocated data. But now, `b` *owns* the heap
// allocated data; `b` is now in charge of freeing the memory in the heap.
let b = a;

Now you have a variable b that owns the data. a is not usable anymore.

// After the previous move, `a` can no longer be used
// Error! `a` can no longer access the data, because it no longer owns the
// heap memory
//println!("a contains: {}", a);
// TODO ^ Try uncommenting this line

As explained here, if a is used, this is an error.

In the example a is move to b. If you had let b = &a, then b would borrow from a. The same way, a would not be usable anymore. The difference with move is that, when b is destroyed, in the first case memory is freed. in the second case, memory is not freed and a is "reenabled".

It is either possible to borrow once and disable who you borrow from, or borrow multiple time and everything become immutable for the time you borrow.

This makes the problems mentioned in this thread effectively impossible happen.
March 03, 2015
On 3/2/2015 9:27 PM, deadalnix wrote:
> On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
>> On 3/2/2015 4:40 PM, deadalnix wrote:
>>> After moving resources, the previous owner can no longer be used.
>>
>> How does that work with the example presented by Marc?
>
> I'm not sure what you don't understand. The comment in the sample code seems
> clear to me.
>
> // "Move" `a` into `b`
> // Here's what happens under the hood: the pointer `a` gets copied (*not*
> // the data on the heap, just its address) into `b`. Now both are pointers
> // to the *same* heap allocated data. But now, `b` *owns* the heap
> // allocated data; `b` is now in charge of freeing the memory in the heap.
> let b = a;
>
> Now you have a variable b that owns the data. a is not usable anymore.
>
> // After the previous move, `a` can no longer be used
> // Error! `a` can no longer access the data, because it no longer owns the
> // heap memory
> //println!("a contains: {}", a);
> // TODO ^ Try uncommenting this line
>
> As explained here, if a is used, this is an error.
>
> In the example a is move to b. If you had let b = &a, then b would borrow from
> a. The same way, a would not be usable anymore. The difference with move is
> that, when b is destroyed, in the first case memory is freed. in the second
> case, memory is not freed and a is "reenabled".
>
> It is either possible to borrow once and disable who you borrow from, or borrow
> multiple time and everything become immutable for the time you borrow.
>
> This makes the problems mentioned in this thread effectively impossible happen.

What if 'a' is a field of a struct in some non-trivial data structure? How is the compiler going to statically keep track of which field instances in this structure are not usable?
March 03, 2015
On Tuesday, 3 March 2015 at 05:40:59 UTC, Walter Bright wrote:
> On 3/2/2015 9:27 PM, deadalnix wrote:
>> On Tuesday, 3 March 2015 at 01:56:09 UTC, Walter Bright wrote:
>>> On 3/2/2015 4:40 PM, deadalnix wrote:
>>>> After moving resources, the previous owner can no longer be used.
>>>
>>> How does that work with the example presented by Marc?
>>
>> I'm not sure what you don't understand. The comment in the sample code seems
>> clear to me.
>>
>> // "Move" `a` into `b`
>> // Here's what happens under the hood: the pointer `a` gets copied (*not*
>> // the data on the heap, just its address) into `b`. Now both are pointers
>> // to the *same* heap allocated data. But now, `b` *owns* the heap
>> // allocated data; `b` is now in charge of freeing the memory in the heap.
>> let b = a;
>>
>> Now you have a variable b that owns the data. a is not usable anymore.
>>
>> // After the previous move, `a` can no longer be used
>> // Error! `a` can no longer access the data, because it no longer owns the
>> // heap memory
>> //println!("a contains: {}", a);
>> // TODO ^ Try uncommenting this line
>>
>> As explained here, if a is used, this is an error.
>>
>> In the example a is move to b. If you had let b = &a, then b would borrow from
>> a. The same way, a would not be usable anymore. The difference with move is
>> that, when b is destroyed, in the first case memory is freed. in the second
>> case, memory is not freed and a is "reenabled".
>>
>> It is either possible to borrow once and disable who you borrow from, or borrow
>> multiple time and everything become immutable for the time you borrow.
>>
>> This makes the problems mentioned in this thread effectively impossible happen.
>
> What if 'a' is a field of a struct in some non-trivial data structure? How is the compiler going to statically keep track of which field instances in this structure are not usable?

Borrowing 'a' from a struct would make the parent struct immutable during the borrow scope of 'a', I believe.
March 03, 2015
On Sunday, 1 March 2015 at 15:44:49 UTC, Marc Schütz wrote:
> Walter posted an example implementation of a reference counted array [1], that utilizes the features introduced in DIP25 [2]. Then, in the threads about reference counted objects, several people posted examples [3, 4] that broke the suggested optimization of eliding `opAddRef()`/`opRelease()` calls in certain situations.
>
> A weakness of the same kind affects DIP25, too. The core of the problem is borrowing (ref return as in DIP25), combined with manual (albeit hidden) memory management. An example to illustrate:
>


1 >     struct T {
2 >         void doSomething();
3 >     }
4 >     struct S {
5 >         RCArray!T array;
7 >     }
8 >     void main() {
9 >         auto s = S(RCArray!T([T()])); // s.array's refcount is
10> now 1
11>         foo(s, s.array[0]);           // pass by ref

        ++  s.array[0] = myt;             // Would also be invalid
12>     }
13>     void foo(ref S s, ref T T) {
14>         s.array = RCArray!T([]);      // drop the old s.array
15>         t.doSomething();              // oops, t is gone
16>     }



1. Assignment to RC types is not the same as assignments to non-RC types.
2. Allocation of RC types is more than just allocating of non-RC types.

Using the above example:

#9: The assignment and allocation does two things:
   a. Decrements the current RC of s.array(here it is null so no RC is used)
   b. Increments the ref count of the allocated RCArray by one.

#11: we pass both s and a referent to inside S. This is odd behavior and not technically required in this case(just pass S). Of course, we can't expect such behavior to creep up in complex code.

#14. In this case the behavior is correct. The assignment does the following:
   a. Decrements the current RC of s.array(here was 1, so now it is 0)
   b. Increments the current RC of the newly allocated RC array and assigns it to s.array.

#15. Since T refers to the old s.array, which now has a ref count of 0(which we can assume to then be unallocated), line 15 becomes a run-time error.

---

How to fix?

It seems that the only way to make it work well is to use sort of "lazy" ref counting.

Here references are created at the start of functions and decremented at the end of functions... rather that in place(which then causes problems with things that happen after it).

But this can't work as in the ++ I added. Here it would solve your problem but not fix the a generalization of it.

---

Is there a solution? Essentially no. Only at the end of the program could we possibly have the last function release all RC counted arrays. Since it is the end of the program it is the only time we can know that no more RC types will be used.

The problem is analogous to the multiple-inheritance problem.

---

Flow analysis can only help so far(it won't help with dynamically allocated types that are allocated depending on some "random" factor.

---

Note that since t is pointing to a part of S, technically we can't release s until t is released.

So, effectively, S has to be ref counted too! And it's ref count must check check T's ref count. If it is non-zero then the it can't release itself(or the reference to it's array).

Also, something eventually has to release S(or which is then s.array). When? Well, since t depends on S, it is when t is released. So we would naturally have to inform t that it should too release s when it is released.

----

So, if that sort of makes sense I'll explain the solution: It might be a bit fuzzy but I'll try to make things very clear. [I will also sometimes conflate the usage of concrete objects and their corresponding abstract types. e.g., "T with t". It should be clear context which is meant. e.g., If I say "T is released", I mean that the object with type T was released. When they need to be distinguished between I will do so(mainly in the code examples]


A *reference counted* type, from here on designated as RCT, is a type that only allows itself to be released to the heap when nothing depends on it. [Obviously if anything depends on it when after it is released will fail to be fulfilled]

If a RCT does not contains any references to any RCT's we will call it a free RCT or FRCT. Such types behave in a simple way. They can be free'ed completely immediately. Since FRCT's do not contain any references to other RCT's all their references are simple references that can be free'ed immediately.

We require that if a type contains a reference to a RCT then it too is a RCT. [the relation ContainsRCT(T1,T2) ==> ContainsRCT(T2,T1) ==> Both T1 and T2 are either RCT's or not RCT's] This isn't a big deal but without it we end up requiring all types to be RCT's. Here we allow some types to be not be RCT's.


In code,

class T { }   // a standard non-reference counted type.
              // It can't use any RCT's as references. It is your basic D type of class.

rcclass FRCT  // rcclass means reference counted class. It is identical to your basic
              // D type of class but has different keyword designator(which is why we call them free).
{
  int x;
  float[] y
  // etc, FRCT does not point to any RCT's
}

rcclass X { }  // Just another RCT

rcclass RCT
{
    FRCT myFRCT;
    int y;
    RCT foo(ref barRCT);
    X x;
    // etc Basically this is a class that can include other RCT's
}

----

Ultimately we could simply use the basic D class but we would have to designate every type as an RCT type... even the basic types. I'm trying to avoid some basic issues of that method that would derail the discussion. [But for those that want to know: one could simply treat all objects in d as rcclass types describe above, even basic types and such, but optimize out the reference counting part in in the compiler for those basic and FRCT types... this then would simply lead back to the method I'm using to describe it but simply some of the details were hidden in the "simple method" by the compiler]


So, once you have such a type system. The way to deal with the complex relationships that exist are to know them:

1. A RCT have the following relationships between it's containing types. We will denote T to be a non-RCT type, FRCT to be an FRCT, and RCT to be an RCT.

     RCT can contain T's, FRCT's, and/or, RCT's.

     In Code:

     rcclass RCT
     {
         T t;
         FRCT myFRCT;
         RCT myRCT;
     }


In this case, which is the only case we will talk about w.l.o.g), when an RCT is released it must attempt to release each of it's references... in example above: t, myFRCT, and myRCT. Since t is a simple type, it can be released immediately. No RCT's point to it because if they did, then T would be an RCT and it isn't. Similarly myFRCT can easily be released because it is just a complex of things like T's. In this case though there is something slightly different about it which is the referencing counting part. Essentially it's contained types are simple types but it still has a reference counting aspect for itself.


Basically T's and FRCT's can free themselves when they need to. This is because they know how to free each reference they contain and it can't cause referential problems simply because they do not contain such references. This by no means they won't cause other parts of code to access invalid references. A pointer to such a member of a class can easily try and access the member after the object has been freed. But this is not our concern because we are only dealing with RCT's. Our goal isn't to have everything be RCT's. If that was the case then the T and FRCT cases would not be needed since we won't use them. e.g., We might want the GC to handle non-RCT and some other method for FRCT's. This allows one to build on current D system. But one must remember that RCT's only guarantee valid referencing when they access other RCT's.


The last sentence above directs where we are going with this: Since we have RCT we can make sure they know how to communicate with each other. (we are creating a metatype here)

By having the proper communication system here it is possible for the types to know when they need to be released.
In fact, it is not hard and several methods can be used, even simultaneously.

Axiom: Every RCT has the ability to have any other RCT that contains a reference to it, to be able to release it. Also, vice versa!

[This requirement essentially allows the very final reference to know that it needs to release other dependencies it had and allows it to not only clean up itself but everything else it needs to clean up]

Once you have the above requirement of RCT's(The implement dealt with at a different time), you can lazily release objects in the background without issue(fragmentation of the heap is a harder matter but still has a solution because of the above requirement).

How do they communicate:

Your example will do:

    rcstruct T
    {
        void doSomething();
    }

    rcstruct S
    {
        T[] array;
    }

    void main()
    {
        auto s = S(new T());  // Here when the compiler creates the array of T's S knows T is a RFC.
                              // In this case it can get at the hidden communications layer of T and
                              // inform T that it is using it. Hence at this point
                              // Both S and T can communicate with each other! (not just blind inc/dec of an int
        auto t = s.array[0];

        foo(s, s.array[0]);   // nothing special except foo much create an added reference to the two argments
                              // This can be optimized out as long as foo returns once for every time it
                              // is called. In this case, At the start of foo, the counters will be inc'ed
                              // and at the end they will be dec'ed. Always a zero sum outside the function call.
        t.doSomething();      // Skip to foo, come back when done with foo! Great, t is still around!
                              // (RC is still zero at this point!)
    }

    void foo(ref S s, ref T t)
    {
        s.array = new T[];    // Here we are reassigning the array. No problem! We simply inc the ref count on the new array and decment it on the old. Just like normal ARC'ing! The only different is that t is not immediately release! Even though it has a RC count of 0. Think of it as lazy ARC'ing... or LARC'ing! ;)
        t.doSomething();      // Great, even though t has an RC count of zero, it hasn't been released yet!
    }



So, what is this LARCING?

Essentially instead of s simply dumping t in foo, it informs t that it is no longer using it. This sets the RC = 0 !!BUT!! t does not free itself immediately! If it does, it leads to the problem you have given and also to the extended problem I have shown above(calling t.doSomething in main).


If t sticks around though, we are not releasing memory and hence what is the point?


Well, as usual this is an engineering problem. We have a few cases.


1. The compiler can prove that a RCT goes out of scope and is not used any more. In this case t can release itself at the end of the function where it is provable that it is no longer used. (In your example it is at the end of foo ad in my example it is at the end of main) Flow analysis, simple brute force checking, etc are used here. Most cases in program will fall under this category. Local variables that are not referenced to by global variables are proven to be releasable at the end of the function call. Hence, many cases will work as simple ARC'ing and not require LARC'ing.


2. The compiler cannot prove when a variable is release. This is when the type itself has to decide when it is not being used. (this the last part of the problem and your example falls under it)


In this case, the communication between S and T is crucial. When s decides it no longer need t, it informs t. t has a list of every reference that is using it(this can just be a pointer to the ref
count of the RCT). Having this list, while potentially weighty, lets t know when all the references to it are not needed any more(sort of chains or links between any other RCT and t have been broken).


Once this is done it is a simple matter of t trying to release itself every time it goes out of scope.

     a. At some point t will release itself
     b. t will never be able to release itself(this is generally due to program error in which a reference was made to t without removing the reference when it was done). Note this is why if every type is an RCT, prevents dangling(e.g., we would have full LARC'ing... or FLARC'ing.). FLARC'ing may not be optimal though(all pointers would have to be RCT which would probably be quiet inefficient. Also interdependencies are handled easily:


         s.t = t;
		 t.s = s;
         while (!isFree(t) && !isFree(s))
         {
		     s.t = null;
			 t.s = null;
             if (t.refCount == 0)
                s.release();
             if (s.refCount == 0)
                t.release();

         }

         The reason that t and s are released in LARC'ing is because at the 2nd time through the loop s.release() knows that t no longer depend and can release itself immediately(because t.s = null set s's *t entry* to 0 on it so now s knows that t doesn't depend on it). This is just a prototype that can be used to show that any level of nesting between types will eventually resolve all releases properly.
		
     c. Alternatively, a GC like memory manager can scan through the types and release all types who's reference count = 0 and who's dependencies are all invalid(nothing depends on it). It can do this in the background without issue and can compact consecutively release objects without issue. i.e., the GC knows what part of the heap belonged to objects there were ref counted and if it is proven they have any references then it can mark them as free'ed memory. Consecutive free'ed objects in memory can be combined since they never will be used again. This can be done behind in parallel and is sort of minor defragmentation. In fact, analogous to defragmenting a drive. If you know a file is not being used then you can defragment it in the background and move the various parts around without issue because you know it is not being used. Once it is being uses, you run into issues with having to stop the threads that are using that part of the heap. (I think this limitation could ultimately be removed for most practical cases.)
	
	 In this case the GC would have to have a list of all objects of RCT's. Essentially the GC is given to the task of LARC'ing in the background.
	

	
Finally, which such an structural approach(Essentially implementing the reference list, which is a random access list containing all the references(pointers to all classes that use the type))  to ref counting becomes pretty stable and in probably 99%+ of program usage, statically proven to release memory(step a above, The compiler inserts many static releases that are released immediately and many types will be simple types that don't need LARC'ing).


The two issues with this, as always, are speed and memory usage. I think it should rather fast(most releases are direct and just release themselves since they don't have references. On top of that the remaining are mostly FRCT's which can be released nearly immediately(they can, but don't until they are informed it is ok).


For the full blown RCT types, the way they release things is in a hierarchical manner which can create avalanches of changes. If many objects are waiting for one object to be released, then when it does, they all could be released immediately. But this chain reaction is simply lazily recursive and eventually terminates.


The only real problem is how to deal with un-managed references to RCT types. These are references(say pointers) to RCT's that may not be "smart"(inform the RFC that it is no longer being refered to. A simple pointer to a class that doesn't do any communication is the prototypical problem.


The only solution seems to be to just accept it. It is a necessary evil. Allow it but require the program to know the intricate details. Such pointers are needed for speed and don't inherently cause any problems. It's only when they are abused can issues creep in.


If one, say, had RCT pointers that are used for some types of problems and simple pointers when they are needed, it becomes pretty marginal of an issue. (if you are doing driver programming and have to have speed and direct access then use the simple pointers but it is your just to know if your references are working as expected.


In many cases, RCT pointers can be used in many of the cases which make further minimize the singular problem case(that of non-informative referencing). (We could call such types "Peeping Toms"... but as long as they only peek it is ok(not by law, in programming ;)).


At this, imagine that such a system was implemented. It would provide almost complete automatic memory management coverage with releasing resources as they are generally released in the program flow. In fact, with a GC(don't you just hate it when your garbage man doesn't pick up your trash sometimes? specially if he never picks it up) that essentially works parallel, most of the actual releasing can be done in the background without ever stopping the program. The benefits are: Better real-time behavior, Some programs might never need to compact the heap(in fact, if intelligent heap allocation is used, probably most programs could benefit from it), and debugging is much easier(Essentially more like manual management for most objects... just the compiler handles for us, but we know when, most likely, an object goes out of scope it will be released). Seg faults due to invalid memory access will almost be reduced to 0(since the corner case... the problem case of simple references is not used as much as all the other ways to use objects).


The downside? There really isn't one except memory usage! One can come up with special cases that simply don't work well under LARC'ing but because it allows one to use non-LARC'ing types, the programmer will just have to manage it on is own or a mixed combination.


Another great benefit is performance. One may think with all the all the extra allocation and deallocation code that the end's of functions would be slowed down(do to objects being lazily released). This is not necessarily the case because it is LARC'ing, the releases can be done later... say outside a performant piece of the code. It can even be faster than directly releasing it because that could be slow in and of itself, which, again, with LARC'ing, we can chose a better time to release the object or do it in the background.


Unfortunately, if I'm not mistaken, with FLARC'ing, all classes would essentially double in size. This is because if every reference the type has is a RCT then it would essentially need to store a pointer to the object as to provide a link to it. With limited LARC'ing I think one could probably achieve a good balance. (for large classes and/or a large number of objects, one could just avoid LARC'ing to prevent the overhead and doing it manually or with GC)

Some codetalk(tm) of how the interaction goes:

     Object s is being initialized. It is a RFC type. It has, as a member, a object x that is also a RFC type.  In the course of s being initialized, it initializes x. We'll assume it is not null.
	 To do this s creates the object for x then informs x that it has a reference to it(we could have multiple types of references for further optimization if we want). e.g.,
	
	     x = new X;
	     x.CreateReference(s);
		 s.CreateReferenec(x); // We use directionless binding references
		
		
    Now, suppose at some point s is being released. At the end of scope the compiler inserts
	
	     s.Release();
		
    S now tries to release itself. It checks it's ref count and the list of all references it has.
	
	     e.g.,
		 refCount = 0;
		 refList = {}
		
		 then in s.Release()
		
			if (s.refCount == 0 && s.refList.IsEmpty)
			{
				GC.RemoveRCT(s); // GC.RemoveRCT is not necessary needed but using it hear for expressive purposes.
				s.Free();
			}
			else
				GC.AddRCT(s);   // Here, since the compiler potentially isn't going insert another s.Release() outside the function we have to let the GC handle it
			
		 in this case s free's itself immediately.
		
		 If either refCount != 0 or refList != empty, the GC is given control(it is made aware that S needs to be periodically checked to see if it can be released.(the GC simply does the
		 if (s.refCount == 0 && s.refList.IsEmpty) s.Free(); in a parallel thread. If s.refCount == 0 and s.refList is empty at some point then the GC ends up free'ing s.
		
		 Also, the compiler may be able to insert another s.Release() in the outer scope of the function all if it releases that it s was probably not released before. This doesn't hurt anything except an extra check that might be wasted(profiling could remove this check or even insert it).
		
		
		 The only way s can't be released is if it's refCount and refList are not empty. This is "danling" references and can only happen when mixing non-RCT's with RCT's.
		
		 So we know how s should release it self but how does t know that s no longer needs it? (so t can release itself at some point)
		
		 well, this has to do when x is assigned to. e.g.,
		
		 s.x = new X;
		 // Hidden, we have s update it's ref list and inform both the hold x and the new x what is going on.
		
		 here the assignment will need to remove the old x from refList and add the new x. This keeps the the list up to date. (there is a big corner case here that I believe would never occur but will have to think about it more)
		
		
		
Any ways, I'll leave it at that for now!
		
		
		
	




	







1 2 3 4 5 6 7 8 9 10 11 12 13