Closed rossberg closed 2 years ago
With that split combined with the ability to cast directly to any type in a single instruction, we'd end up with
ref.is_base <baseheaptype> : [anyref] -> [i32]
ref.is_concrete <typeidx> : [anyref] -> [i32]
And in the future:
ref.is_rtt <typeidx> : [anyref, rtt] -> [i32]
I'm a bit confused about what exactly the split is supposed to express. The discussions point to (at least) two definitions of the split:
While these distinctions might be equivalent in at least some of the discussed RTT models, putting the distinction into the instruction set like this locks this correspondence in place. If the intent is for the split to be (and remain) between RTT and non-RTT types, then we are prevented from ever changing the expressibility of RTTs in a way that breaks this correspondence.
If the intent is actually to distinguish between deftypes and other heap types, independently of what we decide to do with RTTs, then I have no big issues with that. It's completely redundant, but I don't see it causing any trouble apart from wasting opcode space. Depending on what we do with RTTs, at worst we will end up with a somewhat inconsistent instruction set.
We discussed this in the subgroup meeting today. The key takeaways are that:
No one in attendance had a problem with having just a single instruction to cast from any
to any of its subtypes, even if in the future we introduce new rtt-using casts that can only target a subset of any
's subtypes.
No one in attendance felt strongly in favor of adding an explicit nullable cast instruction, and in particular we did not think that engines could produce better code if the null check were part of the cast.
I talked with @kripken after the meeting about the option of adding a separate nullable cast instruction and he pointed out that the only downside would be spending an opcode, which is much less bad than in the last code size discussion in which the downside was potentially restricting future type system extensions. On the other hand, we also estimated the code size benefit of having a nullable cast instruction to be less than 1%. (Also, I was wrong every time I mentioned avoiding a combinatorial explosion in opcodes, since we're only thinking of adding a single ref.as_or_null
, not a new variant of every cast-related instruction.)
After thinking about it some more, I lean toward keeping null-polymorphic casts because they preserve type information better than a separate nullable cast instruction would. A separate nullable cast instruction would "forget" that its non-nullable input is non-nullable. This could be fixed by optimizing to use the non-nullable cast instruction instead, but this optimization wouldn't be necessary if there were just a single null-polymorphic cast.
With all that in mind, what do folks think of this concrete proposal? It is identical to @askeksa-google's proposal in https://github.com/WebAssembly/gc/issues/274#issuecomment-1079108782 except that this new proposal does not include ref.as_or_null
. I've also included the detailed description of validation against labels from the current MVP.md.
ref.is ht: [anyref] -> [i32]
ref.as ht: [(ref null? any)] -> [(ref null? ht)]
br_on l ht: [t0* t] -> [t0* t]
where t <: anyref, C.labels[l] = [t0* t'], (ref ht) <: t'
br_on_not l ht: [t0* t] -> [t0* (ref ht)]
where t <: anyref, C.labels[l] = [t0* t'], t <: t'
Each of these instructions should also work with func
instead of any
just like the current instructions.
@tlively IMO these instructions should require ht <: any
so that we have a little more leeway to fiddle with the type hierarchy in future (e.g. if we end up deciding to make func
a subtype of any
later, we don't want have to think about possibly-existing valid programs containing ref.as func
).
EDIT: in the long run, if we add meaningful subtypes to func
and extern
in separate hierarchies, we may want separate casting instructions for each hierarchy. If we used (e.g.) ref.as
for all three (with input type suitably generalised), baseline compilers would need to propagate information from validation to codegen in order to avoid being forced to generate a naive if func then trap else if extern then trap else do real casting logic
for every ref.as <some struct type>
(especially if there's no uniform representation, which IIRC was the motivation for the separate hierarchies).
func
already has all of the typed function references as nontrivial cast targets! We definitely need a way to downcast to them from func
. I don't believe having the single set of casting instructions be any
/func
polymorphic would prevent us from making func <: any
in the future, but I could be missing something. ref.as func
would not change behavior in existing programs but would validate in more places than before.
Good point about complicating baseline codegen, though. I wouldn't be opposed to having different versions of the instructions for the different hierarchies. If we did that and in the future wanted to make func <: any
, then I suppose we could make the func
and any
(and/or extern
) versions of the instructions aliases for one another.
ref.as func
would not change behavior in existing programs but would validate in more places than before.
I guess this is presuming a validation rule that looks something like the following?
(edit: since the rule above just has any
as the input type, ref.as func : any -> func
would validate with or without func <: any
)
ht <: t
---------
ref.as ht : [ref null? t] -> [ref null? ht]
That would address the "future func <: any
" concern. For clarity, it's equivalent to the following:
(t = any && ht <: any) || (t = func && ht <: func) || ...
---------
ref.as ht : [ref null? t] -> [ref null? ht]
~I'd still be interested to know if there's appetite for separate casting instructions for each hierarchy for the codegen reasons above, especially since I didn't realise we already need to downcast from func
(I think one cast instruction per hierarchy would be fine, although @rossberg will likely have thoughts later).~
EDIT: actually, the typing rule just above would obviate the need for separate casting instructions for each hierarchy, because even naive codegen can just look at ht
to determine which hierarchy it's in.
Yes, that's the rule I had in mind. I tried to convey it briefly by the use of any
or anyref
in the rules I wrote down, but it's much clearer when you expand it like that.
[My apologies for missing the meeting, I've been out sick last week.]
Casts are tricky. There is a central design goal that the GC proposal has been very careful about: avoid the trap of a “fat object system”.
That can be split into several interrelated goals, and the design of cast operators touches most of them:
I’m increasingly worried how recent GC discussions have been gravitating towards one or several of these. For Wasm I think we ought to do everything to stay clear of them, because missing these goals would pretty much destroy Wasm's biggest advantage over prior VMs.
A “fat object system” particularly is one where all heap values are required to carry non-trivial meta data, which may even have to be computed and allocated dynamically. While perhaps normal for certain dynamically-typed languages like JavaScript, and bearable for languages with heavy-weight object systems like Java, that is not at all what you’d want for other languages that allocate lots of short-lived, light-weight heap values but have no type casting mechanism whatsoever. Just for perspective, OCaml's runtime “type” information fits into 4 bits – that’s all its runtime and GC need to know (and I have worked on complex VMs that required even less). It is unlikely that Wasm will be able to compete with that, but that’s no excuse for not keeping heap values as lean as possible.
A pervasive type reflection mechanism would be a particularly reliable way of locking us into that trap. Consequently, it was never the intention of the proposal that one can recover every possible type at runtime for every reference! Instead, the idea was that users can control whether selected objects have just enough runtime type tags attached to them to enable selected casts. Then, it is up to them to organise their data layout in a way that suffices their needs. And avoid cost that isn’t needed.
Over time, we deferred much of that from the MVP. For example, the ability to choose whether an object has an RTT, the ability to control where to materialise RTTs, the ability to control which points in the type lattice are even castable to.
Deferring probably was the right decision. With the current limitations of the type system, we can get away with arguing away implicit allocations as being static. But that does not work in general and it doesn’t mean that we can afford to drop the goal moving forward.
Static types and runtime types are very different beasts. They must be coherent, but the idea that they are to be arbitrarily interchangeable via full reflection would paint us into way too many corners, and I want to strongly push back against any expectation in that regard.
Finally, this hopefully explains why there is a principal difference between abstract "classifications" and concrete casts in the proposal: the idea is that concrete casts may or may not be opted into for a given object. On the other hand, abstract classifications must work for any object, regardless of whether they have concrete runtime type information attached. That means they have to be limited by design (arrayref is already a bit dubious in that regard). There is a high risk of slippery slopes. As a seemingly minor example, if we allow reflection on properties like eq-ness, then that would force a certain amount (even if just one bit) of extra runtime info to be bolted onto every value, forever, including those without full RTTs (assuming coherent rules). Even if that is cheap now, it may cause pain later on. I think we need to be maximally conservative here.
Hope that makes sense.
Thanks, yes, that mostly makes sense.
- Avoid implicit allocations
Agreed!
- Avoid implicit computations
Agreed in general, but this is fuzzy since GC instructions are necessarily higher level than MVP instructions and have to do more no matter what.
- Avoid monolithic types and instructions
Not sure what this means specifically, but my guess is that you would consider the proposed ref.as
to be monolithic. The sense in the wider group has been that it's better for the GC MVP to be self-consistent and factored slightly differently than it would be if it included post-MVP features than for us to try to depend on future proposals when designing the GC MVP.
- Avoid full-blown and pervasive type reflection machinery
Agreed!
All the MVP types have RTTs (even though they're implicit), so the proposed cast instruction is compatible with having custom RTTs in the future. Values with no RTTs would have to be distinguished somehow (probably at the type level) because no MVP cast will work with them, no matter how we design it.
Finally, this hopefully explains why there is a principal difference between abstract "classifications" and concrete casts in the proposal: the idea is that concrete casts may or may not be opted into for a given object. On the other hand, abstract classifications must work for any object, regardless of whether they have concrete runtime type information attached. That means they have to be limited by design (arrayref is already a bit dubious in that regard). There is a high risk of slippery slopes. As a seemingly minor example, if we allow reflection on properties like eq-ness, then that would force a certain amount (even if just one bit) of extra runtime info to be bolted onto every value, forever, including those without full RTTs (assuming coherent rules). Even if that is cheap now, it may cause pain later on. I think we need to be maximally conservative here.
Thanks, this makes sense. However, a type you cannot cast to does not seem useful. If I have two anyref
s that I want to either compare for equality or do something else with if they don't both support equality comparisons, then manually doing a case analysis on each value does not seem better than having the engine do that work for me, especially since many engines will be able to cast to eq
with much less work than a full case analysis. We should probably have a more thorough discussion about this.
If we do want to have abstract types that you cannot cast to, then we'll need conversions from externref to those types so they can be round-tripped through JS. Also, in that case I would be fine having ref.as
and simply disallowing some abstract heap types like eq
from being used with it.
All the MVP types have RTTs (even though they're implicit), so the proposed cast instruction is compatible with having custom RTTs in the future. Values with no RTTs would have to be distinguished somehow (probably at the type level) because no MVP cast will work with them, no matter how we design it.
In an earlier version of the design, we considered allowing structs to be created with no/partial RTTs. If we had cause to allow ref
-type objects without RTTs as part of a post-MVP extension, I think things could just work by making ref.cast
dynamically fail in this case (conceptually, ref.cast
checks if the object's RTT allows casting to the target type - for an object with an "empty" RTT the answer is always "no").
Reviving the idea of "partial" RTTs might be more awkward because IIRC we want the "1 type to 1 RTT" association for optimisation purposes.
As a seemingly minor example, if we allow reflection on properties like eq-ness, then that would force a certain amount (even if just one bit) of extra runtime info to be bolted onto every value, forever, including those without full RTTs (assuming coherent rules).
If my sketch above is correct, and we could support an "empty" RTT with ref.cast
failing dynamically, would it not be the case that when attempting to cast such an object to eq
, the cast could just fail?
@rossberg That's probably a larger discussion that might be worth surfacing in another thread.
Generally I agree that we should avoid baking overhead into Wasm's object model. However, it's not as simple as that, because there is a risk that being too simple just pushes the cost up a level in a way that engines cannot optimize across the boundary. I gave an example a couple months back about the double object header problem (one for Wasm, one for the language above) and how we'll need to figure out a way to have programmable metaobjects to avoid burdening language objects with two levels of meta information.
The kinds of hyper-optimized metadata examples that you describe are really only possible when the entire engine has one and only one program to host and that entire program is available at compile time. Because ultimately Wasm engines are programmable machines and their internal representation of objects necessitates enough metadata to distiguish Wasm objects within one program or instance from objects in another program or instance, just by virtue of them sharing a garbage collector implementation.
Having complex casts with lots of configurability (e.g. is the input null, non-null, or if null, how to treat null) is a way of presenting the cast operation as a single unit to the engine, rather than a cascade of casts that require the engine to do peephole or strength-reduction optimizations to get the optimal machine code. We've danced around the issue of how much global or even just inter-instruction optimization engines should be expected to do, but it's clear there are a couple of different viewpoints on the issue.
If we had cause to allow ref-type objects without RTTs as part of a post-MVP extension, I think things could just work by making ref.cast dynamically fail in this case (conceptually, ref.cast checks if the object's RTT allows casting to the target type - for an object with an "empty" RTT the answer is always "no").
I can even see the implementation of this being no-overhead. Already in our design we are assuming engines will be doing the Cohen display for constant-time subtype checks, which implies a bounds-check first. Such objects would just have no supertype display, so they'd fail all casts.
(For anyone else wondering who this Cohen fellow is: https://dl.acm.org/doi/10.1145/115372.115297)
I think we need to be maximally conservative here.
I did the thought experiment of considering which abstract type tests/casts are actually needed in dart2wasm. The result was: none of them.
eq
can be useful in connection with external values as mentioned earlier. But this ties in with the question of whether external values are comparable inside Wasm in the first place. And as a fallback, the comparison of external values can be performed via an import. That's what we do now.data
is mainly used as a stepping stone for casting from any
/eq
. With direct casting, this is no longer needed. It is also used in a few places where a few different, unrelated structs can be stored. But it would not be a problem to define a superstruct of all of them to use in these situations.array
is useful to abstract over the element type when accessing the length. But it would always be done via upcasting, usually through field covariance.i31
is the odd one out, since it's built-in but concrete. If/when we decide to include that, we can consider what its tests/casts look like.What would the situation look like for other producers if we decided to not have abstract tests/casts in the MVP at all?
It turns out that J2CL doesn't use those casts either.
What would the situation look like for other producers if we decided to not have abstract tests/casts in the MVP at all?
If we can move the MVP forward faster by having a single ref.cast
instruction that can only cast to concrete types, that wouldn't be the worst thing in the world, but it seems like quite a strange salami-slice given that it doesn't seem any more onerous for engines to allow (e.g.) any -> eq/data
casting. I'd be willing to bet that lack of such a cast will cause friction when GC is properly released and gains wider adoption. It's not too surprising that dart2wasm and J2CL don't need such casts as their compilation is totally monolithic.
To follow this path to its radical conclusion, what would it look like to remove all non-concrete types below any
(and potentially add them as needed in post-MVP)? This prevents certain abstractions, but also prevents the frictions that would come from having these abstractions without associated casts.
array
is useful to abstract over the element type when accessing the length. But it would always be done via upcasting, usually through field covariance.
Where does the field covariance come from? Is the point that source-level arrays are represented as a struct
with a length field and a Wasm-level array
field?
If we can move the MVP forward faster by having a single ref.cast instruction that can only cast to concrete types, that wouldn't be the worst thing in the world, but it seems like quite a strange salami-slice given that it doesn't seem any more onerous for engines to allow (e.g.) any -> eq/data casting. I'd be willing to bet that lack of such a cast will cause friction when GC is properly released and gains wider adoption.
I agree with this part.
Random holes in combinatorial spaces tend to end up forcing weird workarounds, particularly with use cases we haven't yet envisioned. (I am reminded of the one weird exception to C#'s generics design: can't use void
as a type argument). While there is certainly a case to be made for conservatism in leaving features out, we shouldn't just leave holes in things that language implementations will trip over.
A concrete case for casts to the abstract types (heh, sorry for the pun) is implementing type erasure. Typically source compilers (like javac) erase polymorphic types to their (least upper) bounds, and then automatically insert downcasts at usage sites. That all seems fine if usage sites are concrete types. But usage sites can be from other polymorphic code, which will get written to whatever bounds are computed by the compiler, which might be an abstract type like eq
or data
. So that's exactly how you end up with a cast down from any
to data
or eq
.
what would it look like to remove all non-concrete types below any (and potentially add them as needed in post-MVP)? This prevents certain abstractions, but also prevents the frictions that would come from having these abstractions without associated casts.
We'd have to remove ref.eq
because it operates on eq
types, or define it to operate on either struct
types or array
types, or i31ref
.
IMHO, we should err on the side of shipping a little more generality than what is demanded by specific languages right now, when that generality is clear and self-contained. In the specific case of casts, it's a simple rule to explain: any subtype subsumption can be reversed with a dynamic cast[1]. Leaving a random hole there will definitely get tripped over by a compiler that does any kind of LUB calculation to implement polymorphism.
[1] edit, to clarify: any subtype subsumption can be reversed, if needed, with a dynamic cast. We might want to make some of those casts opt-in, but there should be a way to express them.
A concrete case for casts to the abstract types (heh, sorry for the pun) is implementing type erasure. Typically source compilers (like javac) erase polymorphic types to their (least upper) bounds, and then automatically insert downcasts at usage sites. That all seems fine if usage sites are concrete types. But usage sites can be from other polymorphic code, which will get written to whatever bounds are computed by the compiler, which might be an abstract type like
eq
ordata
. So that's exactly how you end up with a cast down from any todata
oreq
.
Are we expecting to see type erasure in a source language that goes all the way to one of these abstract types? For (e.g.) Java I'd have expected the worst-case upper bound to be a struct
representing Object
.
Where does the field covariance come from? Is the point that source-level arrays are represented as a
struct
with a length field and a Wasm-levelarray
field?
Consider an abstract string class with two concrete subclasses that represent the string using 8 or 16 bits per character, respectively (that's what we have in dart2wasm currently).
The superclass can have a field typed as ref array
which is specialized covariantly in the two subclasses to arrays of either i8
or i16
. Operations that need to access the contents of the string will cast the string struct to its concrete subclass and read the specialized array field (which then does not need another cast). But an operation that just needs the length of the string can read the ref array
-typed field in the superclass and get the length from there, thus not needing any casts at all.
In any case, there is no need anywhere to test whether a particular value is an array.
@tlively:
Values with no RTTs would have to be distinguished somehow (probably at the type level) because no MVP cast will work with them, no matter how we design it.
The casts would just fail, e.g., because the value's given RTT is conceptually any
, so simply is not a subtype of the cast target.
However, a type you cannot cast to does not seem useful.
That is the very assumption I am arguing against, and that I suspect is underlying some of these discussions! Just drive this argument to its logical conclusion: it would imply that a language without casts isn't useful. Clearly, that's wrong. There are many languages that do not have casts – many don't even have subtyping to boot. You should ideally be able to compile those without the overhead of unneeded runtime type information in the heap. Likewise, even if you need casts, that by no means implies that you need casts everywhere and for everything. Moreover, as long as you can recover concrete subtypes, you can recover their supertypes.
many engines will be able to cast to eq with much less work
Unless the operation will be needed frequently, the amount of work is less relevant than the space overhead to support it. And the concern is that this becomes persistent and eternal. A conservative design would try to avoid baking in such decisions prematurely.
@conrad-watt:
we could support an "empty" RTT with ref.cast failing dynamically, would it not be the case that when attempting to cast such an object to eq, the cast could just fail?
That would perhaps be a possibility, but it would introduce an incoherence/discontinuity. It's desirable to have the natural property that if A <: B are both castable(!) types, and an object can be cast to A, then it can also be cast to B. Your suggestion would break that for data <: eq.
@titzer:
I gave an example a couple months back about the double object header problem (one for Wasm, one for the language above) and how we'll need to figure out a way to have programmable metaobjects to avoid burdening language objects with two levels of meta information.
I agree that it's not that easy and I agree that meta objects make sense (after our recent discussion of that, I have started a sketch of a design that I haven't found time yet to polish). But the crucial point is that any such a mechanism, and any overhead implied by it, needs to be opt-in.
their internal representation of objects necessitates enough metadata to distiguish Wasm objects within one program or instance from objects in another program or instance
Agreed, but the information needed by the GC might still be much less precise than what's needed for casts. For example, there is no need for a GC to know all the differences between struct i32 i32, struct f64, struct (mut f64) and so on. There may not even be a need to distinguish arrays and structs in most cases.
to clarify: any subtype subsumption can be reversed, if needed, with a dynamic cast. We might want to make some of those casts opt-in, but there should be a way to express them.
What I was getting at above: even as an opt-in, this would be far too strong as a general design constraint. Just consider the extension with nested arrays/structs and interior references: I don't think we ever want to allow reversing subsumption on interior references. Similarly, I brought up eq as a simple example where this might already be problematic for regular references. Moving forward, I think we will encounter many more such examples.
@conrad-watt:
Are we expecting to see type erasure in a source language that goes all the way to one of these abstract types? For (e.g.) Java I'd have expected the worst-case upper bound to be a struct representing Object.
Both Waml and Wob use eq
. Any language with uniform representation will (or any
), unless they make no use of i31, but off-hand I can't think of one that wouldn't want to.
Both Waml and Wob use
eq
. Any language with uniform representation will (orany
), unless they make no use of i31, but off-hand I can't think of one that wouldn't want to.
I tend to look at this from the opposite angle: the fact that the lack of an i31
union type forces uniform representations that include i31
to use eq
makes i31
in its current form unattractive.
@askeksa-google, while I'm generally sympathetic to unions, I doubt they'd buy you much for this use case.
I've put this on the agenda for the meeting tomorrow, and I hope we can decide on something by the end of the meeting.
@rossberg, what design for casts would you prefer given the constraint that casts to valid cast targets (whatever those may be) can be done directly from their corresponding top type?
to clarify: any subtype subsumption can be reversed, if needed, with a dynamic cast. We might want to make some of those casts opt-in, but there should be a way to express them.
What I was getting at above: even as an opt-in, this would be far too strong as a general design constraint. Just consider the extension with nested arrays/structs and interior references: I don't think we ever want to allow reversing subsumption on interior references. Similarly, I brought up eq as a simple example where this might already be problematic for regular references. Moving forward, I think we will encounter many more such examples.
Or it would imply that we wouldn't want to put interior references under the existing reference hierarchy, but under a new hierarchy. I suppose we'll cross that bridge when we get to it, but I'm increasingly making space in my mind for having fat pointers as a separate type hierarchy provided by Wasm and grouping interior references under that. With that implementation strategy, type casts aren't as big an issue as they would be if interior references must indeed be implemented with a single indirection (or box).
Again I'll reiterate a point I made earlier: implementing parametric polymorphism with type erasure puts source compilers in a position where they compute upper bounds, erase to those upper bounds, and then insert downcasts at usage sites. Since this happens automatically, holes are much more likely to be tripped over. Which is why I stand by my earlier assertion that we'll want, in the end, that any subsumption can be reversed with a cast.
@rossberg There is an efficiency/usability tradeoff spectrum that we need to acknowledge and perhaps we disagree on. If we err too far on the side of performance, neglecting to offer a reasonable feature because we are hyper-optimizing for a language that does not need it, then we may force language implementers to reinvent the feature one level up (usually at greater cost), or worse, to pass Wasm by altogether. We should not be looking to make Wasm a less attractive platform because it might save Prolog two bits in a header when compiled in whole-program AOT mode. This is why language requirements need to be part of our calculus and not simply optimizing Wasm's overhead in a vacuum.
@tlively, not sure how to answer that, since the constraint will not be satisfiable in a regular way.
@titzer, oh, I totally assume that interior references will be fat pointers in their own hierarchy. But that doesn't mean that casts are gonna be easy. To enable them, an engine would need a mechanism to figure out where into an (arbitrarily nested) type an offset is pointing. That implies arbitrary much implicit computation.
Again I'll reiterate a point I made earlier: implementing parametric polymorphism with type erasure
A compiler that is dependent on such invariants would simply avoid data constructs that do not support downcasts (or package them up accordingly, like we now require for externrefs or funcrefs). Their use is optional, avoiding them should always be possible. OTOH, I don't see how it helps that compiler to deprive everybody else of the option to use them.
@tlively, not sure how to answer that, since the constraint will not be satisfiable in a regular way.
We really can't budge on the requirement that casts can be done directly from any
because that's the only way to ensure the engine does the minimal amount of casting work without having to fuse operations. Although I do appreciate regularity, we wouldn't sacrifice performance for it.
One way to satisfy the requirement regularly would be to allow downcasting from any
to any other type in its hierarchy, as @titzer and others have been arguing for. Another would be to keep the current design with separate instructions for downcasting to particular abstract types, but to change ref.cast
and ref.is
to take any
. There are plenty of design options here, and we're quite flexible besides that one requirement.
@tlively:
We really can't budge on the requirement that casts can be done directly from any because that's the only way to ensure the engine does the minimal amount of casting work without having to fuse operations.
Sorry, but I haven't seen any evidence for this rather strong claim. Didn't Jakob report that it doesn't matter one way or the other? Are we putting premature optimisation before conservative and future-proof design?
You may remember that the risk of painting us into a corner wrt casts was exactly one of the major worries I expressed when we discussed deferring explicit RTTs. We are now stumbling into exactly that corner. :(
I see the currently proposed general ref.cast
as capturing exactly the casts one can do without needing a "materialised" RTT. The main design hazard would come from ensuring that future proposals which introduce/require real RTTs interact correctly with ref.cast
. I think it's reasonable to commit to a blanket restriction that ref.cast
can't do anything with (quantified, post-MVP) type variables, which I believe avoids at least some obvious problems. @rossberg do you see an issue with this general approach?
Didn't Jakob report that it doesn't matter one way or the other?
Definitely not. I am strongly in favor of casting from any
to concrete
(i.e. any concrete type) directly. Checking for data
first is a waste of time. An any->data
cast on its own is already more expensive than a direct any->concrete
cast. (At least if the latter gets lucky; its cost depends on circumstances.)
@rossberg , you said yourself that you didn't want to have a ref.as_eq
instruction at all, because you worried that this would be very expensive to execute due to the large set of eq
types. I am opposed to ref.as_data
(or at least: opposed to requiring that as the first step of a cast from any
) for exactly the same reason.
(What I said wouldn't make a difference is whether any->data
and any->concrete
is done with the same parameterized instruction, or two separate instructions that each allow a non-overlapping set of type parameters. Personally I'm also happy with the idea to postpone all casts to generic types, as has been suggested in the meantime: "introduce features when they have uses, not preemptively" is a good principle to follow. But I don't feel strongly about that; implementing casts to generic types isn't difficult, so if @titzer believes they have uses, we can certainly keep them. I just want to avoid forcing these expensive casts into performance-sensitive code paths when they provide no benefit.)
After discussing the performance considerations and many other things in today's subgroup meeting (notes PR), we have decided to go with a design that has one set of instructions casting from any
to concrete types and another set of instructions casting from any
to abstract types. Details on the treatment of nullability and on the encoding are TBD, but we hope to resolve them in this issue before the next meeting.
I've written up my proposal to resolve this as PR #325. I believe this addresses all the requests made in this thread.
Closing via #325.
With the addition arrayref and structref, it would make sense to refactor cast and classification opcodes to something more modular: instead of a long list of individual opcodes, use an immediate, like in
br_on <heaptype>
(with some suitable restrictions), and similarlyref.is <ht>
andref.as <ht>
. The interpreter has long been using something similar internally.Edit: To clarify, this is solely suggesting a change to the encoding of this group of instructions, not affecting their semantics.