[dmd-concurrency] tail-shared by default?

Sean Kelly sean at invisibleduck.org
Sat Jan 9 11:59:53 PST 2010


On Jan 9, 2010, at 10:56 AM, Michel Fortin wrote:

> Le 2010-01-09 à 13:26, Sean Kelly a écrit :
> 
>> I think we should, though one issue needs to be discussed related to this.  If we allow the compiler to optimize away use of atomic() when it can determine that atomic behavior is unnecessary then the exact same code may compile with one compiler and not another (if the second doesn't optimize in exactly the same manner). This is a big enough issue that I'm almost inclined to say that atomic() isn't necessary, though I would hate to do so.  Are there any other options?
> 
> I'm not sure I follow. If we implement atomic() as a function returning a temporary struct, we just need to overload atomic() so that it doesn't insert barriers when the passed variable isn't shared (because all operations on non-shared variables are atomic anyway).
> 
> Or were you thinking of more subtle optimizations like using flow control to detect that a variable isn't shared *yet*? I don't think those optimizations can get very far.

I was mostly thinking about tail-shared.  The problem with no tail-shared for class references mostly disappears if you can let the compiler detect when a reference itself is not shared and eliminate the requirement for atomic behavior for the reference.  However, it should not be possible for two different compiler implementations to disagree about whether a program is valid, so any optimizations have to be invisible to the user (the "as if" rule).  So I think there are two options.  First, consider the following code:

    class A
    {
        this( shared B b_in ) { b = b_in; }

        void callB() shared {
            b.doSomething(); // 1
        }

        private shared B b;
    }

    a = new A( b );
    a.callB();

Here, even though 'b' is declared shared, the only thing that needs to be shared is the class instance because the reference itself never changes.  ie. what we really want here is tail-shared, and not having it would mean a completely unnecessary atomic op at point 1, making the behavior:

    void callB shared {
        atomic(b).doSomething();
    }

This is sufficiently expensive that we really must leave the door open for "as if" elimination of the atomic op, so I think we have 2 options:

1. Not require atomic() for reads and writes of shared variables.  All other fancier stuff would still need an atomic() wrapper, and the compiler could be totally ignorant of this because such a wrapper would accept a "ref shared X" and then disappear into ASM code, thus there's no binding of compiler behavior and a specific library design.

2. Require atomic() for everything, make the compiler aware of the atomic module and allow it to rewrite the call.  So if we define:

    enum msync {
        full,
        none
    }

    Atomic!(T, s) atomic(msync s = msync.full, T)( ref T val ) { ... }
    struct Atomic(T, msync s = msync.full) { ... }

   Then the compiler could rewrite "atomic(x)" as "atomic!(msync.none)(x)" when it can detect that synchronization is unnecessary.  Like option 1, this allows the same code to work across all compiler implementations, but has the downside of requiring the compiler to know about the atomic module so the rewrite can occur.


More information about the dmd-concurrency mailing list