- Can't optimize out the type check, which would let us get the
Scalar
assignment down to potentially just a pointer write - Can't optimize out the auto-viv check either (would be nice if we could do that as part of the type check)
- Can't optimize invocation of the auto-viv (for example, inlining it) if it tends to happen
- Auto-vivification closure is too expensive to take
- 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
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".
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 constantself.?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
andp6decontrv
.
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.
Registers a language specialization function for $lang
named $name
. The
$handler
should be something invokable.
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.
Adds a guard that $value.WHAT
must be precisely $type
.
Adds a guard that $value
must be concrete.
Adds a guard that $value
must be a type object.
Adds a guard that $value
must be precisely the specified object.
Gets the attribute specified, and makes the result of the operation something we can further guard against.
These are all written in NQP.
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)
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";
}
});