Skip to content
This repository has been archived by the owner on Oct 12, 2022. It is now read-only.

assumeLocal: convert shared lvalue to a non-shared one #724

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/core/atomic.d
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,73 @@ version( CoreDdoc )
* loads and stores after the call.
*/
void atomicFence() nothrow @nogc;

/**
* Converts a shared lvalue to a non-shared lvalue.
*
* This functions allows to treat a shared lvalue as if it was thread-local.
* It is useful to avoid overhead of atomic operations when access to shared data
* is known to be within one thread (i.e. always under a lock).
* ---
* shared static int i;
*
* // i is never used outside of synchronized {} blocks...
*
* synchronized
* {
* ++i; // ERROR: cannot directly modify shared lvalue
*
* atomicOp!"+="(i, 1); // possible overhead
*
* // Directly modify i
* assumeUnshared(i) += 1;
* // or:
* ++assumeUnshared(i);
* // or:
* i.assumeUnshared += 1;
* }
* ---
* Usage of this function is restricted to allowing limited lvalue access to shared instances of
* primitive and POD types (e.g. direct use of operators), thus it is not defined for classes.
*
* Note: this function does not perform any ordering.
*
* Note: assumeUnshared is a special-purpose primitive and should be used with care. When accessing
* shared variables both inside and outside of synchronized blocks, atomic operations should be
* used instead.
*
* Params:
* val = the shared lvalue.
*
* Returns:
* The non-shared lvalue.
*/
ref T assumeUnshared(T)(ref shared T val) @system @nogc pure nothrow
if(!is(T == class) && !is(T == interface))
{
return *cast(T*) &val;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the ddoc version, right? Then it should not have an implementation.

}

/**
* Converts a shared lvalue to a non-shared lvalue.
*
* This functions allows to treat a shared lvalue as if it was thread-local.
* It is useful to avoid overhead of atomic operations when access to shared data
* is known to be within one thread (i.e. always under a lock).
*
* This is a specialization for immutable lvalues. Since immutable is implicitly
* unshared, this version accepts any type (even classes and interfaces).
*
* Params:
* val = the shared lvalue.
*
* Returns:
* The non-shared lvalue.
*/
ref immutable(T) assumeUnshared(T)(ref immutable(T) val) @safe @nogc pure nothrow
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be a use case for this? It doesn't change the type of its argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic code.

shared struct Example(T)
{
    private T payload;

    T getUnsharedCopyOfPayload() const
    {
        return payload.assumeUnshared;
    }
}

auto createExample(T)(auto ref T value)
{
    return shared(Example!T)(value);
}

immutable int i = 42;
auto example = createExample(i);
assert(is(typeof(example.getUnsharedCopyOfPayload()) == immutable(int));

This way, Example doesn't have to specialize such use case.

Copy link
Contributor Author

@radcapricorn radcapricorn Apr 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOTW, it's safe to assume immutable as unshared. But the signature ref T assumeUnshared(T)(ref shared T val) will not accept immutable lvalue, hence the specialization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

immutable is actually implicitly shared

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, and assumeUnshared should accept shared arguments, therefore it shouldn't fail to compile with immutable argument. It works as advertised: takes shared lvalue and yields lvalue that can be treated as unshared. It just so happens that immutable is both at the same time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andralex @radcapricorn anything else still not clear?

Copy link
Member

@andralex andralex Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still unconvinced about the usefulness. By that logic we should also define assumeUnshared for regular data so assumeUnshared works transparently with all data types (after all unshared data can be assumed to be unshared!)

One possibility is to not introduce this overload yet, and see how necessary it is in practice.

At any rate, if we do introduce the overload the documentation must be merged with the other overload's.

{
return val;
}
}
else version( AsmX86_32 )
{
Expand Down Expand Up @@ -1290,6 +1357,19 @@ if(__traits(isFloating, T))
}
}

// assumeUnshared is architecture-independent: it is just a cast
ref auto assumeUnshared(T)(ref shared T val) @system @nogc pure nothrow
if(!is(T == class) && !is(T == interface))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obey phobos, if flush to the left and space after it

{
return *cast(T*) &val;
}

// immutable is implicitly unshared
ref auto assumeUnshared(T)(ref immutable(T) val) @safe @nogc pure nothrow
{
return val;
}

////////////////////////////////////////////////////////////////////////////////
// Unit Tests
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1545,4 +1625,81 @@ version( unittest )
atomicOp!"-="( c, d );
assert(c == 1);
}

pure nothrow @nogc @system unittest
{
int base = 0;
shared int atom = 0;

// only accept shared lvalues
static assert(!is(typeof(assumeUnshared(base))));
static assert(!is(typeof(assumeUnshared(cast(shared)base))));

++assumeUnshared(atom);
assert(atomicLoad!(MemoryOrder.raw)(atom) == 1);
}

pure nothrow @nogc @system unittest
{
shared const int catom = 0;
shared immutable int iatom = 0;
// allow const
static assert(is(typeof(assumeUnshared(catom))));
static assert(is(typeof(assumeUnshared(iatom))));
// preserve const
static assert(!is(typeof(++assumeUnshared(catom))));
static assert(!is(typeof(++assumeUnshared(iatom))));
}

pure nothrow @nogc @system unittest
{
class Klass {}

Klass c1;
shared Klass c2;

// don't accept class instances
static assert(!is(typeof(assumeUnshared(c1))));
static assert(!is(typeof(assumeUnshared(c2))));
}

pure nothrow @nogc @system unittest
{
interface Interface {}
Interface i1;
shared Interface i2;

// don't accept interfaces
static assert(!is(typeof(assumeUnshared(i1))));
static assert(!is(typeof(assumeUnshared(i2))));
}

pure nothrow @nogc @system unittest
{
// test assumeShared with inout
shared struct S
{
int atom = 0;

@property ref get() inout
{
return atom.assumeUnshared;
}
}

shared S sm;
shared const S sc;
shared immutable S si;

static assert(is(typeof(sm.get) == int));
static assert(is(typeof(sc.get) == const(int)));
static assert(is(typeof(si.get) == immutable(int)));

static assert( is(typeof(++sm.get)));
static assert(!is(typeof(++sc.get)));
static assert(!is(typeof(++si.get)));

sm.get += 10;
assert(atomicLoad!(MemoryOrder.raw)(sm.atom) == 10);
}
}