58

GopherJS vs WebAssembly for Go

 5 years ago
source link: https://www.tuicool.com/articles/hit/BRrEZzB
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

tl;dr

  • 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. Same as WebAssembly port, 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 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'.

ameMrum.jpg!web

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

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.

7jaumaQ.png!web

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

Then access http://localhost:8080/github.com/hajimehoshi/ebiten/examples/sprites/ to see the example.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK