-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cmd/compile: create GOARCH=wasm32 #63131
Comments
What is the plan for when 64-bit will be finally supported for the browser? Which additional port(s) will be added? What about when WASI starts supporting 64-bit too? |
Thanks for the proposal. SGTM overall. A few questions:
I guess this is probably true. Some supporting links would be great. It is unlikely for the server side to have a plan to go to 64-bit?
Integers are interesting. Wasm support 64-bit integer operations. I assume we'll continue to use those for 64-bit integer operations (Go's
Thanks. |
Hi @cherrymui Since WASI is one of the primary motivators behind this additional port, I'll use it as an example. Referencing https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md, you'll see they (thankfully) are explicit in their integer sizes. The question of the width of the Go The user interacts for this are pretty minimal so we're not worried about it having much an an impact. I'll say that as far as migrations are concerned, people who use Go's wasm support today are having to migrate existing code today because of the use of 64bit pointers today. So when wasm32 is introduced, they'll be able to effectively undo those migrations and go back to doing it the same way they do with all the other wasm compiled code. Coding wise, our hope is that because of the structure of the Go compiler abstracting most of this, it should be a fairly small change. But that change is likely to be diffuse amongst a bunch of places. Next week during Gophercon I'm hoping to do pick up where @dgryski left off here https://github.com/dgryski/go-wasi/tree/dgryski/wasm32/ and see how far it can be pushed forward quickly. That should give us a better idea of how much more is required. |
@eliben I wish I had a better of how long before we see wasm64 uptick in browsers. To be honest, we sort of expected to see it by now. I think that beyond browser's adopting, one of the bigger roadblocks to adoption is the wasm ABI interactions with javascript. Let's assume for a moment that the javascript code wants to pass a string to a function implemented in webassembly. The wasm ABI requires today that javascript know the exact byte layout, in memory, that the webassembly code will see the string in memory. In fact it requires it so much, that the caller has to write the string into the linear memory that is shared with webassembly, and then pass the offset of the value in the linear memory byte array, which the webassembly sees as a pointer. 😓 Put another way, wasm doesn't provide any ABI abstractions between JS and Webassembly code, which exacerbates issues like the number of bytes a pointer takes up because JS is constantly manually constructing these values by hand. |
@evanphx thanks!
Thanks for the link. From the API it is clear that pointer is 32-bit. For integers, it seems the system APIs use explicitly sized types like
instead of using
Yes, for the completeness of the port, we need to decide the size of Go's We also need to determine the alignment of 64-bit types like Thanks for sharing this! This includes some interesting changes that are more internal to the compiler and irrelevant to the system API, such as cf5a3fa . This switches to use 32-bit operations for 32-bit values. Currently in the current Wasm port, the "registers" (for the Go compiler, see the original design doc) are 64-bit. Do you plan to switch to 32-bit "registers"? A mixture of 32-bit and 64-bit (which seems more complex to me)? Is there a clear benefit for changing the internals to 32-bit? I assume 64-bit operations are just as efficient when the Wasm execution engine is running on a 64-bit machine (probably most servers). Thanks. |
When the browser supports 64-bit addresses, I think the existing js/wasm port should be well positioned to take advantage of it, though it is not yet clear to me what the mechanism would be. There would be no need for an additional port for the If the WASI ecosystem starts moving towards 64 bit (which there is little interest in, as far as I can tell, though I don't have any hard sources on that) the existing wasip1/wasm port would be well suited to target that environment. This is why the potential deprecation of this port would have to be considered in a separate proposal, as I think we'd need to very clearly prove that 64 bit WASI hosts are not going to be relevant to users to make such a change.
I would love to provide some supporting links but unfortunately I've only heard this from various people working closer to the runtimes, so I don't know that I have any concrete evidence for it. Wasmtime does support 64 bit addresses behind a flag. Regardless, all runtimes use 32 bit by default today, so there is a gap in the user experience when using Go. As mentioned before, we'd keep the 64 bit arch around and only seriously consider deprecating it if we knew for sure 64 bit runtimes were not going to become a standard in the ecosystem.
|
Hi @cherrymui
One thing we'll have to double check (and I'm pretty sure about this) is that 32bit integer ops will wrap around correctly at the 32bit boundary even when a 64bit local is involved. |
FYI, in TinyGo's WASI support, both |
Thanks. SGTM overall. We could determine the compiler's implementation detail later (e.g. on a CL, as it is internal to the compiler). One more question: it sounds to me the issue is mostly about interacting with the system API. Besides that, the current 64-bit wasip1/wasm port works reasonably well? Instead of a whole new port, would it be possible to solve it in a API level? Like, add a I guess passing (pointer to) struct is still tricky. But I think that is a tricky problem anyway. On other platforms, when interacting with the system API or C, we still need to ensure the data are laid out the same on both side. In general we don't want to assume a Go struct has identical layout as a C struct. We can do the same for here. And it is usually only needed in a few low-level packages. With the current wasm port, where What is the down side of handling it in an API level? Thanks. |
This is what we've done in the Fastly Compute Go SDK, except to Previously we were passing pointers to structs (in TinyGo), but this was a largely mechanical change and the use of generics does offer us the type safety we would have otherwise lost going straight to As for whether a port is necessary or not, it's unfortunate that every pointer wastes 4 bytes, especially as these serverless compute platforms are fairly limited in terms of memory. |
Thanks. So it sounds like a wrapper Memory savings could be a benefit for a full 32-bit port. The original proposal doesn't seem to discuss it. Maybe that could be added, if the authors think that is important? Do you have an estimate of how much memory it could save? Thanks. |
Regarding the general theme I'm sensing in this thread that it makes sense to always target 64-bit memories when they are available: Note that 64-bit Wasm memories can't benefit from virtual memory guard pages to elide bounds checks, they need to be explicitly bounds checked which is slower (about ~1.5x slower on Wasmtime and SpiderMonkey, for example, depending on the benchmark; I'd expect similar or worse in other engines). Additionally, when pointers are 64 bits, sizes of various structs get larger, and you ultimately get worse locality. I don't have a link, but there have been benchmarks that are actually faster in wasm32 than x86_64 because of these effects. Therefore, unless your program actually needs the additional heap capacity, it generally makes sense to target 32-bit memories even when 64-bit memories are available. |
I'm definitely a Go outsider, but what I would expect Go to aim for, from the perspective of someone pretty involved in Wasm, is to have an endstate like this:
The strange in-between-32-and-64-bit |
A fascinating thought. We hadn't gone down the "special kind of pointer" route and it absolutely merits consideration. To get an idea about it, I think we'll need to lay out all the places that we'd expect the ABI to leak out to JS and see if wasm32.Ptr could handle all of them. |
Allowing a This may or may not be relevant, but hypothetical WASI Preview 2 ( |
The current |
Unless it is targeting 64-bit Wasm memories, I don't see how that is possible. All the load/store instructions that interact with a 32-bit memory take 32-bit addresses, so even if pointers are stored as |
@fitzgen Ah, I see what you're saying. Fair enough, when we pull a pointer value out of the heap and then when we go to deref it, yes, that value is truncated to 32bits. |
@fitzgen do you have any links to those benchmarks? The performance benefit seems worth noting in the proposal. |
Unfortunately I don't, It was from a long while ago. Might have been on https://arewefastyet.com back in the day. I believe the benchmark was creating and manipulating a large binary tree. Assuming it had a representation like |
One big place that locality and density shows up is on the stack or really any linear data structure. For languages like Rust that heavily manage stack and linear structures, the cache line hits probably have a fairly significant effect. |
Best I can find is https://twitter.com/kripken/status/1262092956070109185:
|
(Hi, another Wasm-focused person here -- I work on Wasmtime/Cranelift). Re: benchmarks, a good approximation for the cost that wasm64 carries is the cost of explicit bounds-checks over a virtual memory-based sandbox (reserve 4GB and use 32-bit offsets only). @fitzgen has worked on this in Wasmtime a lot and in this issue noted the factor is 1.52x-1.56x for a complex program (a JS runtime inside the sandbox). That is an optimistic lower bound on the impact that wasm64 would have: wasm64 additionally inflates pointer sizes with the implied effects on cache efficiency. |
Thanks for everyone for your comments on performance implications of a 32 bit port. I've made a minor update to the proposal to mention that we expect there to be performance benefits to a 32 bit port, though we might have to do some measurements to quantify that. I just want to explicitly mention that a wasm64 port is out of scope of this proposal, but could be something we consider in the future. |
Thanks for the discussion. I'm not sure I really understand the bounds check issue? Is that about Go's current I agree that the current |
The current pattern seems to be that {arch} typically means 32-bit, and {arch}{bits} is for 64-bit. At least for arch in arm, mips, loong, riscv. Having 'wasm' become 64-bit and wasm32 for 32-bit seems somewhat counter to that. From https://pkg.go.dev/cmd/go#hdr-Build_constraints:
Have you considered whether it could work well to use the Notably that would come with About being entrenched, as some points of reference:
In both those cases, it seems that preference was given to making the GOOS and GOARCH values optimal for the future, paying the short-term transition costs. The wasip1/wasm port was introduced in Go 1.21, one major release ago, and also marked as experimental. The "p1" refers to Preview 1 in the wasi_snapshot_preview1 spec, so it's expected at some point there may be a GOOS=wasip2 or GOOS=wasi added if that spec isn't the final. Do you have a sense of whether that next 32-bit Go WASI port would continue to use It seems unfortunate if adding a new |
Thank you for the precedent examples regarding changing of a GOOS/GOARCH meaning, that's illuminating. I still worry about changing the meaning of Regarding As a first step that would mean introducing |
@dmitshur, I hear you concerns about the 32 suffix, but it still seems like the best of a few bad options. It's not a huge deal that wasm is weird in yet another way. |
Sounds good to me. It seems using GOARCH=wasm32 for GOOS=wasip1 and potential future WASI ports is prioritizing user experience in the future, then, which I think is the right call. Thanks for taking the points I raised into account. |
Are there plans to continue supporting the existing hybrid arch past a certain point? If not, then it seems reasonable to implement |
This proposal is just adding GOARCH=wasm32. The existing GOARCH=wasm is not changed and continues to be supported for now. At some point we may choose to deprecate it, but that would be a separate proposal. |
No change in consensus, so accepted. 🎉 The proposal is to create a new GOARCH value, wasm32, which looks like a 32-bit CPU to Go programs and uses 32-bit wasm external interfaces. The current GOARCH=wasm uses 32-bit wasm external interfaces too, but it looks like a 64-bit CPU to Go programs. Perhaps some day there will be a GOARCH=wasm64 that looks like a 64-bit CPU and uses 64-bit wasm external interfaces, but it is not this day. |
When looking at #64856 I realized that it could be more of an issue for wasm32. Currently in Go's wasm port, a "PC" is encoded as If we do wasm32, |
+1 for aligning 64-bit values on 8 bytes. The current CL has workarounds for passing an 8-byte aligned pointer to a This is particularly relevant for WASI Preview 2, which uses a richer type system (like lists (slices), records (structs), etc.). Its canonical ABI specifies type alignment and struct layouts, which map identically to Go, assuming 8-byte alignment of 64-bit values. This allows the caller to pass pointers to Go structs without conversion boilerplate. We’re starting with TinyGo, with the intention that this informs how Go can eventually support the Component Model and WASI Preview 2. Some examples (this works in our fork of TinyGo): https://github.com/ydnar/wasm-tools-go/tree/main/wasi Edit:
|
Demonstrates how to get a 64-bit aligned struct on 32-bit arch. Related to golang/go#63131
Change https://go.dev/cl/578355 mentions this issue: |
Change https://go.dev/cl/581316 mentions this issue: |
This is for the proposal, plus a few bug fixes that would/will be necessary when this is put into actual use. Fixes #66408. Updates #63131. Change-Id: I3a66e09d707dd579c59f155e7f53367f41214c30 Reviewed-on: https://go-review.googlesource.com/c/go/+/578355 Reviewed-by: Austin Clements <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: David Chase <[email protected]>
Since there haven't been many updates on this recently, I'll give a quick update: The latest version of the prototype for this work is still in the CL stack introduced by https://go-review.googlesource.com/c/go/+/570835/1. Unfortunately, due to conflicting time commitments, we haven't been able to get this CL to a point where it's ready to be merged. As it is, everything builds successfully and the resulting compiler can produce wasm32 binaries, some which run fine and some which crash catastrophically. We haven't been able to iron out the last few bugs, so for now this effort is somewhat stalled. We think that the easiest way to get this almost-done CL stack over the line now would be to go back to tip and merge changes across gradually. Something like this:
Since this is sort of a from-scratch rewrite, we think other interested parties may be able to participate in this effort, independently or by joining with our effort. If you are interested in taking this on, please reach out in the #webassembly channel on Gophers Slack. It isn't too late to get wasm32 into Go 1.24, but time is running out. |
Background
The
GOARCH=wasm
architecture was first introduced alongsideGOOS=js
in 2019. The choice was made to use a 64 bit architecture because of WebAssembly’s (Wasm) native support for 64 bit integer types and the existing Wasm proposal to introduce a 64 bit memory address space, as detailed in the original Go Wasm design doc. The assumption at the time was that the Wasm ecosystem was going to switch to a 64 bit address space over time, and that Go would benefit from having used a 64 bit architecture from the start.On the browser side, Firefox and Chrome both have experimental support for 64 bit memory, and it seems poised to become stable in the coming year. However, on the server side, all existing hosts use a 32 bit architecture, and it has become clear that it is here to stay.
In most uses of Wasm with Go, the architecture is transparent to the user, but with the introduction of
go:wasmimport
in #38248 and #59149, knowing the memory layout of input and output parameters becomes very important, as described in #59156. A short term solution of restricting the types of input and output parameters to scalar types andunsafe.Pointer
was adopted to make this clear to users, but this user experience is not a desirable long term solution.As an example, the fd_write import from
wasi_snapshot_preview1
accepts an *iovec of this type:Because
GOARCH=wasm
has 64 bit pointers we have currently defined this type as:which gives the correct alignment but has issues:
The conversion from
*byte
touint32
causes the compiler to lose track of the pointer, potentially causing the GC to reclaim the memory before the imported function is called. It requires usingruntime.KeepAlive
on inner pointers passed to imported function calls to ensure that they remain alive.It creates a poor and error prone user experience, as the conversion of the pointer type looks like this:
This issue is already affecting early adopters of the wasip1 port: Fastly SDK.
Performance
A 32 bit architecture would allow the compiled Go code to be more performant, due to the effects of locality, the larger size of certain structs and the difficulty of avoiding bounds checking. It would also use less memory.
Proposal
Create a new
GOARCH=wasm32
architecture which uses 32 bit pointers and integers. The architecture would only be supported in a newwasip1/wasm32
port.int
,uint
anduintptr
would all be 32 bit in length.The maintainers of the existing
wasip1/wasm
port (@johanbrandhorst , @Pryz, @evanphx) volunteer to become maintainers of this new port.Discussion
The introduction of
GOARCH=wasm32
would allow us to write safer code, because the pointer size matches what the host expects:In this case, the compiler can track the use of the slice pointer, and there is no risk that the GC will collect the objects before calling the imported function. There is also stronger type safety since there is no need to bypass the compiler to perform unsafe type conversions.
This provides a much improved user experience for users writing wrappers for host provided functions.
The existing
wasip1/wasm
port would remain and retain thego:wasmimport
type restrictions. It may eventually become deprecated and removed, in accordance with the Go porting policy. Such a change would be subject to a separate proposal.There are no plans for introducing a
js/wasm32
port.The text was updated successfully, but these errors were encountered: