EDIT: For the latest of GopherJS/Wasm comparison, see Wasm benchmark result

Hi all!

This article describes about my experiment of the new WebAssembly port of Go. WebAssembly port is now available on the master branch of Go, and you'd need to compile Go yourself.

I have created GopherWasm, an agnostic WebAssembly wrapper that works both on GopherJS and WebAssembly port.

Performance of GopherJS and WebAssembly depends on browsers. GopherJS is faster than WebAssembly on some environments, and slower on other environments. For Ebiten 'sprite' example, (GopherJS on Chrome) > (WebAssembly on Firefox) > (GopherJS on Firefox) > (WebAssembly on Chrome) with 5000 sprites.

Go on browsers

Running Go applications on web browsers must be awesome. Needless to say, Go is an awesome language. I don't discuss how good Go is in this article :-)

There is a transpiler from Go to JavaScript - GopherJS by Richard Musiol. This also enables Go programs to run both on browsers and Node.js. You can use all the features of Go. The compilation result is reasonably readable JavaScript. The performance is so-so due to some overhead. To emulate Go behaviors precisely, GopherJS adds some overhead like boundary check of index access to slices. Instead of emulating Go behavior by JavaScript, executing binaries like WebAssembly on browsers seems much more efficient.

WebAssembly is a performance-wise format compared to JavaScript. WebAssembly is supported by most of modern browsers. WebAssembly is a low-level language as the name says, and it is expected that WebAssembly binary is generated from other languages. Actually C, C++ and Rust already support WebAssembly port.

The latest Go version 1.11 supports WebAssembly port by Richard Musiol, the same author of GopherJS. Now Go 1.11 is on the way releasing, but you can test WebAssembly APIs with the latest Go by compiling yourself. Your compiled program for WebAssembly is available both on browsers and Node.js. You can use full features of Go including goroutines. You can call any JavaScript functions from Go, and you can pass Go function as a JavaScript callback. The API is defined at syscall/js package. The environment variables for WebAssembly are GOOS=js and GOARCH=wasm . As WebAssembly is performance-wise format, this should be faster than GopherJS, right? Unfortunately, this was not true. I'll describe this later.

Ebiten

Ebiten is a dead simple 2D game library by me. This is basically an OpenGL wrapper. This works on browsers with WebGL by GopherJS, and actually you can see some examples work on the website and the jsgo playground by Dave Brophy. Recently (actually today!) I fixed Ebiten (master branch) to accept WebAssembly compilation of the latest Go compiler except for the audio part. Thus, Ebiten now works both on GopherJS and WebAssembly!

Port GopherJS library to WebAssembly

As I said, Ebiten can already work with GopherJS. GopherJS's API is similar to WebAssembly, but different. For example, the counterpart of js.Object of GopherJS is js.Value of syscall/js .

Then, how can I write libraries to accept both GopherJS and WebAssembly? Of course it is easily possible to write similar duplicated code, but isn't there a more elegant way?

I've created GopherWasm, an agnostic WebAssembly wrapper. If you use GopherWasm, your library automatically works both on GopherJS and WebAssembly port! GopherWasm API is almost same as syscall/js . The only one difference is js.ValueOf accepts []float32 or other slices in GopherWasm, not in syscall/js . I have already filed to fix syscall/js.ValueOf to accept such slices, so the situation might change in near future.

Performance comparison

I've compared the performances between GopherJS and WebAssembly port with my Ebiten example 'sprites'.

By pressing left or right arrow keys, you can change the number of sprites and see how FPS (frames per second) changes.

On my MacBook Pro 2014, I took very rough measurements by showing 5000 sprites:



GopherJS on Chrome: 55-60 FPS GopherJS on Firefox: 20-25 FPS WebAssembly on Chrome: 15-20 FPS WebAssembly on Firefox: 40-45 FPS

Chrome: Version 67.0.3396.87 (Official Build) (64-bit)

Firefox: 60.0.2 (64-bit)

Ebiten: 460c47a9ebaa21bcce730a460a7f87fa6cbe56ed

Go: 534ddf741f6a5fc38fb0bb3e3547d3231c51a7be

This is a very interesting result. Before this experiment, I thought WebAssembly should always be faster than GopherJS. However, the result depended on browsers. For 5000 sprites, the result was (GopherJS on Chrome) > (WebAssembly on Firefox) > (GopherJS on Firefox) > (WebAssembly on Chrome) . I guess optimization way is different among browsers.

I took rough profile and it looks like allocation ( runtime.mallocgc ) was the heaviest task on WebAssembly. This is different tendency from GopherJS. I'm not sure the details how objects are allocated on WebAssembly, but at least WebAssembly requires different optimization from GopherJS.

I plan to do optimization to keep 60 FPS as much as possible. Stay tuned!

Binary size comparison

-rw-r--r-- 1 hajimehoshi staff 7310436 Jun 16 05:23 sprites.js -rw-r--r-- 1 hajimehoshi staff 278394 Jun 16 05:23 sprites.js.map -rwxr-xr-x 1 hajimehoshi staff 8303883 Jun 16 04:03 sprites.wasm

It looks like WebAssembly binary is slightly bigger.

Appendix - How to do experiments

Install Ebiten and other libraries

go get -u github.com/hajimehoshi/ebiten/... go get -u github.com/hajimehoshi/gopherwasm go get -u github.com/gopherjs/gopherjs

Get the latest Go and compile it

cd git clone https://go.googlesource.com/go go-code cd go-code/src # Compile Go. ./all.bash is also fine if you want to run tests. ./make.bash

Compile an Ebiten example for WebAssembly

cd /path/to/your/wasm/project # Compile 'sprites' example for WebAssembly GOOS = js GOARCH = wasm ~/go-code/bin/go build -tags = example -o sprites.wasm github.com/hajimehoshi/ebiten/examples/sprites # Copy wasm_exec.js cp ~/go-code/misc/wasm/wasm_exec.js .

Prepare an HTML file to run the wasm file. This file is based on ~/go-code/misc/wasm/index.html .



<!DOCTYPE html> <script src= "wasm_exec.js" ></script> <script> // Polyfill if ( ! WebAssembly . instantiateStreaming ) { WebAssembly . instantiateStreaming = async ( resp , importObject ) => { const source = await ( await resp ). arrayBuffer (); return await WebAssembly . instantiate ( source , importObject ); }; } const go = new Go (); WebAssembly . instantiateStreaming ( fetch ( " sprites.wasm " ), go . importObject ). then ( result => { go . run ( result . instance ); }); </script>

Run an HTTP server as you like.

Run GopherJS server

gopherjs serve --tags = example