WebAssembly / gc

Branch of the spec repo scoped to discussion of GC integration in WebAssembly
https://webassembly.github.io/gc/
Other
992 stars 71 forks source link

Remove anyref #254

Closed RossTate closed 2 years ago

RossTate commented 2 years ago

The Overview describes the purpose of anyref to be to support the sort of uniform representation typically used by polymorphically or dynamically typed GC languages. However, we have seen that these languages are not using anyref for their uniform representation. In short, no one has first-class values corresponding to function references, and as such everyone is using eqref for its uniform representation at the least. So there appears to be no particularly good reason to have anyref.

On the other hand, there are good reasons to not have anyref:

  1. The subtyping externref <: anyref forces JS values to be coerced to and from externref. For example, V8 uses SMIs, which conflict with i31ref, and so the coercion to externref requires boxing SMIs and requires unboxing i31ref-values-as-JS-values, and the reverse coercion has to do the opposite. Removing anyref would enable us to eliminate this coercion.
  2. When we were discussing long-term JS API considerations, one thing that came up was being able to access fields of typed JS objects directly from within WebAssembly. One way we could do this would be to import externref field accessors/members. However, the coercions required by the externref <: anyref subtyping prevent this, since those fields would store JS values in their "native" JS representation rather than their wasm representation. Removing anyref would enable us to have direct field accessors/members into typed JS objects.
  3. The instance-object model we're currently using means that many translations of GC languages into WebAssembly have to use closures where they would normally have just code pointers. Indeed, we're seeing substantial overheads in specifically the translations using function references, and my experiments (outside of wasm) suggest that using closures in place of code pointers indeed causes substantial overhead due to the extra chained load on a key language operation. One way we could reduce this overhead is to use a wide representation for function references, so that the code pointer sits alongside its environment (typically the instance-object) in memory, thereby reducing a chained load to a parallel load (and also likely improving locality). However, the funcref <: anyref subtyping forces function references to be represented the same as data references, preventing this optimization. Removing anyref could help us overcome the performance issues the current proposal is facing.

So, because anyref is not needed for its intended purpose, and because its presence obstructs various features and optimizations, I propose we remove it.

tlively commented 2 years ago

Overall I don't know of any good reasons not to do this. @rossberg, even Wob and WAML don't depend on func <: any, do they?

@jakobkummerow, would we be interested in experimenting with wide function pointers for function references?

The instance-object model we're currently using means that many translations of GC languages into WebAssembly have to use closures where they would normally have just code pointers.

Perhaps this would be better split off into a separate issue or discussion, but I would be interested in specific examples of this.

RossTate commented 2 years ago

The instance-object model we're currently using means that many translations of GC languages into WebAssembly have to use closures where they would normally have just code pointers.

Perhaps this would be better split off into a separate issue or discussion, but I would be interested in specific examples of this.

OO languages use a v-table for each class, with a field for every method of the given class. In a typical OO runtime, those fields are code pointers. In WebAssembly, those fields are typed function references. Because those typed function references can be implemented by functions from any module instance, and because different instances of the same module share code pointers in the instance-object model, they (with anyref) have to be represented at the least as a pointer to a closure containing the code pointer and the particular module instance the reference comes from.

The same issue arises with closures in functional languages. Even for the ones that use an instance-object model, normally the closure object contains the code pointer as a field. If the closure happens to depend on the module instance, then the pointer to the module-instance object is included directly in the closure object. But with anyref and WebAssembly's instance-object model, the (application-defined) closure must instead use a pointer to a (engine-defined) closure containing the code pointer and its module instance.

Thus anyref and the instance-object model add an extra (typically chained) load (with reduced locality) to the key operations of method dispatch and closure application. Furthermore, it's an extra load that's behind the scenes, so the generator cannot do anything to eliminate the extra operation.

jakobkummerow commented 2 years ago

I weakly support dropping anyref. "There's no use case" is all the reasoning I need. It's also consistent with our earlier decisions to drop things that don't have use cases (yet).

would we be interested in experimenting with wide function pointers for function references?

I doubt it. V8 heavily relies on a uniform representation internally. Exceptions are conceivable, e.g. we could special-case "array of funcref" or something. For playing such games, I don't think we care much whether funcref <: anyref or not.

RossTate commented 2 years ago

V8 heavily relies on a uniform representation internally.

Can you clarify this? Why is it problematic for a wasm field funcref to be internally represented as two fields? Presumably you already have support for wide fields like field f64.

jakobkummerow commented 2 years ago

Sure, struct fields could potentially also be special-cased.

timjs commented 2 years ago

We didn't discuss Waml yet during the meetings, but how is for example the identity function compiled to Wasm? For sure it needs to work on data as well as on functions, so I'd say you need anyref to support parametric polymorphism.

RossTate commented 2 years ago

Although we have not yet discussed Waml, @rossberg did confirm in the last meeting that Waml does not use anyref for its uniform representation. In answer to your question, parametric polymorphism only needs uniform representation for all values of the language at hand. All Waml's values are either datarefs or i31refs, and so eqref is sufficient for its uniform representation. (Furthermore, OCaml would likely want at least eqref for its uniform representation in order to provide a simple and efficient implementation of its polymorphic physical equality operator ==.)

rossberg commented 2 years ago

Anyref is the common supertype of externref and Wasm-internal reference types(*). As such, it is the only type that can abstract from whether some abstract reference (or type) external to a module is implemented in Wasm or by the host. Since making such abstraction possible is a stated goal of Wasm, anyref is needed.

As I already said in reply to this question during my talk, anyref would hence likely show up in something like Waml if you tried to support some form of FFI, which it currently doesn't have.

(*) Requiring that every Wasm reference can be externalised in every possible host environment would be quite a significant requirement, so assuming anyref = externref as opposed to just anyref >= externref is probably not a good idea in general, even if it could work in JS engines specifically.

RossTate commented 2 years ago

Polymorphic typed functional languages compiling to wasm need a way to downcast from their uniform representation to the lowerings of their concrete types. But there is no way to downcast from anyref to externref, so Waml's FFI wouldn't be able to use externref for its representation of values corresponding to foreign references.

But what Waml's FFI could do is use ref (struct externref). Yeah, this requires boxing foreign references, but Waml also needs to box float64; that's just an inherent cost of its implementation strategy. In fact, OCaml would likely want to do this anyways, as it keeps the implementation of both physical and structural equality simpler and more efficient.

Object-oriented languages would also do the same. They want to be able to assume all (object) values have methods like toString and equals, and so their uniform representation usually ensures even a v-table. Using externref in the uniform representation would mean having to branch on calls to these common methods. Instead, they'll box the externref in some class for foreign values, whose v-table will be filled with implementations essentially calling the appropriate imported function on the wrapped externref.

rossberg commented 2 years ago

You are missing the point of what I just said, namely achieving independence from how an outside reference is actually implemented (host vs Wasm). Whether that is then boxed up or not inside the language runtime is a different question. (FWIW, no downcast is needed for polymorphic functions if the lowered instantiation type is anyref.)

RossTate commented 2 years ago

Then please clarify your point, as what you're saying seems contradictory to me.

You are missing the point of what I just said, namely achieving independence from how an outside reference is actually implemented (host vs Wasm).

One of the problems with the externref <: anyref subtyping is that it does not allow for such independence. It forces external references to be coerced from their native form to one that is compatible with WebAssembly references.

FWIW, no downcast is needed for polymorphic functions if the lowered instantiation type is anyref.

This would require the FFI to use anyref, which seems problematic (especially if you don't want the foreign system to have to represent arbitrary wasm references, which I thought you were suggesting above).

This also forces anyref to be the uniform representation. That makes downcasts from the uniform representation to (other) types more costly, both in terms of instruction size and in terms of run time performance. This does not seem to be a worthwhile tradeoff compared to the boxing approach.

Could you articulate why the boxing approach is unsatisfactory to you?

rossberg commented 2 years ago

I thought we had long settled this. Yes, you need a uniform representation for anyref and externref. And no, there is no way around it given the goal of implementation independence for imported modules. Not unless you completely change the Wasm compilation model. Even without anyref you'd have the exact same problem with abstract type imports.

Again, boxing is a separate question and does neither help nor hinder in that regard.

RossTate commented 2 years ago

I thought we had long settled this.

I do not know how you came under this impression. #143 is precisely on this topic, over a year old, and unresolved.

Even without anyref you'd have the exact same problem with abstract type imports.

There are also unresolved issues regarding the type imports proposal related to the expectation that all imports should be subtypes of anyref: https://github.com/WebAssembly/proposal-type-imports/issues/6#issuecomment-631615549 and WebAssembly/proposal-type-imports#10.

Even if we want to stick with the model that imported types should not need to be known before compilation, you can achieve this by allowing unbound imported types to be instantiated with any reference type or with externref—so long as the engine has the same size for the two, this is still compatible with that compilation model.

rossberg commented 2 years ago

Even if we want to stick with the model that imported types should not need to be known before compilation, you can achieve this by allowing unbound imported types to be instantiated with any reference type or with externref—so long as the engine has the same size for the two, this is still compatible with that compilation model.

I don't see how that can work efficiently. One might be garbage-collected, the other might not? Both might use different tagging schemes? In general, the compiler already needs to know which is which. The GC will need to know it. Size is not enough. If they don't statically know how to interpret the representation they'd have to compile in multiple alternative paths everywhere, and dynamically select based on some per-type mode flag, which is no better than a uniform representation.

Can you provide any concrete example of a real life runtime that would not simply implement a uniform representation in such a scenario?

RossTate commented 2 years ago

Every browser's GC would have no problem with this.

I hear the problems you are raising, but externref <: anyref does nothing to help those problems either. If anything, they exacerbate the problem by having a type that mixes wasm and external references together.

If you want external references to be implemented using a different GC and to be usable as an abstract import, then you'll need to defer compilation until after type-import instantiation. At least that's a choice the engine will have available to it if the subtyping is removed.

tlively commented 2 years ago

Trying to prototype an FFI system taking advantage of externref <: anyref in Wob, WAML, or any other system and seeing what problems come up, if any, would be an excellent way to settle this question for good.

RossTate commented 2 years ago

I believe I already articulated those issues above.

My impression is that the outstanding issue is that @rossberg is concerned that, without this subtyping, externref will not be usable as a type export in his compilation model for wasm that mandates modules be compiled before type imports have been supplied. My most recent response was articulating why the subtyping at hand is unrelated to that issue.

titzer commented 2 years ago

I value implementation feedback higher than long discussion threads like this. I was under the impression that this was a settled matter, too.

I feel we risk falling back into old patterns and making no or negative progress. We are now closer than ever to an MVP that has a set of capabilities that are motivated by clear use cases and implementable in all known engines. If we want to make modifications then our discussions don't need to be hypothetical. We are deep in the implementation phase and work should be focused there.

For this issue, it's clear to me from the above discussion that removing externref <: anyref just creates problems, and solves none. The hypothetical size-compatibility requirement mentioned above is just representation compatibility by another name and there is unclear motivation why that shouldn't be full-blown subtyping. If it's a matter of a missing casting capability, we should discuss how urgent that is and whether we can add it.

I would also strongly encourage us to keep the possibility of type imports of nonref types open. Otherwise we are baking in the same (foolish IMHO) assumptions that were baked into Java's erased generics.

RossTate commented 2 years ago

The hypothetical size-compatibility requirement mentioned above is just representation compatibility by another name and there is unclear motivation why that shouldn't be full-blown subtyping.

Items 1 and 2 of the OP both identify issues with full-blown subtyping that are not issues with size compatibility.

If it's a matter of a missing casting capability, we should discuss how urgent that is and whether we can add it.

It's not. Comments above articulate why languages would not want to use anyref as their uniform representation.

If we want to make modifications then our discussions don't need to be hypothetical. We are deep in the implementation phase and work should be focused there.

Before @rossberg raised his objections, we were discussing concrete implementation tasks that could be explored should this subtyping be removed. Specifically item 3 in the OP is about addressing performance issues with the MVP. I did my own experiments outside of wasm to estimate the overhead caused by the funcref <: anyref subtyping, and arrived at roughly 30%. Obviously that should be taken with a grain of salt, but it's the only estimation of that overhead we have right now. Ideally we would implement the optimization suggested in item 3 in engines and run it on Java/Dart/Kotlin/whatever programs compiled to GC-wasm, but that requires substantial effort, and people understandably do not want to go through that effort if they are worried that you and @rossberg will object to removing the unused subtyping due to hypothetical compilers for hypothetical languages.

tlively commented 2 years ago

Right now the trade off is between hypothetical engine optimizations to the representation of externref and funcref and hypothetical FFI use cases that depend on type imports, which we have no implementer experience for and have not been collectively paying much attention to. Unless we can make this discussion much more concrete via implementation work one way or the other, it doesn't seem worth our attention right now.

If we still have no concrete experience with either side of this discussion when it comes time to cut the MVP for real, it will be prudent to make the decision based on our principles of incremental development. If the MVP does not include any, we can always add it later if users ask for it, but the reverse is not true.

RossTate commented 2 years ago

If we still have no concrete experience with either side of this discussion when it comes time to cut the MVP for real, it will be prudent to make the decision based on our principles of incremental development. If the MVP does not include any, we can always add it later if users ask for it, but the reverse is not true.

This seems like a reasonable plan.

Unless we can make this discussion much more concrete via implementation work one way or the other, it doesn't seem worth our attention right now.

What I can't tell is that if the engine optimizations were implemented and found to improve performance, would @rossberg and @titzer still object to removing anyref? If so, and the group were to uphold that objection, then I don't want to waste @jakobkummerow's time exploring this optimization.

So a way to make concrete progress on this issue is to have the group discuss whether they would support removing anyref should the optimizations perform well. That doesn't commit us to prematurely removing the feature, nor does it commit @jakobkummerow to ever actually exploring those optimizations.

titzer commented 2 years ago

Holding the door open for closures implemented as fat pointers just increases uncertainty to untenable levels. I speak with authority when I say this optimization just will not work in Wasm engines as envisioned. It is by no means trivial to do. (And I don't say that lightly--that's how Virgil implements closures, I love it, it works great, I wish we could--but alas, after years and years of thinking how to do that in Wasm engines, particularly V8 and Wizard, this has about zero chance to happen, because of concerns like value representation in interpreters, interacting with a host environment, the way Wasm instances already work, the requirement of a ton of compiler and runtime work in web engines, etc). I am disappointed by that, but, I think we should all move on from the fat pointer dream.

@RossTate I'll be entirely honest. I hate to be so blunt, but it's bothered me for some time, and it's a bad practice that now needs to be called out. I'm frustrated with your cavalier benchmarking. Big round numbers repeated often, becoming mythic, but really their genesis is microbenchmarks just in passing, written by hand but no source code shown, no link, no machine code, using completely different compilers, which usually isn't even mentioned what one, a totally different or no runtime system, no mention of allocators, etc, and absolutely no way to reproduce any numbers whatsoever. So throwing numbers like 30% around, from a benchmark nobody understands, attributing this to a "dependent load", when in all likelihood no one inspected the machine code by hand, is absolutely not how decisions are made. Bad numbers are worse than misleading, they are just tokens in advocacy of a design choice and delegitimize good numbers. Bad numbers from cavalier side-benchmarking deserve to just be ignored, frankly. Instead, please write some Wasm. I'll be convinced by Wasm. Or write machine code. I'll be convinced by machine code. Or compile something to Wasm. Or compile Wasm to something. Or tweak an engine. I've been trying to say this politely for months and years. Provide something measurements in Wasm ecosystem other people can reproduce.

For this discussion, we gotta stop blocking on hypotheticals or using numbers from other systems. There are at least 5 high performance Wasm engines out there and there's no excuse to not do benchmarking and optimization work on one of them. If we are to work as a group together, then I will strongly advocate that our work be done there. To the point where in my head I will massively discount benchmarking work that could and should have been done in Wasm but wasn't.

Short of fat pointers, which I am sad won't work, I predict zero performance benefit in V8 from removing anyref.

No one addressed @rossberg point above that anyref, short of unbound type imports, is the only mechanism to hide implementation details of another module (namely whether the module is implemented in the host language or Wasm). I'll add to that, again since we lack parametric polymorphism, that it is impossible to write a data structure in Wasm that can store both host and GC references--not even tables. Of course, that would require boxing.

My larger point is that we are just going around and around in circles having the same discussions all over again. Even when some of us believe that we have consensus. We need to keep moving forward and I honestly see at best, no strong motivation to change the status quo and at worst, a significant reduction in functionality.

Our process is far too slow and inefficient to ship things half-done. At this rate it will take us another 4 years to add anyref, if we were to remove it.

RossTate commented 2 years ago

no source code shown, no link, no machine code, using completely different compilers, which usually isn't even mentioned what one, a totally different or no runtime system, no mention of allocators, etc, and absolutely no way to reproduce any numbers whatsoever

Like other people that have posted data from benchmarks, I have made further details available when requested. The (Java) source code was linked upon request here: https://github.com/WebAssembly/gc/issues/249#issuecomment-942649890. The peer-reviewed paper and artifact for the language and implementation is here, which provides the source code of the compiler on GitHub and a VM containing a compiled executable of that source code and scripts to reproduce the plots I presented in the earlier meeting.

For the published experiments and this benchmark, we did review the code produced by the compiler. (Though, for the published experiments, since we generate hundreds of variations of the programs, we reviewed a few random samples as well as outliers and the fully untyped and fully typed extrema.)

In addition, I described the overall methodology of the experiment, which you could easily adapt to other programs, other compilers and runtimes, and other languages. I also described the rationale behind the benchmark design, and I mentioned characteristics of the benchmark that should be taken into account when interpreting its measurement.

Others reached out to discuss the experiments in more detail in order to make their own assessment of them. You could have done the same.

Formative evaluation, such as my experiment, is a critical part of the design process. Yes, it should not be mistaken for the sort of summative evaluation that you would see in a science publication, with p-values and such, but proper summative evaluation requires substantial resources that we do not have, and requiring all formative evaluations to be conducted as such would be crippling. Others have presented similar formative evaluations of overheads with much less information on how those estimations were arrived at, and those numbers have been repeatedly used in discussions that you've been involved in, and you expressed no criticisms. You are selectively applying this standard to just my formative evaluations in an effort to dismiss observations you find inconvenient.

I speak with authority when I say this optimization just will not work in Wasm engines as envisioned.

Rather than speaking with authority (and not offering any insights into why you arrived at this assessment, which could have helped us as a group contribute more effectively), you could have done what @tlively did and ask an actual active implementer of a current wasm engine for their thoughts. This follow-up response suggests that the specific representation optimization that would help with things like class/interface-method dispatch is viable.

To the point where in my head I will massively discount benchmarking work that could and should have been done in Wasm but wasn't.

It is absurd for you to say that I "could and should" have done this experiment in a wasm and at the same time say that the necessary optimization for performing that experiment "will not work in Wasm engines".

No one addressed @rossberg point above that anyref, short of unbound type imports, is the only mechanism to hide implementation details of another module (namely whether the module is implemented in the host language or Wasm).

We already had meetings discussing the kinds of compilation and abstraction we want to support. We decided we are doing whole-program compilation, possibly with module splitting, and with no attempt to abstract implementation details. So this concern seems to be out of scope for the MVP. Besides, as you mentioned, the problem is solved by unbound type imports, so there's a clear path towards addressing the issue without the subtyping. (There's also the larger issue of whether @rossberg's concern is even valid/reasonable. The survey of how various GC VMs support FFI and abstraction thereof, and no one had a type that's the union of GCed VM values and foreign values.)

My larger point is that we are just going around and around in circles having the same discussions all over again. Even when some of us believe that we have consensus.

We did discuss this, and the issue was left clearly unresolved (literally "Open"). If you believe there is consensus (or a group vote has been made), then close issues with the summary of that resolution. Assuming consensus without performing such a check is presumptuous.

As for having the same discussions over again, we recently got more highly relevant information on this topic, and so now we are discussing the issue again to determine if we now have enough understanding to make a decision. To that end, this still seems like a good plan. Though, if you'd like to not discuss this again, we could consider taking these further and decide on whether to remove anyref regardless of what experiments show.

titzer commented 2 years ago

Rather than speaking with authority (and not offering any insights into why you arrived at this assessment, which could have helped us as a group contribute more effectively), you could have done what @tlively did and ask an actual active implementer of a current wasm engine for their thoughts.

Frankly I find this comment massively disrespectful and no one else in this community is doing that. As a cofounder of Wasm and TL in V8 I designed its optimizing compiler, I derped its Wasm engine from nothing, I managed and led the team that maintains and evolves it and now I'm writing a new Wasm engine from scratch. Pretending my opinion doesn't count because I don't work on V8 anymore is just flat out an insult.

I don't assert things on authority often, and not without good reason.

I've learned that for some special reason this proposal is repeatedly derailed and being brief is the only rectification. Otherwise we spend so much energy on trivialities. I've repeatedly tried to remedy that by keeping us on point and making progress. So, I will, again, be brief. Closures as fat pointers won't work as envisioned. It's a wild goose chase that I wouldn't send people on. I could produce a load of noise, but I won't. If that doesn't satisfy you, then the burden of work lies on you, not others.

