Repository navigation
Alternatives to i31ref wrt compiling parametric polymorphism on uniformly-represented values (OCaml) #100
Description
As i31ref doesn't seem to be an unanimously-agreed-on (see #53) part of the GC MVP spec, I am very interested in discussing what the concrete alternatives to it are in the context of parametric polymorphism on uniformly-represented values. (I would appreciate if the answer doesn't immediately read as "use your own GC on linear memory".)
To give some (historical) context: Why does OCaml use 31-bit integers in the first place? Generally, it is possible, to have a model of uniform values where every value is "boxed" (i.e. lives in its own, individually allocated, heap block). Then, every value is represented by a pointer to the heap and can be passed in a single register when calling a function. A heap block always consists of a header (for the GC), and a sequence of machine words (values). From an expressiveness standpoint, this is fine. However, when even simple values such as integers are always boxed (i.e. require a memory access to "unbox" them), performance suffers. Design constraints for the representation of unboxed integers were: a) need to be able to pass unboxed integer values in a single register, and b) need a means for the GC to distinguish (when crawling the heap) whether a value in a heap block represents an unboxed integer or a pointer to another heap block, c) being as simple as possible for the sake of maintainability. In OCaml, the compromise between performance and simplicity that was chosen is to unbox integer values by shifting them left by one bit and adding one. Since pointers are always word-aligned, this made it trivial to distinguish unboxed integers from values that live behind heap pointers. While this is not the best-performing solution (because all integer arithmetic has to operate on tagged values), it is a simple one.
Note that there exist compilation targets of OCaml that use 32-bit integer arithmetic, and the OCaml ecosystem largely accounts for that. Having libraries also consider the case where integers have 64-bits seems feasible. Some code will get faster if we can use native 32-bit integer arithmetic.
Ideally, for the sake of simplicity, we would like to emit one type Value to WebAssembly, which represents an OCaml value, which is either:
- a reference to a heap block
- an unboxed value that fits into a machine register
- a reference to some opaque value from the outside world (traditionally, for OCaml, this is C, but in WebAssembly, this could be either an
anyrefor some address in the linear memory of another module)
A heap block of OCaml traditionally consists of
- a header word that holds a) a tag that gives some information about the block, b) GC color bits (obsolete with WASM GC), and c) the size in machine words of the block
- a sequence of machine words, length of it being the size specified in the header.
The most trivial representation (i.e. the one matching most closely the existing one) that I see when I look at the MVP spec is an anyref array that holds both references to other heap blocks and i31ref values. So, from the viewpoint of having to do as little work as possible in order to compile to WebAssembly and keeping the implementation simple, i31ref is certainly looking very attractive for an OCaml-to-WASM compiler MVP.
In #53 (comment), @rossberg summarized:
For polymorphic languages, there are these [heap representations]:
- Pointer tagging, unboxing small scalars
- Type passing, unboxing native scalars, runtime type dispatch
- Type passing, unboxing native scalars, runtime code specialisation
- Boxing everything
- Static code specialisation
From OCaml's perspective, I think that (2) and (4) don't seem acceptable as a long-term solution in terms of performance. Here, compiling to the WASM linear memory and shipping our own GC seems a more attractive choice.
So, that leaves (3) and (5).
(3) seems fairly complex. If the WebAssembly engine would do the runtime code specialization, or if we could reuse some infrastructure from another, similar language, it could be worthwhile for us to work with that. It currently seems unlikely that OCaml in general will switch to (3) in the foreseeable future, unless we can come up with a simple model of runtime code specialization. I expect that implementing runtime code specialization in a WebAssembly engine goes way beyond a MVP, so it seems unlikely this will happen.
(5) is simpler than (3) in the sense that we do not have to ship a nontrivial runtime. If we analyze the whole program in order to emit precise types (struct instead of anyref array) for our heap blocks on WebAssembly, we wouldn't need to use i31ref and we can reap the other benefits of whole-program optimization (e.g. dead-code elimination, operating with native unboxed values, no awkward 31-bit arithmetic). Still, this will be a sizeable amount of work (possible too much to do it right away).
I also can't say how bad the size of the emitted code will be in terms of all the types we need to emit. Instead of emitting a single Value type, we need to emit one struct type for every "shape" of heap block that can occur in the program. To keep this manageable, we need to unify all the types whose heap block representations have the same shape.
Then, static code specialization kills one nice feature of OCaml: separate compilation of modules. However, instead of doing static code specialization before emitting WebAssembly, maybe it is possible to implement a linker for our emitted WebAssembly modules that does code specialization at link time if we emit some additional information to the compiled WebAssembly modules? This kind of linker could possibly be interesting to other languages that are in a similar position as us, as well. Obviously, link time will be slower than we are used to. I haven't thought this through in detail at all.
It seems likely that these issues are manageable, if enough effort is put into them.
Edit: while the previous paragraph sounds fairly optimistic, looking into whole-program monomorphization (turning one polymorphic function into several non-polymorphic ones) more closely, it is definitely not trivial to implement. Types that we would need for this are no longer present at the lower compilation stages. When I look at the MLton compiler (a whole-program-optimizing compiler for Standard ML), it seems that it is a good idea to monomorphize early, in order to be able to optimize based on the types of parameters. Features like GADTs, and the ability to store heterogenous values or polymorphic functions in hash maps (or other data types) do not make it simpler. It looks to me like this would mean almost a full rewrite of the existing compiler and it is not obvious whether every function can be monomorphized (without resorting to runtime type dispatch).
Are we missing something here, are there other techniques that we have been overlooking so far? Feel free to drop pointers to good papers on the topics in general, if you know some.
Also, I am very interested what perspective other languages with similar value representations have on this and whether there is interest in collaborating on a code-specializing and dead-code eliminating linker.
Activity
RossTate commentedon Jun 27, 2020
Although you've ruled it out, I would actually suggest (4) for OCaml, albeit with some optimization.
First, the optimization I have in mind is to have OCaml function definitions operating on
intcompile to wasm function definitions operating oni32. For polymorphic uses of these functions, have another definition that works on whatever "uniform" representation of OCaml values you use, simply calling the optimized function with appropriate (un)boxing. Or more generally, when a value can be statically determined to be anint, represent it asi32.There are a few reasons I suggest this:
i31ref. It's also possible it'll instead have something likei32ref, which would generally be boxed on 32-bit machines and unboxed on 64-bit machines.So in short, I suspect the short-term costs of this approach are more acceptable than you might expect (with some local/composable optimization techniques), and I suspect it is more likely to adapt well to integrate the improvements that will be made to WebAssembly over time.
There's also a meta-point to consider: having WebAssembly modules compiled from OCaml in this manner will give the CG realistic programs to experiment with and analyze, which can be used to determine if it's worth the effort (at all levels of the process) to give modules more control over their low-level representations. Right now we can only hypothesize.
That all said, you know your specific needs much better than I, and even if this approach might work well for the OCaml community broadly, it still might not be well-suited to more specialized concerns you might have.
sabine commentedon Jun 28, 2020
Fair enough, (4) is close enough to (1) to look like the next best solution in terms of effort needed and we could use
i32arithmetic. I'll look into this in more detail to see if it is indeed so straightforward.sabine commentedon Jun 28, 2020
I'm wondering if I can do this by first compiling to "WASM GC MVP with i31ref" and then translating that code to "WASM GC MVP without i31ref". At least, this way, I will not have to go all the way back to unbox all these integers again when a feature that is equivalent to 31ref arrives. Has anyone done that?
RossTate commentedon Jun 29, 2020
Even with
i31ref, you have to coerce integers to and fromi31refusing instructionsi31.newandi31.get_u/i31/get_s. So, if the design ofi31refchanges, it should be pretty trivial to change your compiler to substitute in the appropriate coercion instructions.The more difficult thing to change would be using 31-bit vs. 32-bit arithmetic. It sounds like putting in the effort for 31-bit arithmetic is only worth it if
i31refexists. Since that's not guaranteed, I'd use easier 32-bit arithmetic for now, and then put in the effort to switch to 31-bit arithmetic only if/wheni31refhas been finalized.rossberg commentedon Jun 29, 2020
@sabine:
That would be the smallest problem. More importantly, as you note in your edit, static specialisation kills any form of first-class polymorphism, of which OCaml has plenty (polymorphic methods and record fields, first-class modules, GADTs, ...). So I think this approach is simply impossible to use. The same is true for any other language with a rich enough type system.
@RossTate:
That is not a scalable approach. In languages like OCaml, you have polymorphic type inference, and by design that results in way more polymorphism than in traditional languages. You also use lots of abstract types. In particular, many functions are polymorphic in multiple independent types, and refer to multiple abstract types. To be efficient, the approach you suggest would require an exponential number of specialisations to be generated upfront for each such function.
There also is the practical problem of porting. If the GC proposal does not support established implementation techniques, but instead requires a major rewrite of a pre-existing compiler and runtime, then it will not be a plausible target for many existing languages.
RossTate commentedon Jun 29, 2020
The advice I gave here is specific to OCaml. There is only one type we were talking about doing this for:
int. You can do these coercions per use site, and you can consolidate coercions if you want. These are conceptually coercions between monomorphic and polymorphic calling conventions. There will need to be coercions between curried and uncurried calling conventions anyways. That is, anint -> int -> intcan and should be called with two arguments (i.e. uncurried), but it can also be given to of type(int -> 'a) -> int -> 'a, in which it must be called with one argument (i.e. curried). That polymorphic function will then return a function using a curried calling convention and so that result often needs to be coerced to an uncurried calling convention. That is, unless you are suggesting that all functions always use the curried calling convention, since WebAssembly does not currently support the sort of variadic arguments that would be necessary to eliminate these coercions. So my suggestion just hooks into infrastructure that would need to be in an OCaml-to-WebAssembly compiler anyways.gasche commentedon Jun 29, 2020
There has been a good amount of research work on type-directed or shape-directed unboxing, supported by code specialization. For a recent example, see the work on call-graph-based specialization of (boxed) polymorphic functions as part of the 2017 thesis of Dmitry Petrashko on Scala. But these work typically require a fair amount of sophistication. In contrast, having an efficient way to "box" immediate scalars without going through the heap is easy for backends to allow, and fairly easy to compile into for types that fit the smaller width. The two approaches are complementary (clever specialization optimizations has other benefits), and yes, boxing everything is of course possible if the low-level substrate does not provide better, but getting reasonable performances requires much more sophistication on the user side.
Many of the successful language implementations work by keeping a tight check on their complexity budget: they go for the 20% of sophistication that brings 80% of the benefits in as many areas as possible. OCaml was faster than most ML, or Chicken Scheme than most Schemes, not through extremely optimizations, but rather thanks a few very-important optimizations (static function calls) and good data-representation choices that ensured that typical programs were fast by default.
It is also possible to build fast systems that are slow by default but fast through excellent, sophisticated engineering (good JVMs or CLR implementations, GHC, Graal+Truffle, etc.), but I agree with @rossberg that it is important to ensure that the 20%-80% approaches are available first.
RossTate commentedon Jun 29, 2020
The issue is "alternatives to
i31ref", and the concerns are fairly specific to OCaml due its use of specifically 31-bit arithmetic. (Haskell, for example, only guarantees 30-bit precision because they have found reserving 2 bits for GC to be useful.) There is already another very long thread, #53, abouti31ref. Let's not pull this thread into an argument abouti31ref, an argument that cannot be conducted well if we have not even considered the alternatives, which is what this thread is about.rossberg commentedon Jun 29, 2020
Consider a function like this one from the Map interface:
This is polymorphic in two type variables and two abstract types. Each of them could either be an integer or something else. So even with just int, that makes 2^4 = 16 different ways in which you'd have to specialise the code according to your suggestion (8 if the compilation scheme allows to fix the export type
t).Or you reduce the number of specialisations, lump several dimensions together, and pay the extra cost of frequent boxing/unboxing conversions for the rest. Many variations of this have been investigated in highly polymorphic languages, and there are reasons few are used in practice. Leroy (among others) actually had a line of papers researching unboxing for polymorphic languages, yet OCaml, like many similar languages, uses a uniform representation instead. He observed that the conversion overhead often is higher than the benefit of unboxing, and that a minimal unboxing scheme produces better results for typical profiles.
I'm not intimately familiar with how OCaml's native code compiler handles currying, but the byte code compiler handles it through clever dynamic stack techniques that are unrelated to compile-time unboxing.
rossberg commentedon Jun 29, 2020
No, they're not, and it's getting a bit boring at this point that I have to keep refuting that. So again: plain ints are just one particular use case. The general purpose is unboxing small scalars of any sort, for which there are many, many use cases, in many, many languages. (And of course, 30 bit ints would fit just fine as well.)
jayphelps commentedon Jun 29, 2020
As a bystander who follows these threads I wanted to share some feedback that this aggressiveness is quite off-putting.
RossTate commentedon Jun 29, 2020
Unfortunately, WebAssembly does not support these techniques. So I believe coercions will be necessary instead. @sabine, have you considered this problem with currying?
To clarify, I was not suggesting specializing polymorphic functions based on their type arguments. I was suggesting coercing monomorphic functions that work specifically on integers to polymorphic functions operating on the uniform representation (when used polymorphically). This could be done with an augmentation of the coercion system I was suggesting above for dealing with currying.
rossberg commentedon Jun 29, 2020
Oh, okay, but that's probably even worse. You would have to box/unbox in all first-order contexts where you pass a value from polymorphic to monomorphic functions and vice versa, which is like all the time. And you'd still have the same problem: for a function of type int->int->int, there are 8 different possibilities of a polymorphic context to which it could be passed in a higher-order fashion. All would have a different calling convention under the approach you suggest. So either your function has 8 different specialisations, or you're creating wrapper functions at each polymorphic use site.
aardappel commentedon Jun 29, 2020
@sabine thanks for summarizing things so clearly.
First, until we invent JIT and other features, the current Wasm, even when including the current GC proposal, is an entirely static, dare I say "closed world" system, with AOT compilation to a single
.wasm, with concrete types at the boundary. By definition, all forms of polymorphism, modularity, separate compilation etc will be resolved by the time that.wasmgets baked, no matter what the language likes to present beforehand. This alone should make (5) by far the most realistic option for most languages that care about speed, again, in the current way Wasm works.There seems to be a lot of default fear of (5), that it going to result in unreasonable code bloat. If you generate specializations under a closed world assumption, you only generate the ones that are actually used. This causes more in-lining of single-use functions (not further increasing code size) followed by much more aggressive optimisation of inlined code working directly on specific naked (scalar) types, often shrinking the code back down significantly. It turns indirect calls into static calls, helping greatly with dead-code elimination. LLVM and binaryen can do endlessly more on code like this than code that stays generic. In my personal experience working with (5) a lot, the amount of code bloat can be remarkably small, and the resulting speedups due to code "collapsing" impressive.
But @rossberg is correct that anyone that prefers (5) can make this happen, regardless of the presence of
i31ref. So why would we wanti31refadditionally?I'd like to see an example of polymorphism that is impossible to compile down to specialized code using (5), and requires a struct of all
anyrefto work. I think, by definition, that such an example does not exist (given that we don't have any dynamic code features in Wasm that would make this statically impossible).There will be people who have code size as their #1 concern, and don't believe my story above that it can actually help reduce code size, when done right. That's fine, because it is hard to make claims about this in the absence of specific languages with specific compilers and specific applications.
Finally, are there downsides to having
i31refwhen you don't use it? Under what circumstances does a Wasm implementation have to check "is this thing ani31ref?" before accessing a pointer? If anyref is always a pointer, always pointing to a heap object where the initial bytes always have the same layout (e.g. a type id), I can imagine that would lead the speedups. If indeed there are frequent checks required fori31ref, that to me would be the strongest reason to not wanting to have it.rossberg commentedon Jun 29, 2020
@aardappel, repeating what I pointed out above, and I believe many times before: static type specialisation does not work in a language with first-class polymorphism or polymorphic recursion.
Simplest possible example in OCaml:
There is no way you can statically specialise anything here, since you don't know what polymorphic definition you are calling. It's completely dynamic. A client of
gcould pass anything forf, it could be taken from a list, or any dynamically computed data structure. Inversely, the site defining anfgenerally has no way of knowing where it flows and what types it will be used at.The same is true for generic methods in a language like C#, which is why the CIL performs jit-time type specialisation. And it is the reason why C++ does not allow "template virtual methods", because its simplistic compilation model cannot support them.
Static specialisation also does not work in a language with polymorphic recursion, because the set of instantiations can be statically unbounded.
If I could make a wish, then that folks in these discussions could accept the fact that there are very good reasons why certain classes of languages are implemented in certain ways, that there has been decades of engineering and research into it, that we are not smarter than all the many folks who did this, and that we need to support a mapping for those compilation techniques instead of hoping for some yet undiscovered trick to make these requirements go away. Please?
82 remaining items
gasche commentedon Jul 13, 2020
@RossTate we could think about having, for example,
(tagged structref funcref). In other words, maybe the strategy of exposing tagging structures in types can also fit your favorite needs.My impression is that
(ref i31 <reftype>)and(tagged <type> <type>)are interesting "alternatives to i31ref" (they feel much more realistic to me than monomorphisizing everything), and they would each design a dedicated PR to expose a design and foster further discussion.(Again, the
primrefPR should really write its design down to summarize the #94 discussion. This would also help with discussing(ref i31 <heaptype>)and(tagged <type> <type>)in the context of that lower-level proposal.)RossTate commentedon Jul 13, 2020
@gasche, you seem to be describing a heterogeneous system, meaning there is no global tagging structure. But then you need
structrefto say what kind of reference fields it has, e.g.(tagged structref funcref), but then the structrefs in those field presumably have the same tagging convention, leading to recursive types. See this proposal for how to carry that out.We're working on it, but it will not have a
structreftype that is parameterized by the type of its reference fields. One of the benefits of the proposal is its simplicity, and furthermore that complexity doesn't solve problems without introducing recursive types (see above).gasche commentedon Jul 15, 2020
Out of curiosity, I manually boxed an OCaml program performing integer arithmetic, in order to evaluate the performance overhead of systematic integer boxing on the current runtime. (The function I used is what I call "sumtorial", like factorial but with a sum instead of a product, basically a complex way to complex n*(n+1)/2.)
boxed_sumtorialis an exact translation of thesumtorialdefinition, with boxed integers instead of unboxed integers.locally_unboxed_sumtorialis a version where I manually inlined the arithmetic constants and operations and applied local boxing/unboxing simplification.On my machine, the
boxedversion is precisely three times slower than the unboxed version. Thelocally_unboxed_sumtorialis not much faster thanboxed(3.6s rather than 3.9s on a test), because for this program it only eliminates redundant unboxing, there is no redundant boxing that can be eliminated locally. (Of course we could change the calling convention of the function to take unboxed integers, but this is not a local transformation anymore.)jakobkummerow commentedon Jul 16, 2020
Quick update: as of just now,
i31refas described by the current proposal is fully implemented in V8's prototype, so you can use that for any experimental performance investigations/comparisons.In case anyone has a demo where they suspect that significant (or at least measurable) time is being spent on taggedness-checks even though the demo doesn't use i31ref, it would be simple to create a custom build (or introduce a runtime flag) to turn off i31ref support and verify this suspicion. I won't do that until/unless someone asks me to though :-)
(Disclaimer: this is not a statement of opinion on whether i31ref should exist in the spec, or in what form. I find the flexibility of a generalized form like
(tagged i31 anyref)appealing; I also like the simplicity of the current proposal (and believe that it doesn't incur an unreasonable cost at runtime); and I'd also be fine with not having it at all -- we can always add it later if needed.)sabine commentedon Jul 17, 2020
Ok, if I get this right, you say that having
tagged i31 anyrefin the spec prevents the engine from using the lower (unused) bits of a heap pointer.I see two cases here:
tagged i31 anyref, this is true. 32 bits are used, there is no way for the engine to embed additional information about the heap pointer in these 32 bits at runtime.anyref, the engine is still free to use lower (unused) bits in any way it sees fit.Since we cannot type cast between
tagged i31 anyrefandanyref, at any point in program execution, the engine knows exactly (thanks to type annotation of the WASM program) which of these representations it is dealing with.It looks to me like there is no difference for the GC walking the heap, when adding
taggedto the current MVP proposal. It already needs to know the type of the heap struct it is walking, in order to know which fields may be pointers.So, there are three entities here:
anyreftagged i31 anyrefIs there anything that prevents the engine from assuming different semantics for the implementation of
tagged i31 anyref(cannot store information in lower bits), andanyref(can store information in lower bits)?It looks to me like adding
taggedinstead ofi31refcan only potentially bring a disadvantage to the users oftagged(who are likely to be willing to incur that because it enables reusing their existing compilers), but not to those who only useanyrefand notaggedrepresentations. In contrast,i31refcomes at a cost to everyone, but is less complex.sabine commentedon Jul 17, 2020
@gasche
This
tagged-idea could in theory even be taken further:tagged (ref $A) (ref $B) (ref $C) i30tagged (ref $A) (ref $B) (ref $C) (ref $D) (ref $E) (ref $F) (ref $G) i61RossTate commentedon Jul 17, 2020
So what you're describing is a heterogenous approach. Unfortunately, the current MVP is designed around a homogenous approach. For example, its compilation model for imported/exported types is designed around a universal representation. As such, you would not be able to use
tagged i31 anyrefas imported or exported types (without effectively changinganyrefto bake ini31ref).But if you want to go with a heterogeneous approach to enable custom low-level representations, then it's better to take things further than just
tagged. For example, right now the coercion fromtagged i31 anyreftoanyref(with specialized low bits) would require a memory read to determine the low-bit information that was omitted fromtagged i31 anyrefto make room for the unboxed scalar. To prevent these kinds of efficiencies, you want a coordinated tagging scheme for your types. That's what the SOIL Initiative's proposal does.For completeness, I should note that
tagged i31 anyrefcan still have a cost even with your clarifications. Note that the coercion fromtagged i31 anyreftoanyrefrequired a way to look at the heap contents to determine the low bits. That assumes the heap has that information. But some garbage-collection implementations don't put meta-information in the heap at all, at least for common small objects, relying on the fact that the meta-information is always available in the low-bits of the pointer. For example, if we were to addi32refandi64ref, instead of a wasting a word in the heap for each of these objects just to say that they arei32refandi64refrespectively, an engine could make sure that the low bits of every pointer to these objects tracks whether they arei32refori64ref(or other). But it can't do that iftagged i31 anyrefexists, because there's not enough room in the low bits to track that information, forcing these values to take more information in the heap. (Since afuncrefis a pair of a code pointer and a module-instance pointer, it too might be treated as a common class of small objects.)sabine commentedon Jul 20, 2020
Thanks for bringing up the type imports spec, I hadn't read that in all details yet. 👍
Okay, the type imports proposal says "As far as a Wasm module is concerned, imported types are abstract. Due to Wasm's staged compilation/instantiation model, an imported type's definition is not known at compile time." (https://github.com/WebAssembly/proposal-type-imports/blob/c9700ff6267571f4a52151c8a46e800f8534f923/proposals/type-imports/Overview.md)
One paragraph later it says "However, an import may specify a subtype constraint by giving a supertype bound with the import"
If I understand this correctly, this means that all type imports/exports must be subtypes of the type
any("the type of all importable (and referenceable) types"). So, we generally cannot import/export any value types, only reference types.Okay, now looking at this from a practical perspective for OCaml: why would we need to import/export types for
tagged i31 anyrefin the first place?When importing something like a global, a function, or a table, I don't need to have a nominal type that wraps
tagged i31 anyref, I can usetagged i31 anyrefas a type directly, just likei32and the other value types, right or wrong?I could still import/export
structorarraytypes that containtagged i31 anyref, right or wrong?You mean, some garbage-collection implementations don't put meta-information in the bit-representation of the small object, but instead they put meta-information in the bit-representation of the pointer pointing to the small object?
How does that work with arrays of
i32refandi64ref, or structs that contain them? I see that, for an individualref i32refor aref i64ref, an engine could store in the pointer whether there is a 32-/64-bit scalar or a pointer stored in the pointed-to heap location, but for arrays or struct fields, the engine must store that information somewhere else?I understand that as "The SOIL initiative proposal aims to give the producer a way to influence the tagging scheme the engine uses." To achieve that, unavoidably, there is some added amount of complexity that the current GC MVP does not have (in particular, it adds complexity to the engine implementation while producers get to pick what they need from the spec). Is there potential for simplifying the SOIL initiative proposal, or do you think it cannot get simpler than it is?
Can you give feedback on the scheme I sketched in #100 (comment)? I was confused about some things, in particular, how to represent arrays. Is
casesmeant as a hint for the engine to use a tagging scheme to represent the different cases?titzer commentedon Aug 6, 2020
@RossTate I believe that forcing imported/exported types to be ref types (by requiring they are deftypes) is a stopgap to avoid the whole polymorphism issue. I believe strongly that we should not preclude true parametric polymorphism in that proposal but should design for the most general case of importing types of unknown representation. That is necessitated by embeddings that need to reference and manipulate values of host types that do not fit into Wasm's type system at all.
What about making
i31refa type constructor instead of a type? We already haveref Tandnullref T, why noti31ref T?RossTate commentedon Aug 14, 2020
Oops, so sorry @sabine! I let this get buried.
Well ideally a WebAssembly built with OCaml would be able to export its values for others to use. If, say, you built an efficient hashmap (as a toy example), then you'd like to export your hashmap type abstractly in WebAssembly (just like you would in OCaml) along with WebAssembly functions for operating on your hashmaps (just like you would in OCaml).
Yep.
There's a wide variety of techniques, and which one a GC can use depends on the invariants it can maintain. Some techniques are to always use the same tagging convention throughout the system (this is what OCaml does). Another technique is to have a descriptor at the top of any struct/array informing the GC how to work with that structure. That descriptor can be encoded as bits in a variety of ways, or point to some meta information (or even a code address to jump to).
Yep. We've been waiting for there to be a proper discussion and round of feedback to determine which simplifications we should explore.
Ah! Even more sorry! You went through all that work and I somehow missed it entirely. I'm happy to give some feedback. Also, note that we did a case study on how to do typed functional languages here. It's handling of closures is a little outdated though, since the call-tags proposal now provides a better way to deal with the complications caused by currying.
Instead of using
$taghere, I would recommend havingvaluedeclare(extensible (cases ...))with all the various cases of reference values you have (which should each be marked ascastable). The proposal will then take care of encoding these as a tag for you, whether in the pointer or in the metadata on the heap. You'll still want$tagintuple<n>though, for distinguishing between cases of an ADT.For arrays, you use
indexedfields. For example,Besides that (and insignificant syntactic things), your sketch looks good to me.
sabine commentedon Aug 21, 2020
@RossTate Thanks for the feedback! It does look like things are representable in the proposal.
Instead of RTTs, there are schemes, and it is possible to test whether a gcref belongs to a certain scheme, similar to being able to test whether a reference has a certain RTT.
It seems nice, from a language user's (someone who writes code in a language that compiles to WebAssembly) perspective, to have the ability to create and link modules that expose opaque values and functions that operate on them.
I don't know, if the ability to express and handle these opaque types" must or should be provided on the WebAssembly level. It looks to me like it is still an open question whether WebAssembly should provide all this infrastructure, or whether languages should invent their own abstraction on top of WebAssembly for this.
The only place where I so far know about built-in opaque values being strictly necessary is in the case of WebAssembly system interfaces, where, for security reasons, handles to operating system resources must not be modifiable by a WebAssembly program.
I would guess that, if one could get many different languages to represent their heap in that model, it should be possible to quantify which of the features are not used (or so rarely used that it doesn't make sense to implement them in a MVP).
If there was an interpreter implementation of the SOIL initiative model, that would enable some experimentation. However, that's a fairly large effort, even though the performance of the interpreter is not important at all.
Another way to look at the model is to look at every piece and try to answer the question "what would be
the effect on performance if this is removed".
Update overview for the zero-byte OOB case (#100)
Remove type annotation on ref.is_null (#100)
tlively commentedon Nov 1, 2022
@eqrion, you may find the opening post of this issue interesting, especially the part where @sabine says that compiling OCaml to linear memory would be preferable to having to box everything if i31 were not available.
Apart from that, most of this discussion is outdated or could be more productive in new issues now that we have folks actually working on compiling OCaml to WasmGC, so I'll close this issue.