Escaping the Tyranny of the GC: std.rcstring, first blood

Dmitry Olshansky via Digitalmars-d digitalmars-d at puremagic.com
Sat Sep 27 04:54:19 PDT 2014


27-Sep-2014 14:23, "Marc Schütz" <schuetzm at gmx.net>" пишет:
> On Saturday, 27 September 2014 at 09:38:35 UTC, Dmitry Olshansky wrote:

>> The good thing w.r.t. to memory about structs - they are themselves
>> already allocated "somewhere", and it's only ref-counted payload that
>> is allocated and destroyed in a user-defined way.
>>
>> And now for the killer reasons to go for struct is the following:
>>
>> Compiler _already_ does all of life-time management and had numerous
>> bug fixes to make sure it does the right thing. In contrast there is
>> nothing for classes that tracks their lifetimes to call proper hooks.
>
> This cannot be stressed enough.
>
>>
>> Let's REUSE that mechanism we have with structs and go as lightly as
>> possible on  untested LOCs budget.
>>
>> Full outline, of generic to the max, dirt-cheap implementation with a
>> bit of lowering:
>>
>> ARC or anything close to it, is implemented as follows:
>> 1. Any struct that have @ARC attached, must have the following methods:
>>     void opInc();
>>     bool opDec(); // true - time to destroy
>> It also MUST NOT have postblit, and MUST have destructor.
>>
>> 2. Compiler takes user-defined destructor and creates proper
>> destructor, as equivalent of this:
>>     if(opDec()){
>>         user__defined_dtor;
>>     }
>> 3. postblit is defined as opInc().
>>
>> 4. any ctor has opInc() appended to its body.
>>
>> Everything else is taken care of by the very nature of the structs.
>
> AFAICS we don't gain anything from this, because it just automates
> certain things that can already be done manually in a suitably
> implemented wrapper struct. I don't think automation is necessary here,
> because realistically, how many RC wrappers will there be? Ideally just
> one, in Phobos.

You must be missing something big. Ref-counting ain't singular thing, 
it's a strategy with a multitude of implementations, see my other post.


>> Now this is enough to make ref-counted stuff a bit simpler to write
>> but not much beyond. So here the next "consequences" that we can then
>> implement:
>>
>> 4. Compiler is expected to assume anywhere in fully inlined code, that
>> opInc()/opDec() pairs are no-op. It should do so even in debug mode
>> (though there is less opportunity to do so without inlining). Consider
>> it an NRVO of the new age, required optimization.
>>
>> 5. If we extend opInc/opDec to take an argument, the compiler may go
>> further and batch up multiple opInc-s and opDec-s, as long as it's
>> safe to do so (e.g. there could be exceptions thrown!):
>>
>> Consider:
>>
>> auto a = File("some-file.txt");
>> //pass to some structs for future use
>> B b = B(a);
>> C c = C(a);
>> a = File("other file");
>>
>> May be (this is overly simplified!):
>>
>> File a = void, b = void, c = void;
>> a = File.user_ctor("some-file.txt")'
>> a.opInc(2);
>> b = B(a);
>> c = C(a);
>> a = File.user_ctor("other file");
>> a.opInc();
>
> I believe we can achieve the same efficiency without ARC with the help
> of borrowing and multiple alias this.

Problem is - there is no borrowing yet in the compiler, or maybe you 
mean something more simple.

> Consider the cases where inc/dec
> can be elided:
>
>     RC!int a;
>     // ...
>     foo(a);
>     // ...
>     bar(a);
>     // ...
>
> Under the assumption that foo() and bar() don't want to keep a copy of
> their arguments, this is a classical use case for borrowing. No inc/dec
> is necessary, and none will happen, if RC!int has an alias-this-ed
> method returning a scoped reference to its payload.

Interesting. However scope must work first, also passing an RC!int by 
borrowing is this:

void func(scope(A) a)

or what? how does it transform scope? (Sorry I haven't followed your 
proposals for scope)


> On the other hand, foo() and bar() could want to make copies of the
> refcounted variable. In this case, we still wouldn't need an inc/dec,
> but we need a way to express that. The solution is another alias-this-ed
> method that returns a (scoped) BorrowedRC!int, which does not inc/dec on
> construction/destruction, but does so on copying. (It's probably
> possible to reuse RC!int for this, a separate type is likely not
> necessary.)

Who would make sure original RC still exists?

> The other opportunity is on moving:
>
>      void foo() {
>          RC!int a;
>          // ....
>          bar(a);    // last statement in foo()
>      }

We should already have it with structs by their nature.

>
> Here, clearly `a` isn't used after the tail call. Instead of copy &
> destroy, the compiler can resort to a move (bare bitcopy). In contrast
> to C++, this is allowed in D.
>
> This covers most opportunities for elision of the ref counting. It only
> leaves a few corner cases (e.g. `a` no longer used after non-tail calls,
> accumulated inc/dec as in your example). I don't think these are worth
> complicating the compiler with ARC.

I don't mind having working scope and borrowing but my proposal doesn't 
require them.

-- 
Dmitry Olshansky


More information about the Digitalmars-d mailing list