At this point I really regret how toxic this thread has gotten and I regret interacting here. It takes a lot of energy to talk about basic things and it's dysfunctional.

RossTate commented 2 years ago

Pretending my opinion doesn't count because I don't work on V8 anymore is just flat out an insult.

It's not that I do not recognize that you have some authority to speak with on this topic, nor did I mean to suggest that your opinion does not count. I see how that excerpt gives that impression, and I apologize for that unintended insult on my part. In past meetings you have shared insights from your experience that I have greatly appreciated. What I had intended to express in that excerpt was that I was upset that you claimed authority that can also be attributed to someone else who had already expressed a cursory assessment (which I referenced in the sentence following your excerpt) without acknowledging that their assessment conflicts with your own, as if their assessment did not exist or did not matter.

I am not sure if acknowledging the harmful impact of my error will have any effect at this point, but I thought it was at least worth trying.

jakobkummerow commented 2 years ago

I'd like to clarify that my assessment does not conflict with @titzer's statement. I expect no performance benefit from dropping anyref. We're not going to move to fat-pointer representations for funcrefs in general. Special-casing "unboxed" storage in a few select cases (with boxing as unified-representation object on demand) is a different strategy, and does not depend/block on the existence of anyref in general or the funcref <: anyref subtyping in particular. (And just to be clear, we haven't committed to spending time experimenting with this. We're very far away from saying "if only anyref didn't exist, we'd jump on this great opportunity immediately".)

In short, purely from a V8 implementation point of view, our perspective on anyref is essentially a big shrug: maintaining it is not a burden, removing it would be easy, removing it now only to re-add it later would not be very annoying.


Personally, I'm confused by the state of the overall discussion around funcref/externref/anyref; in particular it seems to me that we don't have consensus on what our goals/requirements are, and in consequence, what we consider valid arguments.

Specifically, if we argue that "we need anyref in addition to externref because by using externref on its interface, a module can require objects that another module can't fake/polyfill, but that's a key requirement of Wasm, and only anyref provides that capability", then that seems like a reason not to have externref in its current form at all -- yet externref in its current form is something that we definitely decided to have. Was that, perhaps, a mistake that we should undo? Or am I just missing something?

It's also not clear to me how to resolve the apparent contradiction between "anyref provides key required functionality" and "we currently have no known use case for this functionality". But again, maybe I'm just missing something; it's hard to wrap my head around all the hypotheticals that have been thrown around here.

There's also the unresolved "wart" that the current design makes it observable whether an implementation uses i31ref-style tagging for some externref values (issue #76). Depending on how we resolve that, a performance cost could easily arise from that.

(Of course it's not this thread's purpose to address my personal confusion, so feel free to ignore me; my reason for spelling this out is a faint hope that articulating the sources of my confusion might help the larger discussion gain clarity/focus.)

RossTate commented 2 years ago

Thanks for the thoughtful post, @jakobkummerow! I'm not sure I can address your confusion, but I can add my thoughts so that maybe we can all develop a mutual understanding of the goals and constraints.

Special-casing "unboxed" storage in a few select cases (with boxing as unified-representation object on demand) is a different strategy, and does not depend/block on the existence of anyref in general or the funcref <: anyref subtyping in particular.

I'm not sure how that special casing is compatible with that subtyping. With the subtyping, you can have a type $A with an immutable field $x of type anyref as well as a subtype $B where $x's type is refined to be funcref. This would seem to require that the funcref in $B's $x be stored boxed so that someone else accessing it as anyref through the $x field of an $A gets the value in the expected representation. (We could special cases funcrefs in fields that are not refinements of anyref fields, but that stops to work with a number of extensions in the Post-MVP and even MVP.)

Similar problems arise from abstract type imports, and it occurs to me that that might also pose another problem. I recall @titzer mentioning in a meeting that table.set is not necessarily a cheap operation in V8 because V8 does some work up front to make call_indirect perform more efficiently. If funcref is abstractable as a type import, and we want to support type-erased compilation for type imports, then we probably have to eliminate that optimization.


My understanding from when the reference types proposal was being shipped was that the pressing need for externref was specifically to be able to have a way to store and hand around JS references from within wasm. So that purpose is what I have been optimizing for, with the understanding that it should also be able to work for other embedding environments as well. I remember being told in one discussion that externref specifically did not need to be abstractable or virtualizable; it was a primitive type intended solely for storing host values. For example, in the C API I could see it being especially useful for externref to simply denote void*. So I am also unsure how to consolidate statements above with earlier statements and am now generally confused about what externref is for.


It would be very helpful to be offered some insight as to why fat representation of funcref is so difficult. With V8's 32-bit pointer compression, it's a 64-bit value that I would expect operations not specific to funcref (like local.get and such) could treat opaquely like i64 and f64. The two pointers are each stored in their typical form, so I wouldn't expect any special handling to be needed for GC.

jakobkummerow commented 2 years ago

I'm not sure how that special casing is compatible with that subtyping. [...]

Yeah, such a scheme would have to work around these cases somehow, such as by not being applied when subtyping is required. Maybe that makes it too difficult to be viable. For vtables it might still work out, not sure.

It would be very helpful to be offered some insight as to why fat representation of funcref is so difficult.

Off the top of my head:

RossTate commented 2 years ago

Thanks very much for the information, @jakobkummerow! Can I bug you for one more thing that would help me understand the picture? Is call_indirect simply implemented as essentially table.get followed by essentially a merging of rtt.cast and call_ref or is something more advanced done? In particular, are funcref tables optimized in some way to be usable by call_indirect more efficiently, and if so how so?

rossberg commented 2 years ago

@jakobkummerow:

yet externref in its current form is something that we definitely decided to have. Was that, perhaps, a mistake that we should undo? Or am I just missing something?

We only have externref because we removed anyref late in the game before and were left with a gap that we had to fill somehow. It's not something anybody desperately wanted.

rossberg commented 2 years ago

@RossTate:

We did discuss this, and the issue was left clearly unresolved (literally "Open").

As a high-level reply to this: the discussion culture on this repo is way too toxic and personal agendas way too destructive to make closing issues feasible anymore. Trying that for even a single issue would immediately derail into so much more wasted time and energy that I have long given up on formally resolving issues on this repo.

skuzmich commented 2 years ago

We currently use anyref in Kotlin to represent dataref | externref thanks to V8 addition that allows exposing dataref to JavaScript as opaque JS objects. It is useful to be able to test and downcast such unions to datarefs.

RossTate commented 2 years ago

Thanks for the info, @skuzmich! Can you articulate a little more how you use it? Is this for your any type, and if so have you evaluated its performance compared to using a type that guarantees a vtable with methods for equals and such? (I vaguely recall being told by Andrey that Kotlin native did not use packed integers for such performance reasons, but if y'all have found it's not an issue here than that would be very helpful to learn.)

