-
Notifications
You must be signed in to change notification settings - Fork 74
Pushing RTTs to post-MVP #275
Comments
I'd advise against that, for a number of reasons:
Ah, if that is your main motivation: I am pretty sure that this is no longer the case after #243. Because that change removes Does that make sense? |
Yes, thanks, your arguments make sense, but I still think removing RTTs is the best call for the MVP.
This is making a lot of assumptions about what the JS API will look like and what capabilities it will have, when my understanding is that we haven't really focused on the JS API design so far. It seems backwards to put as cross-cutting a feature as RTTs into core Wasm in service of the JS spec layered on top of core Wasm, although I'd be happy to discuss this further once we have a better idea of what we want from the JS API.
Maybe, but we don't have a cost model in any technical sense and I would hope that it's clear enough that
Understood, and I think that will be a very reasonable cost to pay for removing complexity from the MVP, given that it is a cost multiple implementations have been paying without issue.
Thanks, I had missed that we had already made RTTs and the static types completely equivalent for the MVP. This further convinces me that removing RTTs from the MVP is the best course of action. |
FWIW, when this was discussed previously, the suggested design involved attaching JS property information to the RTT. Due to canonicalisation, it would be somewhat tricky to associate the same information directly with the static type declarations if RTTs were made implicit - maybe object creation would have to be annotated through some custom section in order to to attach JS-specific information to the implicit RTT? |
There are only 10 instructions (including experimental ones) that require an rtt input:
I do not think it is a big burden to duplicate 10 or a few more instructions later, especially given that the rtt-less instructions are simply syntactic sugar of the explicit-rtt ones, therefore easy to implement. Note that the V8 implementation already provides both alternatives.
I would agree that local assignment could perform better in absence of certain optimizations, but
When it comes to globals, I do not think reusing them is easier than rtts: Since canonical rtts are global to the module, they need to be loaded from the instance just as globals, therefore the same optimizations are required to eliminate multiple loads thereof. In fact, mutable-global loads are harder to eliminate than (immutable) |
I am not sure I follow this. To check my understanding, for the relevant optimization here, it is not enough to compare RTTs - we also need to know which RTT an object has based on its type, statically. Here is a concrete example: (func $method.of.X (param $this (ref $Object))
(ref.cast $X ;; $X is some subtype of the generic $Object
(local.get $this)
;; =>
;; Inlining or some other VM or toolchain inference leads us to see that the
;; static type of $this is in fact $X, and not the more generic $Object.
;; =>
(func $method.of.X (param $this (ref $X)) ;; the type here changed
(ref.cast $X
(local.get $this)
;; =>
;; The relevant optimization: A ref.cast of something of type $X to $X is a no-op.
;; =>
(func $method.of.X (param $this (ref $X))
(local.get $this) ;; the cast has been removed Does #243 give us what we need for the last step here? |
I see, thanks. But do we expect that to stay the same in the future? If we extend wasm later in a way that makes it possible to construct an item with an RTTs other than |
Yes, that's right. And to prevent optimization regressions for modules that don't need the post-MVP extensions that use RTTs, we would want to add the RTT-free instructions at that point anyway. |
I added this to the agenda for our next meeting so we can discuss it in real time if necessary. |
I hadn't seen this thread so I missed chiming in earlier, but I agree with Andreas and Conrad that it seems like this will be quite limiting for potential JS API designs.
I agree that it would be tricky to do this kind of approach with a custom section. This has been brought up before in discussions, and there was also some concerns raised that this kind of use of a custom section would be new for Wasm, in that the behavior isn't really optional (e.g., for tooling or optimization) but would be required to be interpreted for the JS part of the program to work. Another thing I wonder about is how casting at boundaries between Wasm and host programs will work without instructions like |
@takikawa If a custom section has issues, could this be done on the JS API side? Without RTTs we basically need the JS side to declare a mapping of wasm type index to JS info, which there are a few ways to bikeshed. Another benefit of using the JS API is that this is really a JS-specific issue, so leaving it out of the wasm file and core wasm spec seems to make sense. The only maybe odd aspect that I can think of to such a mapping is handling of wasm canonicalization (which is I guess the issue you refer to before @conrad-watt ?). Say if type indexes |
If the goal of the GC MVP's JS API is to allow JS glue code to present wasm GC objects to JS code with nice idiomatic property names and methods on the prototype chain without an extra indirection through a JS wrapper object, I don't see a way to achieve this if the core wasm GC MVP doesn't provide the ability to supply something generative (i.e., not globally canonicalized) directly to You could of course imagine that each unified structural type is given an address in the store and so each Realm ends up associating one global prototype for each such unified structural type. However, I think this would, in the limit, lead to unrelated libraries accidentally clobbering each others' prototype chains when they happened to use the same structural type. (In particular, to avoid this problem, I've been assuming that wasm GC objects created with empty or canonical rtts would have a The "directly to The three possible sources of generativity I'm aware of are: type imports (allowing the JS API to generate fresh types imported by the module), rtts (allowing either wasm or the JS API to generate fresh rtts), |
Whether we want this and how much complexity we are willing to add to the core Wasm part of the MVP to get it is definitely something we should discuss more. My personal take is that this kind of rich interop would be a nice-to-have, but it's not worth complicating the core spec to get it. There are also potential benefits to not having such rich interop. Namely, if the JS API encourages the use of exported and imported getters and setters of more primitive data (e.g. raw numbers, opaque references, and arrays thereof) over the direct exposure of complex internal data types, that would give Binaryen more room to prove that certain fields are unused or always constant and remove them. If the JS API instead encourages exposing complex internal data types directly, Binaryen will need to be told how they will be used via some other more complicated means to perform those optimizations. |
I think I meant something equivalent to the solution you describe, in which at |
I don't think this should be a problem. In a world where we remove RTTs from the MVP, |
@tlively Totally fair questions regarding prioritization; I'll leave that to you all. But regarding:
I wouldn't imagine the JS API would encourage exposing any more accessors/mutators than otherwise: It seems like the surface area of the interface exposed to JS would be determined independently of the implementation mechanism used (whether fancy wrapper-free JS API or the JS wrapper objects you can do today).
It seems like, for a general wrapping story (like I've seen implemented in some linear-memory-language contexts already), you want to allow many different source-language classes/types to be exported to JS, each getting their own prototype with its own methods/accessors. In this scenario, you'd need the ability to control which prototype object to use for each individual |
Interesting! To make sure I follow, given this: (type $java.lang.Foo ..)
(func
..
(struct.new $java.lang.Foo) ;; location 1
..
(struct.new $java.lang.Foo) ;; location 2 It sounds like you want location 1 to use one JS prototype, and location 2 another? That's an interesting amount of flexibility. AFAIK all the things I've seen would be fine with both of those locations using the same JS prototype, since they have the same wasm type, but I'd be curious to hear more. (If we need this then we need this, but the more static our type story is the better it will optimize, which is why I'm concerned here.) |
If we end up needing to attach JS-specific information to individual occurrences of That being said, I'd somewhat prefer keeping explicit RTTs over the above (I was thinking about what it would look like to design an explicit FWIW I don't think this design question is inherently JS-specific, although it's obviously the main host we need to make sure we support. Any potential host that wants to treat the Wasm-level type as defining an object layout and expose Wasm objects to user code through some "regular-source-object-like" view (embedding Wasm in JVM?) probably wants a similar capability to attach richer (implicit, if necessary) RTTs to Wasm objects which carry host-specific info. |
Many good points have been made in this thread. Let me elaborate on the stated “1:1 correspondence” between static and dynamic types (in the new type system), since there were some questions about it. There is a bit more nuance to it.
This should be sufficient reason to decouple RTTs from both type definitions/imports and from object creation. I think it is desirable to have a more fine-grained and explicit cost model than just “struct.new allocates”, even for the MVP. Take the API problem as additional evidence that this decoupling is adequate. ;) As for the API question itself:
And with respect to cost and optimisations:
A couple of individual responses:
It is fine if some engines do additional optimisations. But as mentioned, producers should not have to rely on them to produce reasonable code.
Globals for RTTs can be defined as immutable just fine, since Anyway, sorry for the longish response. |
I don't see that happening. The fast runtime subtype checks we currently have depend on each RTT storing its list of supertype RTTs. That only works when there is exactly one RTT per subtyping level (as is currently the case, thanks to having only
I disagree.
For now, they can; but when modules or their embedders can generate customized RTTs, then the globals used to store them can no longer be immutable.
In my view, that is precisely why we should postpone RTTs. The MVP doesn't need them (it literally gets zero value from having them); and since making the model more flexible will have its costs, we should do that if and when we need it (and at such a time of course design it such that modules that don't care about the new flexibility can remain on the earlier fast path). |
Much of this discussion is devoted to how RTTs (or their absence) affect the JS API (and potentially other host APIs). Given that the JS API is going to be critically important for making this proposal usable on the Web, and thus a thing we'll need in some form for the MVP, I propose that we table the RTT discussion until we've done more work on the JS API. And when doing that work, we treat the existence of RTTs not as a given but as one option in the solution-space. That's not to say there aren't other arguments for & against RTTs-in-the-MVP raised in this thread, only that without more shared agreement on what the requirements & constraints are for the JS API, discussions about RTTs in isolation are less likely to be fruitful. What's been laid out so far has been useful, and can be picked up down the road when we're clearer on the JS interop question. |
@rossberg
Total strawman sketch of one possible tool for attaching prototypes to RTTs. It could be a
True, however that approach would require a bunch of JS code to get executed before the module can be instantiated. While (AFAIK) nobody has even approximate numbers on the cost of this (especially for large real-world modules) at this time, I am worried that it might be unacceptably slow. |
Right. It's important to acknowledge that Wasm cannot be expected to maintain type invariants that lie outside it. That is a very general observation, we're just touching the tip of the iceberg here. The only way such "round-tripping" could ultimately be achieved would be by including all possible features of all possible host type systems into the Wasm type system to subsume their expressiveness, which clearly is infeasible. For example, if a host type system had generic types, then Wasm would also need generic types, with the exact same semantics.
Yes. For types, I would expect this to be "declarative" to the extent possible. In the case of the JS API, I would assume there will be constructors to define Wasm types, and these constructors can take additional configuration arguments. Incremental modification like you suggest might also work, as long as it's not mutation.
It is unclear what you are comparing this cost against. Surely, somewhere something has to happen in any approach, it's just shifting the work left to right, isn't it? |
I believe it came up on a number of occasions, but the only pointer I have off hand is the discussion that @conrad-watt referred to above. |
@rossberg Thanks! I had indeed missed that discussion. (Reading it and the linked issues I still don't understand how the toolchain side would work, like how a bundler would merge modules safely, but maybe I'll open a new issue for that.) |
I agree with @lukewagner that RTTs somehow need to be generative somewhere--they do cannot map 1:1 onto Wasm types. That's critically important to allow source languages to piggy back source-casts (and typecases) on Wasm casts. In the above discussion, it seems like all discussion of |
To clarify: I'm comparing against any kind of declarative solution, which would allow engines to create the required RTTs and related internal structures lazily on demand, instead of batching it all up on the critical path before the Wasm module's first function ever executes. (FWIW, a "no-frills" solution wouldn't have to do any of this work.) |
@rossberg, you wrote:
If I understand correctly, you are envisioning that in a future where multiple distinct RTT values can denote the same Wasm type, there will be no difference between those RTT values with respect to casting. Is that right? So optimizations on casts that today assume RTTs correspond 1:1 with Wasm types would still be valid in the future? If we can agree on that now, then I would be more amenable to including RTTs in the MVP. But that understanding is at odds with @titzer's stated desire to have generative RTTs that can be used to piggy-back source-level casts. If we can't agree now on whether RTTs should be generative with respect to their interaction with casts, then we should punt on RTTs entirely to avoid being blocked on that decision and possibly having GC-MVP programs regress in optimizability in the future. We won't be able to gather performance data to inform that decision until we start working on post-MVP proposals that would introduce new sources of RTT values. |
@tlively, I would definitely want to maintain this invariant moving forward, since it improves the coherence of the type system (static vs dynamic), which is a desirable property in itself. I don't think we'd want generative RTTs as its own mechanism in the current design. Instead, if there is a strong need for generative types, we'd introduce generative type definitions, whose rtt.canon then likewise is a distinct type. That's all that's needed for the use case Ben describes. See also my reply to @kripken. |
I've proposed an agenda item to discuss this at the May 3 subgroup meeting: WebAssembly/meetings#1013 |
If we maintain the 1:1 correspondence between static types and RTTs even in post-MVP extensions, then isn't it true that RTTs will always be semantically redundant with static type annotations? So not only could we remove RTTs from the MVP, there also would never be any reason to reintroduce them. |
@tlively, no, not so. Because static types will (hopefully soon after MVP) involve components that are not known at compile time, either type imports or generic type parameters to functions. In those cases, you'll still have a 1:1 correspondence, but you cannot construct the RTT implicitly – unless engines implicitly pass RTTs around almost everywhere, which is highly undesirable. As a I've pointed out in the past, that is the most important reason for having explicit RTTs: to avoid a hidden type passing semantics once we add those features. Because that would introduce inherent complexity and substantial hidden runtime costs (including non-trivial allocations at generic call sites). Thus RTTs are crucial later, and they are important now for proper forward compatibility. If we defer them now, we'd have to introduce extra versions of all the relevant instructions later, and impose odd restrictions on the RTT-less ones, both of which would be ugly. In brief, it'd be shooting a big hole into the design. |
I think this is the key point and I'd like to understand it more. It has previously come up that explicit RTTs could allow compilers to better express how the RTTs would be loaded and register allocated, but the feedback from implementers was that this wouldn't really be useful since engines do their own register allocation from scratch. I know that in the future when we have type parameters to functions, the idea is that explicit RTTs corresponding to those type parameters would be passed as arguments. With implicit RTTs, the engine would add those arguments implicitly based on the type parameters to the function. For generic type imports the engine would similarly generate implicit RTT imports based on the static type imports. Are there any other situations in which you expect the engine would have to implicitly pass RTTs around if we got rid of explicit RTTs? It's not obvious to me that this implicit passing is undesirable. I expect that in the vast majority of cases explicit RTTs would be used to express the exact same passing schemes that the engine would have generated anyway. I don't see any intrinsic value in duplicating the static immediate/parameter/import as an additional value-typed immediate/parameter/import, since the former should make it clear that something is being passed. Is there more to this that I'm not seeing? |
Right. The problem is that an engine generally needs to have a fixed calling convention. That implies that by default, the engine would have to pass RTTs to all generic functions, regardless of whether they need them or not. That would be terrible! An engine could possibly optimise those away in some limited cases where a function doesn't escape, but in general it is almost impossible to avoid that overhead. And this overhead isn't just passing RTTs. Worse, it involves allocating and constructing RTTs at call sites with somewhat unbounded cost! For example, when a generic function calls another generic function with a type involving its own type parameters, then it cannot be pre-allocated:
Here, the call to Now, unlike an engine, a producer will have much more systematic knowledge about where casts are introduced and hence RTTs are needed in its compilation scheme. In fact, some producers may have no need for casts at all once we provide generics! So if RTTs are properly manifest, producers can systematically control and minimise their construction, caching, and passing for their use cases. Also, they can afford much more effort for expensive global analysis to optimise them. This separation making the operational manifestation of runtime types explicit is pretty much standard, one way or the other, in principled designs for low-level languages combining runtime typing with generics [e.g., 1, 2, 3, 4]. The primary counter example would be the CIL (counting that as low-level), whose reified generics are notorious for the engine complexity they induce and probably inspired some of the cited work to do better. |
Ah I didn't think about generic functions calling each other. That does make the possible overhead seem more "real". Returning to the question of whether we could remove RTTs for now and add them in back later, could we make |
If explicit RTTs are required for generics, then we should introduce generics and explicit RTTs at the same time. Both now, or both later. (Yes, adding |
After the latest discussion, it's clear that it will make sense to have RTTs in their current form once we introduce type imports/exports, generics, or potentially other post-MVP extensions. However, it remains the case that RTTs would have no benefit in the MVP, and in fact would come with a 6% code size penalty (as measured on Dart by @askeksa-google). This code size penalty would not go away or pay for itself in the future since some toolchains such as J2CL will never use post-MVP features that would benefit from having RTTs. Removing RTTs from the MVP and introducing them later also does not have any cost beyond maintaining RTT-free and RTT-using versions of cast and allocation instructions. This cost is trivial not only in implementations but also in the spec, where the RTT-free instructions would be defined in terms of their RTT-using variants composed with Although I would welcome new information to the contrary, removing RTTs from the MVP would decrease MVP spec complexity without affecting future spec complexity, would have small but significant code size benefits, and would be in line with our philosophy of incremental development, especially since we have no clear timeline for working on generics or type imports. Unless anyone can identify any concrete problems with removing RTTs from the MVP and adding them back in later, I would like to wrap up this discussion no later than our meeting on May 31. |
I think this would work and I would be fine with that, but it might be even simpler to allow RTT-free instructions to work even with type parameters, where the implicit |
After recent discussions, it is clear that the most active stakeholders agree on the technical details around this decision but will not be able to unanimously agree on a path forward. To settle this issue one way or the other and move on to other things, I've scheduled a consensus poll on this for our meeting on Tuesday. The proposed question to poll for is "should we defer RTTs from the MVP and reintroduce them alongside generics, type imports, or another post-MVP proposal?" If the poll does not demonstrate consensus in favor of deferring RTTs, then we will keep them as currently proposed in the MVP. |
After a good discussion this morning, we successfully polled for consensus to defer RTTs from the MVP with the precise text given above. I'll leave this issue open until we can update the proposal docs to reflect this change. |
We landed #306, so closing this. |
https://bugs.webkit.org/show_bug.cgi?id=243079 Reviewed by Justin Michaud. This patch removes RTT support from the Wasm GC implementation. This part of the spec was postponed to later post-MVP phases in recent decisions. PR for RTT removal in spec: WebAssembly/gc#306 Discussion issue: WebAssembly/gc#275 Instead of using RTTs, MVP instructions will use the type index for the information needed to allocate values or do casts. For example, array.new_canon $t : [t' i32] -> [(ref $t)] instead of array.new $t : [t' i32 (rtt $t)] -> [(ref $t)] * JSTests/wasm/gc/rtt.js: Removed. * JSTests/wasm/wasm.json: * Source/JavaScriptCore/bytecode/BytecodeList.rb: * Source/JavaScriptCore/llint/WebAssembly.asm: * Source/JavaScriptCore/wasm/WasmAirIRGenerator.cpp: (JSC::Wasm::AirIRGenerator::gRef): (JSC::Wasm::AirIRGenerator::tmpForType): (JSC::Wasm::AirIRGenerator::emitCCall): (JSC::Wasm::AirIRGenerator::AirIRGenerator): (JSC::Wasm::AirIRGenerator::gRtt): Deleted. (JSC::Wasm::AirIRGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp: (JSC::Wasm::B3IRGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmCallingConvention.h: (JSC::Wasm::WasmCallingConvention::marshallLocation const): * Source/JavaScriptCore/wasm/WasmFormat.h: (JSC::Wasm::isValueType): (JSC::Wasm::isValidHeapTypeKind): * Source/JavaScriptCore/wasm/WasmFunctionParser.h: (JSC::Wasm::FunctionParser<Context>::parseExpression): (JSC::Wasm::FunctionParser<Context>::parseUnreachableExpression): * Source/JavaScriptCore/wasm/WasmLLIntGenerator.cpp: (JSC::Wasm::LLIntGenerator::callInformationForCaller): (JSC::Wasm::LLIntGenerator::callInformationForCallee): (JSC::Wasm::LLIntGenerator::addArguments): (JSC::Wasm::LLIntGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmOperations.cpp: * Source/JavaScriptCore/wasm/WasmOperations.h: * Source/JavaScriptCore/wasm/WasmParser.h: (JSC::Wasm::Parser<SuccessType>::parseValueType): * Source/JavaScriptCore/wasm/WasmSlowPaths.cpp: * Source/JavaScriptCore/wasm/WasmSlowPaths.h: * Source/JavaScriptCore/wasm/js/WasmToJS.cpp: (JSC::Wasm::wasmToJS): * Source/JavaScriptCore/wasm/wasm.json: Canonical link: https://commits.webkit.org/252788@main
https://bugs.webkit.org/show_bug.cgi?id=243079 Reviewed by Justin Michaud. This patch removes RTT support from the Wasm GC implementation. This part of the spec was postponed to later post-MVP phases in recent decisions. PR for RTT removal in spec: WebAssembly/gc#306 Discussion issue: WebAssembly/gc#275 Instead of using RTTs, MVP instructions will use the type index for the information needed to allocate values or do casts. For example, array.new_canon $t : [t' i32] -> [(ref $t)] instead of array.new $t : [t' i32 (rtt $t)] -> [(ref $t)] * JSTests/wasm/gc/rtt.js: Removed. * JSTests/wasm/wasm.json: * Source/JavaScriptCore/bytecode/BytecodeList.rb: * Source/JavaScriptCore/llint/WebAssembly.asm: * Source/JavaScriptCore/wasm/WasmAirIRGenerator.cpp: (JSC::Wasm::AirIRGenerator::gRef): (JSC::Wasm::AirIRGenerator::tmpForType): (JSC::Wasm::AirIRGenerator::emitCCall): (JSC::Wasm::AirIRGenerator::AirIRGenerator): (JSC::Wasm::AirIRGenerator::gRtt): Deleted. (JSC::Wasm::AirIRGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp: (JSC::Wasm::B3IRGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmCallingConvention.h: (JSC::Wasm::WasmCallingConvention::marshallLocation const): * Source/JavaScriptCore/wasm/WasmFormat.h: (JSC::Wasm::isValueType): (JSC::Wasm::isValidHeapTypeKind): * Source/JavaScriptCore/wasm/WasmFunctionParser.h: (JSC::Wasm::FunctionParser<Context>::parseExpression): (JSC::Wasm::FunctionParser<Context>::parseUnreachableExpression): * Source/JavaScriptCore/wasm/WasmLLIntGenerator.cpp: (JSC::Wasm::LLIntGenerator::callInformationForCaller): (JSC::Wasm::LLIntGenerator::callInformationForCallee): (JSC::Wasm::LLIntGenerator::addArguments): (JSC::Wasm::LLIntGenerator::addRttCanon): Deleted. * Source/JavaScriptCore/wasm/WasmOperations.cpp: * Source/JavaScriptCore/wasm/WasmOperations.h: * Source/JavaScriptCore/wasm/WasmParser.h: (JSC::Wasm::Parser<SuccessType>::parseValueType): * Source/JavaScriptCore/wasm/WasmSlowPaths.cpp: * Source/JavaScriptCore/wasm/WasmSlowPaths.h: * Source/JavaScriptCore/wasm/js/WasmToJS.cpp: (JSC::Wasm::wasmToJS): * Source/JavaScriptCore/wasm/wasm.json: Canonical link: https://commits.webkit.org/252788@main
I would like to propose pushing RTT value types and all instructions that use them to post-MVP. Binaryen and V8 support RTT-less versions of all allocation and casting instructions that use static type index immediates instead of dynamic RTT values and j2wasm and Dart have both found these static versions of the instructions to be sufficient for their needs in the presence of explicit supertype declarations, as used in #243. In particular, Dart has implemented its own userspace type information that it uses to implement language-level casts, but that custom type information does not depend on RTT values.
We have also found that Binaryen can optimize modules that use RTT-less instructions to be about 3% faster than the same modules with RTTs because the reduced dynamism allows Binaryen to prove that more casts are unnecessary. This better optimizability will become increasingly important as engines do more inlining and follow-on optimizations themselves.
As a concrete example, consider an inlined method call that downcasts its
this
parameter. With RTTs, showing that the cast always succeeds requires proving that thethis
value has always been allocated with the same RTT used in the cast, whereas an RTT-less cast can be shown to always succeed just by looking at the static type of thethis
argument and seeing that it is a static subtype of the parameter's type.If we find that we need to add RTT values back into the language as part of a post-MVP generics proposal or for some other reason, we can easily add RTT-using versions of the allocation and cast instructions back as well. That we have already been supporting both kinds of instruction in Binaryen and V8 demonstrates that the maintenance overhead of that duplication is not a problem.
The text was updated successfully, but these errors were encountered: