gc
gc copied to clipboard
Should there be an "escape hatch" for vtable structural subtyping?
The "median" target for this proposal appears to be Java-like languages (though others can certainly be represented) which will heavily make use of vtable calls. In that light, it seems odd that we're willing to put up with adding code like:
(block $fail (result (ref $D)) (set_local $this (cast_down (ref $Cthis) (ref $D) $fail (get_local $Cthis))))
to most methods, which is both a speed and code bloat issue.
I'm probably missing something, but maybe someone can clarify why we shouldn't special-case this super common use case.
I'd imagine we can have an escape hatch to structural subtyping that goes something like this:
- Mark certain structs as being a vtable.
- When checking structural subtyping between types A and B, allow B to be a subtype of A even if it has a vtable member whose function types have a B where an A is expected.
- Disallow regular load/store on vtable structs, instead provide a new call operation on the parent of the vtable that enforces that parent as first argument.
There is a wealth of options for enriching the type system in a principled manner to avoid casts in certain situations. They all tend to conflict with simplicity, of course. We will need implementation experience to see what is bearable and what isn't -- such casts can be much cheaper than you'd expect and may already be dominated by the cost of a call. That said, a special type system hack for the benefit of Java-like languages -- and language bias in general -- is something we'd rather want to avoid.
I think we should make virtual calls in Kotlin-like languages efficient at runtime and in terms if code size.
We would get a lot of value having a simple type-check rule and an instruction call_vtable
that can be easily desugared into existing struct.get
s and ref_call
.
Proposed shortcut can be completely ignored without any cost by languages that don't need this feature.
My main concern with current proposal is the size of generated code, a quality we absolutely want to have when targeting web clients.
Inside a function that overrides virtual method we would need 4 extra instructions and extra local
:
(local $new-this ...) ;; if 'this' is used more than once
(set_local $new-this
(ref.cast ... ...
(get_local $old-this)
(get_global $rtt)))
If we would like to keep zero-overhead direct calls we'd put this cast into a bridge method. It would require an extra func
and extra call
. This would make virtual calls even slower.
Virtual methods become bloated, but call sites look even worse. In order to translate a virtual call:
(<object-instance-expression>).foo(<arguments...>)
in some cases we would need 5 extra instructions and a temp local
(local $tmp)
...
(set_local $tmp ;; Store value in temp local
(<object-instance-expression>)) ;; if expression is big or has side-effects
(call_ref
(get_local $tmp)
(<arguments...>)
(struct.get $vt <foo-slot>
(struct.get $class 0
(get_local $tmp)))
compared to proposed call_vtable
:
(call_vtable $class <foo-slot>
(<object-instance-expression>)
(<arguments...>))
We have discussed improved method dispatch as a potential high-priority post-MVP feature, but we won't be adding it to the MVP, so closing this.