skuzmich commented 2 years ago

@RossTate

Is this for your any type ... ?

No. Currently our kotlin.Any is a GC struct with vtable, as you described. We would want to measure impact of using anyref instead, but it is not a priority at the moment.

We use anyref for our external interface types, they represent JS API. They are very similar to our external interfaces in Kotlin/JS. These interfaces are currently auto-boxed when assinged to Any, or just down-casted to Any when they are already an instance of some Kotlin class.

RossTate commented 2 years ago

Ah, thank you for the information and links! Looking through the code, it looks like the one point where you use anyref is in the implementation of externRefToAny(ref: ExternalInterfaceType): Any?. That function was the sort of thing we wanted to facilitate by improving the JS API, e.g. by providing support for testing whether a JS value belongs to a particular wasm type. But, since you're not using anyref for your universal representation, it doesn't really rely on the subtyping in question here. In fact, using externref as the implementation of ExternalInterfaceType would have the advantage of ensuring that your compiler always inserts the complementary anyToExternRef(x: Any): ExternalInterfaceType function at the appropriate points so that boxed JS-as-Kotlin values are always unboxed before being passed back to JS. If the externref <: anyref subtyping were removed, it would also mean that ExternalInterfaceType would be able to denote JS values in their native representation.

As a side note, your link also references a reminder that a good JS API would enable wasm references to extend JS classes. Similar issues came up with J2CL, and the ability to write good (custom) coercions between JS and wasm references seems like an important JS API consideration. But I don't think we should need to add a core wasm type and subtyping to support such coercions. Unfortunately, it's been unclear how to progress the JS API to address your needs without first knowing whether we can or cannot hook into a nominal type system to facilitate these things.

jakobkummerow commented 2 years ago

@RossTate

Is call_indirect simply implemented as essentially table.get followed by essentially a merging of rtt.cast and call_ref or is something more advanced done?

Define "essentially" and "advanced", but probably the latter :-) call_indirect obviously predates both ref.cast and call_ref, and so does its implementation. We currently use three arrays in the instance object: an array of code pointers, an array of signature IDs, and an array of "references" to pass along (usually the instance, except in case of imported JS functions). The call_indirect sequence performs a single bounds check and then loads the i-th entry in each of the three arrays. The signature check is currently an equality comparison, not a full subtype check; that may have to be changed eventually.

So this could be seen as a variant of "special-cased fat-pointer-like representation". Memory locality obviously isn't great, but that's hard to change because the types are all fundamentally different: one array holds on-heap/GC'ed pointers (and is on the managed heap itself), one holds plain 32-bit integers, one holds 64-bit off-heap pointers; there's no good way to put all three into a single array. I wonder how a simple array-of-funcref implementation would perform in comparison (it'd have an additional indirection aka chained load, but then the final three loads would be from adjacent bytes in memory), but exploring that currently doesn't have high priority.

RossTate commented 2 years ago

Thanks for the information, @jakobkummerow! It's definitely helping me get a clear picture of how things work and what the constraints are.

I wonder how a simple array-of-funcref implementation would perform in comparison (it'd have an additional indirection aka chained load, but then the final three loads would be from adjacent bytes in memory), but exploring that currently doesn't have high priority.

The type imports proposal, as I currently understand it, would require this. A module could export its funcref table to another module that imports a ref $imported table (provided its imported heap type $imported is instantiated with func). Thus the (separately compiled) importing module would not know to use the special casing you described above.

skuzmich commented 2 years ago

@RossTate i agree, we (ab)use this subtyping only in a shallow way. Having some form of explicit fast coercion that retains identity when round-tripping is probably fine for Kotlin.

tlively commented 2 years ago

271, which makes extern an alias for any, recasts this question from "should we remove any?" to "should func be a subtype of extern/any?" since we can't remove externref. That new question is still reasonable to consider, but a lot of the discussion here is no longer directly applicable. I'll close this issue for now, but if anyone has anything to add to this discussion, please file a new issue.

For the sake of that future discussion, if we have it, here are some of the salient points from this discussion: