Skip to content

Instantly share code, notes, and snippets.

@jnthn
Created April 25, 2018 15:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnthn/e51a06c6882fbc6219e0fa0a3dd373e6 to your computer and use it in GitHub Desktop.
Save jnthn/e51a06c6882fbc6219e0fa0a3dd373e6 to your computer and use it in GitHub Desktop.

Design notes

Problems with Scalar assignment

  1. Can't optimize out the type check, which would let us get the Scalar assignment down to potentially just a pointer write
  2. Can't optimize out the auto-viv check either (would be nice if we could do that as part of the type check)
  3. Can't optimize invocation of the auto-viv (for example, inlining it) if it tends to happen
  4. Auto-vivification closure is too expensive to take
  5. Auto-vivification costs us an extra pointer on every Scalar container, even if we never actually need it; on a large array that's an extra 8 bytes per element

Revisiting the descriptor

The $!descriptor today points to a ContainerDescriptor object that stores a bit of meta-data used by the assignment process. It's a sizable bit of code that's quite opaque to spesh, JIT, etc.

This could be replaced by an object with a method that is used in order to perform the assignment. When we want to do an array or hash auto-viv, we'd install an object there that references the hash and the key, as well as the descriptor to use post-auto-viv. On the initial assignment, it would then replace the descriptor with the nested one. This would let us get rid of the 8-byte storage that we need today for $!whence.

Spesh as it stands today would be able to guard the type of the desciptor, but that's not really enough. In fact, this is just one of a bunch of places where we'd really like a more "programmable spesh".

Other problems to consider

We'd gain quite a lot if we were able to teach spesh more about the higher level semantics of the language. In fact, in doing so we'd find that we can perhaps simplify spesh itself over time.

Some other places we'd like to teach spesh to do better are:

  • Private method calls in roles. It'd be really nice to be able to guard on invocant type (which will often already be proven anyway) and then hold the resolved result constant.
  • self.Foo::bar() calls, where we again can resolve once and hold constant
  • self.?foo calls
  • callsame and friends. Again, if we can specialize on the invocant type, then for a simple parent method delegation we should be able to treat the method that is resolved constant and call it in various cases.
  • Lookups by type variable could also potentially be handled better and more generally by a guard on self, eliminating the current less-general means to do that in spesh today.
  • Various of the extops we'd like to get rid of could be replaced and better optimized by this mechanism, including p6recont_ro and p6decontrv.

API

MoarVM would provide some new ops to form the pluggable spesh API. The basic idea is that one registers a handler to be invoked, and within that handler set up guards. The code to execute if the guards are met will then be returned. Beyond that point, an invocation will first check the guards and see if they are satisfied. If so, it will evaluate to the result that was previously returned when those guards matched. If not, it will run again and add to the guard tree.

When the code is specialized, the tree at this point will be analyzed for its content. The guard tree will be spat out as a sequence of lookup ops, which will be subject to further specialization, along with guards, which will be subject to elimination if an earlier guard already proved that property.

It is only allowed to guard:

  • The incoming arguments
  • A value resulting from another guarding op that produces a result

Thus it's possible to dig in to data structures and guard things inside of them.

nqp::speshreg(str $lang, str $name, obj $handler)

Registers a language specialization function for $lang named $name. The $handler should be something invokable.

nqp::speshresolve(str $name --> obj)

This is a form of invoke, and thus arguments are provided to it using the standard arg ops. On the first call, looks up the current language's spesh handler registered under $name and invokes it with the arguments. It is, for now, only allowed to use positional arguments, and only the object ones may be guarded against.

nqp::speshguardtype(obj $value, obj $type)

Adds a guard that $value.WHAT must be precisely $type.

`nqp::speshguardconcrete(obj $value)

Adds a guard that $value must be concrete.

`nqp::speshguardtypeobj(obj $value)

Adds a guard that $value must be a type object.

nqp::speshguardobj(obj $value)

Adds a guard that $value must be precisely the specified object.

nqp::speshguardgetattr(obj $value, obj $type, str $name --> obj)

Gets the attribute specified, and makes the result of the operation something we can further guard against.

Examples

These are all written in NQP.

Private methods

We will always pass an already decont'd self to the handler. Thus the handler would be:

nqp::speshreg('perl6', 'privmeth', -> $obj, str $name {
    nqp::speshguardtype($obj, $obj.WHAT);
    $obj.HOW.find_private_method($obj, $name)
});

And we would code-gen a call self!foo($bar) as:

nqp::speshresolve(nqp::decont(self), 'foo')(self, $bar)

Scalar assignments

We'll start to code-gen these as:

my $tmp := nqp::decont($the-value);
nqp::speshresolve($the-scalar-target, $tmp)($the-scalar-target, $tmp)

Assuming a descriptor like:

class StandardScalarDescriptor {
    has $!type;
}
class ArrayVivScalarDescriptor {
    has $!target;
    has int $idx;
    has $!next-descriptor;
}

We register this specialization:

sub unchecked-simple-assign($scalar, $value) {
    nqp::bindattr($scalar, Scalar, '$!value', nqp::decont($value))
}
sub make-checked-simple-assign($type) {
    return -> $scalar, $value {
        nqp::istype($value, $type)
            ?? nqp::bindattr($scalar, Scalar, '$!value', nqp::decont($value))
            !! X::TypeCheck::Assignment.new(...).throw
    }
}
nqp::speshreg('perl6', 'assign', -> $scalar, $value {
    if nqp::istyperaw($scalar, Scalar) && nqp::isconcreteraw($scalar) {
        nqp::speshguardtype($scalar, nqp::whatraw($scalar));
        nqp::speshguardconcrete($scalar);
        my $desc := nqp::speshguardgetattr($scalar, Scalar, '$!descriptor');
        if nqp::istype($desc, StandardScalarAssigner) {
            nqp::speshguardobj($desc);
            my $type := nqp::getattr($desc, StandardScalarDescriptor, '$!type');
            if $type.HOW.archetypes.nominal && nqp::istype($value, $type) {
                nqp::speshguardtype($value);
                return &unchecked-simple-assign;
            }
            else {
                return make-checked-simple-assign($type);
            }
        }
        elsif nqp::istype($desc, ArrayVivScalarDescriptor) {
            # ...
        }
    }
    else {
        die "Cannot assign to a non-assignable thingy";
    }
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment