Design: Tagged GC Pointers

Created on 17 Dec 2016  ·  41Comments  ·  Source: WebAssembly/design

I noticed in the GC doc that there is no mention of tagged pointers. I wanted to make the case for it. When compiling languages that might heavily rely on tagged unions (such as Rust or OCaml) with GC interop you will have to store the tag as a separate field which can be quite inefficient.

Even as part of the initial proposal for opaque reference types, it would be nice to expose a way to allow a tag to be associated with the reference.

I realize that not all architectures or VM implementations will be able to take advantage of this and might even need to box in some cases. The available space for a tag may also already be used by the JS VM to store its type information. However, for environments that can take advantage, it would be very helpful to have the appropriate affordances.

I could imagine this being related to a possible JavaScript enum proposal as well (which I'm interested in exploring in TC39). That could be the JavaScript representation of this value.

managed objects

Most helpful comment

Hi @shriram, rest assured that we are thinking beyond C++ and do not consider these features as something weird or unimportant. Supporting them is on the post-MVP agenda. We might not necessarily add general call/cc, but more confined forms of delimited continuations are not unlikely. One possible direction might be algebraic effect handlers -- we already have a proposal for structured exception handling, and adding resumption would perhaps be the "easiest" way forward to providing a structured form of "stack switching" and enabling all the control abstractions you mention.

All 41 comments

You can do this already in the linear memory, just mask the pointer before use. I have been battling for years now to get this use case considered, and could use support, and I have a plan.

@sebmarkbage do you have an outline for the enum proposal?

For tag: would you rather have the VM give you a property "N bits of tag available" and anything above you do handle yourself, or auto-magically use side-storage if you exceed an unspecified limit?

@sebmarkbage, we are thinking about the possibility (although you probably don't primarily mean tagged pointers but tags in object headers, plus tagged integers for plain enums). It is gonna be rather tricky, though, because of the diversity of VMs and architectures, and would essentially require making both sums and products primitive in the Wasm type system. Whether that can work out is hard to say at this point.

@jfbastien I'm not sure yet whether it would be better to be in control or have it built in. Now I also think that it might be better to use object header tags than tagged pointers in some cases. Generally I like being in control as an author but in this case you might have to ship a lot of extra code for each branch so I think I'm leaning towards automagically use side-storage.

I can't seem to find the enum proposal online. It was part of value types but I think we can do it decoupled from value types. cc @littledan

@rossberg-chromium I did mean tagged pointers but now that you mention it, I can see how tags in object headers might be more useful in some designs like V8. I could see it being useful to pick one or the other depending on platform. Tricky indeed.

@rossberg-chromium Could you please be more specific when you use the term "we" as in "we are thinking about" in future. As far as I am aware no thoughts on this matter have been under discussion by this community group and I don't want the work of this community group misrepresented. For example you may wish to word this as 'The v8 team' in future or to create a name for a subgroup giving the membership. If the Chairs have allowed a private discourse to continue that has driven the direction of this groups work without keeping the members informed and in control then could they please hand back their Chair. If there are Chairs not elected by the members then could they please hand back their Chairs.

I'd suggest pursuing tagged pointers in a way that's decoupled from value types or typed objects. For example, you may want to restrict to 2-4 tag bits here, whereas an ADT added to JavaScript should not surface such a restriction. cc @tschneidereit

When @rossberg-chromium talks about "we" he means in informal settings amongst people working on WASM. Considering that I sit 3 meters away from him, it seems like a lot of overhead to establish specific subgroup to bless this communication :-)

I think tagging is less general than sum types. Tagging is also unfortunately tied to the underlying machine's word width in bits. However, unrestricted sum types would leave their value representation completely up to the engine, which some language implementors may be uncomfortable with. So maybe restricted sums (e.g. only over some subset of the primitive types and references) might lead to an efficient value representation without a lot of complexity in the engine.

@titzer Coincidentally, "we" (@lars-t-hansen, I and some other wasm folks) were discussing the same basic idea of some kind of restricted sum that always allowed a tagged-word representation.

What fell out of a conversation this summer was the idea that in a design where there's a separate heap of gc-able objects (TypedObjects, probably) there might be two new wasm types:

A "Pointer" is an opaque reference (maybe null) to some gc'd thing in the gc-able heap. It is 4 bytes on Wasm-32 and 8 bytes on Wasm-64.

A "Box" is a Pointer-sized triple:

  • pointer designator bit (1 bit)
  • tag bits (2 or 3 bits, could be wordsize dependent)
  • value (a Pointer if the pointer designator is set, otherwise an integer)

Some plausible operations:
boxptr(pointer, tag) -> Box[1, tag, pointer]
boxval(integer, tag) -> Box[0, tag, integer & ~15]
ptrbitof(Box) -> 0 or 1
tagof(Box) -> tag
ptrof(Box) -> Pointer or null if the box is not a pointer box
valof(Box) -> integer value or 0 if the box is not an integer box

Local and global variables in wasm would be box-typed or ptr-typed in addition to current types. Pointers and boxes are not storable in the linear heap.

What I've sketched should be appropriate and performant for many dynamic languages, at least; in practice, 3 tag bits (pointer bit plus two bits) is usually OK, additional tag bits can be stored in the objects after that. The pointer designator and tag bits are stored in the low bit of the word.

It might also be nice to allow an integer box to be reinterpreted as an integer value, this is a no-op if the wasm implementation can prove the box is an integer box, and ditto, to allow an integer to be converted to an integer box by the wasm implementation just masking off the pointer bit. This probably requires pinning down which bit is the pointer bit. Anyway, this provides wordsize-1 bits for integers, the "tag" is just a bit field within the larger value.

@lars-t-hansen I think type test operators are needed so the code could hoist a if(is_int(p)) ... and then the compiler would know that p is tagged as an integer and not a pointer and could optimize valof() without the compiler having to do the hoisting. I don't see a need for ptrof() as the runtime needs to check the type on uses anyway and an integer is just another type to test.

Not sure about calling the tagging 'boxing', as 'boxing' often means allocating in a heap box.

There is an opportunity to also test or mask the high address bits, at the same time that the low tag bits are masked, proving that the index is within the linear heap. I've been battling to get this recognized for two years now, it needs more attention to the type derivation improvements which are currently a P5 in the Mozilla code, could you give that some thought please.

@wllang, yes, I left off type test opcodes because I had not worked that out in my head yet, in particular, whether there would be any performance guarantees associated with such tests. is_int(x) is equivalent to eqz(ptrbitof(x)) which is not a terribly hard pattern to recognize, for starters; is_ptr(x) is just ptrbitof(x). Since it's a stack machine there's a fine line indeed between a hypothetical one-byte operation "i32.isint" and a two-byte operation comprised of two unary single-byte ones, "i32.ptrbit; i32.eqz".

But the exact form does not matter, what matters is the effect. It seems clear that within "if(is_ptr(x)) { ... } else { ... }" the compiler may assume x is a pointer in the then block and an integer in the else block, if it can prove value equivalence of x within those blocks to the x in the test. This does not seem hard for an SSA-based system. But would you require the omission of a tag check within those blocks?

Supposing we allow a bytecode to result in two values; abusing syntax, an operation ptr_i32.ptrof would return a pointer (or null) and a boolean. Then there wouldn't be any question about what the compiler would have to "know", it would just have a value which is a pointer by construction. (There's also a question of a type system for these typed objects and in what sense pointers are typed, but I think that's probably orthogonal to tagging.)

(Digressing slightly, there are additional interesting ideas about whether arithmetic and bitwise operations should be allowed on possibly-int boxes with certain simple tags, with some sort of status code return, comparable to the tagged add instructions on SPARC. i32x2.add k, a, b would result in (a+b, 0) if both a and b are int boxes with lower k tag bits == 0 and there is no overflow, otherwise (garbage, 1). i32x2.or k, a, b ditto a|b. These operations are hard to express efficiently at the wasm level but easy to implement at the hardware level. But it's possible these are too specialized to be worth it -- the case would have to be made that it's interesting to target Lispish languages to wasm.)

As for high address bits... I don't know what you've written about this before. Do you want to implement some type of BiBoP scheme or are you wanting to use the high bits for tags?

(Reference to the mozilla bug you're mentioning?)

the case would have to be made that it's interesting to target Lispish languages to wasm

If you think that WebAssembly should not attempt to support all programming languages in an effective manner, I think you're underestimating Wasm's importance. Done right, Wasm will take over the world. Wasm will decrease the lock-in power of operating systems, of CPU architectures, and (I hope) of programming languages. Everybody is going to use it, for offline and online apps alike, and for client and server alike.

That world should not be one in which certain programming techniques and programming languages and are doomed to be inefficient and impractical because the WebAssembly designers thought those languages were not "important" enough to support. A lot of the things we take for granted now, as well as features of new languages, started in obscure academic languages and/or Lisp.

Our goal should be to support all languages and all programming techniques with decent performance, say, always within 2x memory and CPU time of the optimal implementation of a given technique. For starters, a high-performance Wasm implementation should be able to efficiently host another high-performance Wasm implementation or the JVM or CLR running inside it (all of which would leverage the outer Wasm engine for code generation), or a Lisp VM or a Stackless Python. Beyond all that implies, I'd like to see continuations (or at least some sort of microthread system), "first-class" labels for fast gotos within a function... well, basically everything in Future Features and then some. While the work has to be prioritized, let's not forget that all of it is important.

@lars-t-hansen I'd love to get a better grasp of your design ideas for the GC/opaque heap (as well as everyone else's ideas.) What would the effect of the tag bits be in relation to the WebAssembly VM?

How do we make an "all purpose" VM? something that

  • Supports precise, compacting, generational GC
  • Supports array slices, and pointers that refer to the middle of an object or array, not just the beginning
  • Supports object systems based on both "thin" and "fat" pointers - or both! (thin pointers rely on type information stored in the object pointed to, while fat pointers store that information in an extra word or two alongside the pointer)
  • Allows "maybe pointers" that can dynamically switch between being pointers and integers
  • Supports union types efficiently (pointer to A or B or C - it must be one of those, but which one is dynamic)
  • To the extent possible under our security constraints, allows client control over memory allocation and GC.
  • Supports opaque references - even though this seems to be in tension with the general goal of putting Wasm client code in the "driver's seat" such as letting the client perform its own GC algorithms
  • Supports other stuff I haven't even thought of

Disclaimer in that this comment isn't particular to the issue in OP.

If you think that WebAssembly should not attempt to support all programming languages in an effective manner, I think you're underestimating Wasm's importance. Done right, Wasm will take over the world....

I don't think "take over the world" is a great goal to shoot at for Web Assembly (literally and figuratively 😉). Standards are moving more quickly in the world now that a majority of browsers are evergreen, and Web Assembly as it is, is designed to fill a gap in those standards, in a quickly serializable and transport-optimized instruction set for processor-sensitive applications. Focusing on fulfilling that requirement as fully as possible by sacrificing some niceties that would make handling languages that require runtimes and garbage collection easier seems like the reasonable thing to do for me, given that newfound malleability is allowing us to more easily approach new problem domains more quickly and improve our solutions to existing ones as collective insights and experience accumulates.

I don't personally think WebAssembly doesn't have the wherewithal to fulfill these grandiose breadth of applications; I personally grew interested in WASM from the perspective of the concepts of non-browser-oriented internet-based virtual applications that could be shared and enjoyed safely like html and css, without the attachment to the document publishing reality of web browsing. Particularly, I don't think that WebAssembly's core obligations completely remove it from discussion that progresses the possibility of an ISA that fulfill all these kinds of possibilities, whether or not that ISA would be WebAssembly for your given problem domain.

I don't think "take over the world" is a great goal to shoot

Just my prediction. I'm not saying taking over the world is the goal - I'm saying WebAssembly is uniquely positioned, such that there's a good chance it will take over the world without anyone specifically trying to make that happen.

So, what would the effect of the tag bits be in relation to the WebAssembly VM? What are they for?

I extracted my comments from above and elaborated a bit, so that we can have a place to watch the ideas evolve without having to scan this entire thread every time:
https://github.com/lars-t-hansen/moz-sandbox/blob/master/wasm-tagging.md

That writeup also has a little bit more of a rationale for "why tagging?" and some comments on security, prior art, etc.

@qwertie, I don't want to hobble Wasm or prevent it from supporting any particular language. But we should probably acknowledge that some source languages are more important than others in the context of WebAssembly, and also that supporting some languages well will require a large effort. For Scheme we'll need closures, tail calls, first-class continuations, dynamically typed object fields, dynamically typed variables, and efficient generic arithmetic. Some of these can be implemented from simpler structures, at acceptable cost. Tail calls and continuations can be simulated by pushing all frames into the gc'd heap and/or onto a manually managed stack, but you probably don't want that, it's likely too expensive. So you'll need native support. But if Scheme is the only use case for call/cc I think it'll be an uphill battle to get acceptance for it; Scheme is, after all, a dead language.

The tagging discussion is kind of fascinating because thus far, the discussion on our side of the fence has been about taking something we must have (pointers into an opaque heap) and adding a cheap feature that many find useful and well-performing and portable (low-bits tagging) that then opens up for one more idea (storing non-pointer data in the pointer part of the tagged thing). This goes a long way in supporting Lisp and Prolog and some other languages.

@sebmarkbage really asked about a more general tagging scheme, and the design space is quite large, but IMO we should be driven by use cases, and the use cases are probably particular implementation techniques that have proven their value, for languages that are broadly enough used to "matter". (And for languages that remain unsupported by that strategy, there's always flat memory and roll-your-own GC, if you want those languages to run on the Web.)

The tagging discussion is kind of fascinating because thus far, the discussion on our side of the fence has been about taking something we must have (pointers into an opaque heap) and adding a cheap feature

I disagree that pointers into an opaque heap are feature that WebAssembly must have. I can think of three possible directions for WebAssembly's support for dynamic and GCed languages:

  1. Only worry about being a good VM for non-GCed static languages.
  2. Expose a JS-inter-operable typed and garbage collected heap.
  3. Expose the primitives necessary to implement dynamically typed languages (or statically typed GCed languages) in WebAssembly: e.g. JIT code patching, stack walking for garbage collecting linear memory, etc.

The MVP has limited its scope to the 1st direction without committing to it post-MVP.

This issue and the GC design page assume the second direction. In the context of that assumption, exposing tag bits for GC pointers does make sense as a cheap extension to the opaque GC pointers that "must" be supported.

However, I believe the 3rd direction is the best long-term direction for WebAssembly. The 2nd direction seems to be motivated by making it easier to work with the browser DOM from WebAssembly. However, nobody will be writing code directly in WebAssembly, so some language support would be necessary, and that could use a procedural JS API to access the DOM just as effectively as if there was low-level access to the DOM.

The 2nd direction would also allow WASM backends for JVM/.net, or Scheme, or Ruby to reuse the JavaScript garbage collector. However, garbage collectors are not one-size-fits-all: the likely result seems to be introducing a permanent requirement for WASM VMs to include a typed+GCed heap that won't satisfy the performance requirements of many applications.

The 3rd direction requires more work, but in the long-term should both allow simpler standalone WASM VMs and reduce the complexity of browser VMs as more of today's heroic optimization efforts migrate from JavaScript VMs into the WASM sandbox.

I previously created #733 to discuss the relative priority of exposing the JavaScript GC heap to WebAssembly vs exposing primitives for implementing a garbage collector. Perhaps further discussion of the direction of WASM GC can continue there, and this issue can remain focused on tagged GC pointers with the assumption that support will be added to WASM for opaque pointers to the GC heap.

@lars-t-hansen your writeup doesn't mention how it relates to garbage collection, or to the idea of a memory area for opaque references. Is there an underlying assumption that the opaque reference "heap" or "table" would be a simple array of "boxes" (or uhh... "tagged Ptrs"... TPs?) such that any of those boxes could refer to a JS object, which would thus be kept alive by the JS GC?

After looking over the document, it occurs to me that maybe your idea is not to have WebAssembly do garbage collecting on the Wasm side - but merely allow the opaque reference area to hold both references to JS objects (to prevent them from being collected) and other stuff, like integers and internal references to itself. An (untrusted) GC could then be built for WebAssembly that happens to support the opaque reference area.

Am I getting warmer?

About first-class continuations - this is the one feature that I'm not sure we need. Coroutines or lightweight stacks, sure, but continuations? Does anyone actually use them for anything other than to build some kind of fiber/coroutine/thready thing that behaves as a collection of stacks? Maybe it's enough to support some kind of "lightweight" stack thing with a switch-stack opcode. In that case call/cc wouldn't be available under WebAssembly but the things people tend to build out of call/cc would be.

I agree with @AndrewScheidecker, in that a focus on exposing primitives will provide the flexibility to allow all programming languages, current and future, to target WebAssembly and run code efficiently (and to use different garbage collectors for different workloads). On the other hand:

  • I care deeply about interoperability; having a built-in garbage collector could make it easier for different garbage-collected languages to interoperate.
  • Garbage collectors can be large pieces of software, and having to download a different GC from every web site may be burdensome

So I guess I'd like there to be some sort of default GC, but implementing a custom GC in Wasm shouldn't have a big downside over using the default one.

@AndrewScheidecker, fair point - it is not a given that we must have opaque pointers into a gc'd heap. I suggest we continue that discussion in Issue #733 as you propose. In passing, though, I feel that your claim that "The 3rd direction [...] in the long-term should both allow simpler standalone WASM VMs and reduce the complexity of browser VMs as more of today's heroic optimization efforts migrate from JavaScript VMs into the WASM sandbox." needs both arguments and code in its support; it is far from obvious that this is true.

@qwertie, my general assumption here is that there will be wasm locals and perhaps globals of type Ptr and Box; that the opaque objects could have fields that would be statically typed as Ptr or Box; and that the system's GC would trace these fields, having perfect reliable information about their contents. The idea was not - here - to support some type of pluggable GC, which I consider a different type of project. (Again, this discussion started as "can we add tags to opaque pointers?".)

On Wed, Jan 4, 2017 at 2:59 PM, Andrew Scheidecker <[email protected]

wrote:

The tagging discussion is kind of fascinating because thus far, the
discussion on our side of the fence has been about taking something we must
have (pointers into an opaque heap) and adding a cheap feature

I disagree that pointers into an opaque heap are feature that WebAssembly
must have. I can think of three possible directions for WebAssembly's
support for dynamic and GCed languages:

  1. Only worry about being a good VM for non-GCed static languages.
  2. Expose a JS-inter-operable typed and garbage collected heap.
  3. Expose the primitives necessary to implement dynamically typed
    languages (or statically typed GCed languages) in WebAssembly: e.g. JIT
    code patching, stack walking for garbage collecting linear memory, etc.

The MVP has limited its scope to the 1st direction without committing to
it post-MVP.

Yes, and this was very intentional to resist feature creep and ensure
success.

In various contexts what we've discussed is managed data as an _extension_
to WebAssembly (I even consider the name Managed Data Extensions, or MDX,
to be a good candidate). The Managed data extension would allow WASM code
to allocate low-level structs separate from the linear memory that are
managed (i.e. GC'd) by the WASM engine. The structs would be typed to allow
unboxed fields and would be low-level enough to allow implementing the
object model of higher level languages like Java (with vtables, class
pointers, etc), Go, functional languages, etc.

Positioning the managed data types, values, and operators as an extension
would make it possible to have a core WASM implementation that simply does
not support the extension. On such implementations, it would be possible
(though take some work) to implement an automatic translation that uses a
portion of the linear memory (or a separate linear memory) to emulate these
structs and implement a GC, though at a performance disadvantage.

This issue and the GC design page http://GC.md assume the second
direction. In the context of that assumption, exposing tag bits for GC
pointers does make sense as a cheap extension to the opaque GC pointers
that "must" be supported.

However, I believe the 3rd direction is the best long-term direction for
WebAssembly. The 2nd direction seems to be motivated by making it easier to
work with the browser DOM from WebAssembly. However, nobody will be writing
code directly in WebAssembly, so some language support would be necessary,
and that could use a procedural JS API to access the DOM just as
effectively as if there was low-level access to the DOM.

The 2nd direction would also allow WASM backends for JVM/.net, or Scheme,
or Ruby to reuse the JavaScript garbage collector. However, garbage
collectors are not one-size-fits-all: the likely result seems to be
introducing a permanent requirement for WASM VMs to include a typed+GCed
heap that won't satisfy the performance requirements of many applications.

The 3rd direction requires more work, but in the long-term should both
allow simpler standalone WASM VMs and reduce the complexity of browser VMs
as more of today's heroic optimization efforts migrate from JavaScript VMs
into the WASM sandbox.

I previously created #733
https://github.com/WebAssembly/design/issues/733 to discuss the
relative priority of exposing the JavaScript GC heap to WebAssembly vs
exposing primitives for implementing a garbage collector. Perhaps further
discussion of the direction of WASM GC can continue there, and this issue
can remain focused on tagged GC pointers with the assumption that support
will be added to WASM for opaque pointers to the GC heap.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/WebAssembly/design/issues/919#issuecomment-270484689,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ALnq1EpdSRskLiuHx4v5M83T4IS9wmx1ks5rPAgggaJpZM4LPxE5
.

@lars-t-hansen, it's clear that explicit tagging/untagging is needed for integers, but what is the use case for distinguishing boxed and unboxed reference types in the type system? Is there any operation on unboxed reference that could not be provided on boxed references directly? In which case you don't need to introduce the former.

@titzer Re: 'to emulate these structs and implement a GC, though at a performance disadvantage' - if the wasm core using linear memory can not implement an object system including GC without a performance disadvantage then that seems a core design problem.

What is the nature of these "performance disadvantages"?

My interpretation from my experience on these related projects is that the web browser vendors are part of the problem, or that getting agreement is an incredible challenge, and thus that what the community needs is for the web browser vendors to deliver a high performance low level language so that the development can be free from the requirement to have agreement between the web browser vendors. So I appeal to keep focused on that goal for the benefit of the web community, and that managed objects are a distraction from that goal.

I would just remove higher level managed objects and managed GC from wasm, remove it from the future features, focus on the core and ask groups having performance issues with their object systems and GC implementations in the linear memory to report them so that they can be considered in the core design challenge.

We already know some of the challenges for tagged points in the linear memory and paths to address these - the improved integer type derivation I have tried, but so far failed, to get agreement on; and potential advantages for a SSA encoding to make this derivation cleaner; and the benefit of masking the high bits while masking low bits.

I would just remove higher level managed objects and managed GC from wasm, remove it from the future features

I would strongly disagree with this view. Managed objects is not about simplifying the life of compiler writers from managed languages. The main, tangible feature brought by managed memory to wasm in the future would be to allow any kind of interaction with the rest of the JavaScript world (that goes beyond sending arrays of bytes back and forth, that is).

There is no way you can implement (potentially circular) references from wasm memory to JavaScript objects in the linear memory model. Without this core requirement eventually addressed, there is no hope for languages such as ClojureScript, Scala.js, or any other compile-to-JS language featuring decent interoperability with JavaScript to compile down to wasm instead.

Compiling intra-language features down to anything is easy. We don't really need target support for that. Compiling inter-language features is however impossible without support from the target. IMO wasm should eventually give us all the tools to interoperate with the JavaScript object model (and through this common model, between compile-to-wasm languages). And that requires a managed memory model built into the wasm specification (potentially as an extension, if we want to brand it that way).

@sjrd It would be good to get these 'challenges' detailed so they can be addressed. So the show stoppers you seem to raise are:

  • mapping an integer in the wasm sandbox to a JS object, but this seems to be quite possible using a hash table or array, it just remains how efficient this is, and how efficient does this need to be? This is already done in games etc. A more efficient hash that meets the security of pointers might be possible, so lets explore this if necessary.

  • you also raise the issue of circular references so I presume your concern is how the JS GC could scavenge these GC pointers stored as integers in the wasm VM? But what about pointers from JS to objects in the linear memory too?

These issues can not be addressed in general by adding managed objects and managed GC to wasm, unless it were conceded that in general wasm code uses managed objects and managed GC and that would be basically giving up on the core wasm project to morph it into a higher level VM design.

So we could explore some solutions: a JS GC hook that calls into the wasm sandbox to scavenge pointers that might reference back to JS objects and which would have some defined way to communicate these back to the JS GC. This might just be a few defined FFI calls that add no burden to the wasm core design and sandbox. This is the type of thinking that needs to occur, not being tempted to lean on wasm to solve all the problems.

@sjrd You might also look up 'distributed garbage collection' which might be a framework that could address your use case without being a burden to wasm. I am not sure if JS has all the support needed, but there might be more general interest in adding that support to JS if needed, for cloud object stores etc, and that could be done outside the wasm CG. The wasm side would just be integer wasm code, so could be a separate project (or multiple projects if the web browser vendors and/or web community can not agree). Perhaps for many apps simple strategies such a reference counting, weak pointers, and leased pointers would be adequate without the complexity of global garbage collection across wasm and JS instances. Another option might be to see if more of the JS code in these frameworks can be moved into the sandbox - will someone write a JS interpreter for wasm that has adequate performance for misc JS code that is a small part of a larger code base.

@rossberg-chromium,

it's clear that explicit tagging/untagging is needed for integers, but what is the use case for distinguishing boxed and unboxed reference types in the type system? Is there any operation on unboxed reference that could not be provided on boxed references directly? In which case you don't need to introduce the former.

You don't know whether a Box holds a reference or not. That being so, a pointer operation directly on a Box (which is not problematic per se) would have to check the pointer bit and strip any tag. A pointer operation on a Ptr would never need to do that. So from my point of view this was just an efficiency concern.

@lars-t-hansen, ah, so IIUC raw pointers would be typed but boxed ones untyped? AFAICS, that's not the factorisation you would want, because then every access through a boxed pointer would first require a runtime check -- and a boxed pointer is the only thing you can store in GCed memory. So walking a memory graph would require a runtime check at every edge.

I had in mind something slightly different: a family ref(T) of opaque reference types, where T is either a struct/array, a function (i.e., function pointers), or an integer (i.e., tagged ints). In addition, you have a type anyref that is the union/supertype of all refs, and on which you can perform type tests and checked downcasts. That way, you only need a runtime test when you're actually dealing with a union. Moreover, null would be a value in type anyref, but not in ref(T), so that null checks can likewise be isolated and are explicit in the code.

@rossberg-chromium, indeed I have glossed over how I think we would track type information here. I imagined at some point that Boxes could track type information, ie, Box(Ptr) where T is a struct type would yield a datum known to hold either a tagged pointer-to-T or a (tagged) integer. In that case, dereferencing the box directly would still need to check for pointer-ness (it could be an int), and would still need to remove the tag (because the tag has unknown bits set).

Your ref(T) idea is clearly related but does not allow the pointer to be tagged, if I understand correctly, at least not with user-defined tags, which is the problem I was trying to solve with my design.

(+1 on constraining nullptr somehow via the type system, i think this is orthogonal to tags however.)

@lars-t-hansen, I see. Well, I think typing is actually the crux of a tagging feature. Now that I think about it again, I don't really see how you could achieve it without explicitly introducing tagged types or sum types into the Wasm type system (short of some form of dependent types, which I think we want to avoid).

But if we need some form of tagged types anyway then it seems more attractive to fully model tagging at this higher level of abstraction, and not meddle on the lowest level of pointer values. That gives implementations the necessary leeway to store tags in the best way they see fit, and avoids dependencies on both architecture and VM, or restricting tags to the common denominator. For example, for V8, supporting more than 1 additional tag bit for pointers would essentially require reimplementing half of V8, something that we are very unlikely to sign up for -- especially since it would imply a substantial memory cost on 32 bit platforms. On the other hand, V8 would have a lot of ways for storing larger tags efficiently in the map ("hidden class") of a heap value without wasting space for each value.

@wllang

It would be good to get these 'challenges' detailed so they can be addressed.

I agree. I have been planning to do this for some time, but haven't gotten around to doing it yet.

mapping an integer in the wasm sandbox to a JS object, but this seems to be quite possible using a hash table or array, it just remains how efficient this is, and how efficient does this need to be?

IMO it needs to be at least as efficient as writing the same code manipulating heap objects in JavaScript. I don't think we can get anywhere close to this level of performance with an integer map.

Judging from several of your posts here and in other threads, you seem to be very concerned about the performance of the wasm core when it comes to unmanaged languages. Yet for managed languages and their priorities, you shrug off core concerns with workaround solutions that are inherently suboptimal, performance-wise, compared to a deeper integration with wasm. Why should we discriminate managed languages like so?

These issues can not be addressed in general by adding managed objects and managed GC to wasm, unless it were conceded that in general wasm code uses managed objects and managed GC and that would be basically giving up on the core wasm project to morph it into a higher level VM design.

It can be addressed, e.g., if wasm has two memory spaces: the linear memory it has now, mainly used by unmanaged languages compiling to wasm; and a GC'ed heap supporting joint GC with the JavaScript heap, mainly used by managed languages compiling to wasm. Note that a managed language would probably also make use of the linear memory for "off-heap" allocations (e.g., ByteBuffer.allocatedDirect() in Java); and unmanaged languages would probably also make use of the managed memory for interoperability scenarios with JavaScript.

This might just be a few defined FFI calls that add no burden to the wasm core design and sandbox.

I'm not a GC expert, but I have a feeling that "just a few publicly-specified hooks into the GC" might very well be a significant burden on the wasm core design and/or the VM implementation.

Another option might be to see if more of the JS code in these frameworks can be moved into the sandbox - will someone write a JS interpreter for wasm that has adequate performance for misc JS code that is a small part of a larger code base.

This would have a significantly higher overall burden for everyone involved. And TBH, it feels really absurd to implement a JS VM inside wasm when wasm is bound to be linked to an actual JS VM in most cases. Besides, it does not solve the problem of communicating with the browser APIs, which are essentially JavaScript APIs.

@sjrd

IMO it needs to be at least as efficient as writing the same code manipulating heap objects in JavaScript. I don't think we can get anywhere close to this level of performance with an integer map.

Indexing into an array is fast, perhaps a few machine code instruction. A hash lookup is similar on a hit. JS JITs use a lot of caches with tables and tests internally, so perhaps the performance would be characteristic of JS.

Judging from several of your posts here and in other threads, you seem to be very concerned about the performance of the wasm core when it comes to unmanaged languages. Yet for managed languages and their priorities, you shrug off core concerns with workaround solutions that are inherently suboptimal, performance-wise, compared to a deeper integration with wasm. Why should we discriminate managed languages like so?

If you define 'managed languages' then that might not sounds the same :) Wasm should be able to support efficient object systems written in the wasm core code and stored in the wasm linear memory, and this code would 'manage' the object system for other code written in the sandbox, and I have been working to get better performance for this use case for a few years now. So I do not have a bias against 'managed' code in general, I am not focused just on C code performance alone.

The case that you seem concerned about is integration with the objects managed by a JS runtime operating in the same process address space. But lets say that an implementation of wasm wanted to put the wasm sandbox in a separate sandboxed process, then you have already stepped outside the scope of the use case you seem concerned about - and placing a wasm instance in a separate process may be compelling for security and performance.

It can be addressed, e.g., if wasm has two memory spaces: the linear memory it has now, mainly used by unmanaged languages compiling to wasm; and a GC'ed heap supporting joint GC with the JavaScript heap, mainly used by managed languages compiling to wasm.

It does not work because the foreign pointers can not be stored in the linear memory so an object system within the linear memory can not store them and thus would not be practical so it would be forced to implement it's object system using the managed foreign pointers. Sure the code could allocate some buffers etc in linear memory but it could not manage it's own objects in the linear memory that integrated with the foreign pointers.

I'm not a GC expert, but I have a feeling that "just a few publicly-specified hooks into the GC" might very well be a significant burden on the wasm core design and/or the VM implementation.

Defining some imported/exported function names is not a burden on the wasm design, but the burden on the JS GC implementation might be considerable and something to ponder if global GC is a show stopper.

This would have a significantly higher overall burden for everyone involved. And TBH, it feels really absurd to implement a JS VM inside wasm when wasm is bound to be linked to an actual JS VM in most cases. Besides, it does not solve the problem of communicating with the browser APIs, which are essentially JavaScript APIs.

The burden would be much lower for wasm consumers! A much lower attack surface. It might also enable higher performance and more secure implementations (in a separate process). It would also be a much lower design burden, which seems very significant - how much more effort and resources do you expect it takes to get a feature deployed in web browsers (if it is even possible to succeed) versus contracting people to write an implementation of the feature for the wasm core, would you estimate ten times the resources or one hundred times the resources, and what of the chances of success, perhaps one in ten or one in one hundred?

Also perhaps not so "absurd" considering that the wasm instance might be in a separate process, that wasm might be able to link with VM code that might be cached, and that web browsers might bundle in a version and a mechanism to swap it. Imagine being able to user-select or upgrade your JS VM (for use in a wasm sandbox), being able to swap it out for another version for debugging etc. Also keep in mind that this is also about opening up the ecosystem, unbundling JS to some extent, so people might include a tiny-basic interpreter or micro-python or LUA or CIL VM or JVM etc. How heavy is a simple JS interpreter anyway, if it is relatively heavy then perhaps other frameworks will show benefits anyway?

I would support the lower level JavaScript APIs being made available to the wasm instances without the needing to deal with JS objects, more like an OS layer, and that seems quite possible.

I would also support the wasm encoding being flexible enough to express the JS VM that you wish, which does not seems so great a burden and would help future-proof wasm anyway. I just wonder if I will live to see a high performance low level VM on the web.

@rossberg-chromium

On the other hand, V8 would have a lot of ways for storing larger tags efficiently in the map ("hidden class") of a heap value without wasting space for each value.

Fwiw it does sound a little odd suggesting storing pointer tags in a "class" object because the pointer tags being discussed here are per-pointer not per-class, perhaps v8 does store pointers in more than one word but that would be a surprising and interesting design? It would also not be possible to store these tags in the object that the pointer was pointing to because the object might be an immediate value encoded in the tag bits.

@sjrd Just to establish that the GC issue has solutions raised in this forum (patents) here's one solution. The JS GC adds a new table that maps between JS pointers and integers (wasm side pointers). It might maintain the wasm side of the table directly in the linear memory, so the JS table references an array buffer and an range. On each side there is a live flag and a reference count per pointer. When a pointer is added to the table the live flag is set and the reference count set to one on that side of the table. When the GC on either side runs it defers scavenging these tables until last, so at that point it knows if these pointers on the respective side have live pointers from that side from outside the table, and it updates the live flag for the respective side. If the live flag is clear and the reference count on the other side is zero then the pointer is reclaimed and removed from the table. After handling all these tables the GC continues to scavenge the live pointers keeping anything they reference live. On the JS side, these pointers are either write-only or become unreadable once flagged as not live on the JS side - the wasm side GC would be expected to follow similar semantics (the linear memory holding the table is still readable but the code enforces the object system invariant that they are not readable). After a GC on either side, an async call is triggered if there are pointers flagged as not live, this can call into the other side to update the reference count and when it becomes zero the pointer will be reclaimed on the next GC. These tables could be used for global GC for object stores outside the same thread and there might be much more general interest in such a feature among the JS community.

@wllang, maps / hidden classes are a common implementation technique for tracking the representation of individual values; they are only loosely related to language-level notions of class.

@rossberg-chromium From what I understand of hidden classes in v8 they are stored as a slot in the object. This PR is about tagged pointers so even storing the tags in a separate object slot does not seem relevant, little lone storing them in the hidden class, rather they need to be stored in the pointer thus the implementation could only be free to decide which bits of pointers to use. Or did you have something else in mind that I just have not understood. Perhaps these slides help others http://thlorenz.com/talks/demystifying-v8/talk.pdf

@wllang, what I said is that it likely doesn't make sense to expose _where_ tags are actually stored, because architectures and VMs have severe and wildly different constraints. Tagging support for Wasm would need to be sufficiently abstract to allow multiple implementation strategies.

Included in the GC proposal (https://github.com/WebAssembly/gc/) as a possible extension.

Hey @lars-t-hansen and @qwertie — I'll be the outlier here. Continuations are the one feature that get no respect; and then, people end up having to reinvent them the hard way. Once you have them, you automatically have coroutines, microthreads, generators, goroutines, what have you, for free. _And_ you have a primitive with which to build even more things you didn't earlier imagine; for example:

  • Pyret's reactor feature and DrRacket's big-bang feature give you a first-class event-loop abstraction in the programming language. You need some kind of continuation-like support to implement it in the browser.

  • DrRacket's send/suspend feature (since mimicked by the Smalltalk Seaside framework and a bunch of others) — for server-side Web programming in direct style — requires almost-entirely continuations and is trivial to implement if you have them.

Note that neither of these is call/cc itself — it's an unrelated, nifty programmer abstraction that just happens to be easy to build with continuations and impossible to build without.

The implementers of both WeScheme [http://www.wescheme.org/] and Pyret [https://www.pyret.org/] have paid the price for the lack of it massively. Both painfully, slowly, slog to get it by program transformation, to enable these kinds of features, with a massive performance penalty _and_ complexity overhead.

If you're building your own _user-facing programming language_, it's entirely reasonable to say "I decree that there shall be no non-local transfer of control". If you're building an _assembly_, I feel you gotta' think beyond "continuations are weird and only Scheme has them anyway and who uses Scheme for that matter" and consider the large number of execution control primitives that modern languages have and people keep inventing.

Since the browser folks decided to support neither continuations nor coroutines/stack-switching, it's a moot point, but I am curious - can you describe what these features do, so that I can see why it is not possible to build those features using coroutines/stack-switching as a primitive?

Coroutines suffice for the most part. They would certainly do the trick
here.

Hi @shriram, rest assured that we are thinking beyond C++ and do not consider these features as something weird or unimportant. Supporting them is on the post-MVP agenda. We might not necessarily add general call/cc, but more confined forms of delimited continuations are not unlikely. One possible direction might be algebraic effect handlers -- we already have a proposal for structured exception handling, and adding resumption would perhaps be the "easiest" way forward to providing a structured form of "stack switching" and enabling all the control abstractions you mention.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

konsoletyper picture konsoletyper  ·  6Comments

jfbastien picture jfbastien  ·  6Comments

JimmyVV picture JimmyVV  ·  4Comments

void4 picture void4  ·  5Comments

Artur-A picture Artur-A  ·  3Comments