Skip to content

Commit 49418e6

Browse files
committed
Use semantic linefeeds for more readable diffs, easier review.
Rendered Markdown document is unchanged.
1 parent 392f09c commit 49418e6

File tree

1 file changed

+54
-16
lines changed

1 file changed

+54
-16
lines changed

_posts/blog/2015-09-27-surprises-in-gopherjs-performance.mdown

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ date: 2015-09-27
55
author: Dmitri Shuralyov
66
---
77

8-
The GohperJS project first caught my attention about 2 year ago, back when few parts of the Go spec were implemented. However, I noticed the incredible pace at which Richard was working, making multiple sophisticated commits per day, as well as fixing reported compiler issues within hours. A few months later, I decided to download it and give it try on a relatively [large pure Go package](https://godoc.org/github.com/shurcooL/markdownfmt/markdown?import-graph&hide=1) for formatting Markdown, and I was quite shocked when it... [simply worked](https://github.com/shurcooL/atom-markdown-format/commit/6b5f21c4457309f8eba3a78b82e0c9a458ff13b4).
8+
The GohperJS project first caught my attention about 2 year ago, back when few parts of the Go spec were implemented.
9+
However, I noticed the incredible pace at which Richard was working, making multiple sophisticated commits per day, as well as fixing reported compiler issues within hours.
10+
A few months later, I decided to download it and give it try on a relatively [large pure Go package](https://godoc.org/github.com/shurcooL/markdownfmt/markdown?import-graph&hide=1) for formatting Markdown, and I was quite shocked when it... [simply worked](https://github.com/shurcooL/atom-markdown-format/commit/6b5f21c4457309f8eba3a78b82e0c9a458ff13b4).
911

10-
Since then, GopherJS has made significant progress, both in feature support (by now, full support for goroutines, channels, select statement, and the rest of the Go language spec), as well as quite a few performance leaps. For example, in [issue 142](https://github.com/gopherjs/gopherjs/issues/142), I reported a case where the GopherJS performance was pretty bad, taking nearly 30 seconds to do what native Go did in under 100 ms. Fast forward just a few days later, and Richard came up with optimizations that lead to a [10x improvement](https://github.com/gopherjs/gopherjs/issues/142#issuecomment-68664354) in performance!
12+
Since then, GopherJS has made significant progress, both in feature support (by now, full support for goroutines, channels, select statement, and the rest of the Go language spec), as well as quite a few performance leaps.
13+
For example, in [issue 142](https://github.com/gopherjs/gopherjs/issues/142), I reported a case where the GopherJS performance was pretty bad, taking nearly 30 seconds to do what native Go did in under 100 ms.
14+
Fast forward just a few days later, and Richard came up with optimizations that lead to a [10x improvement](https://github.com/gopherjs/gopherjs/issues/142#issuecomment-68664354) in performance!
1115

12-
One day, I was perusing the golang.org home page and decided to play with the [concurrent pi](https://play.golang.org/p/RdbPXQcZHi) sample. I wanted to see how much overhead using goroutines was (they were used to demonstrate how lightweight they are compared to threads, but it's still suboptimal for performance), so I converted the program to a purely iterative one. It looked like this:
16+
One day, I was perusing the golang.org home page and decided to play with the [concurrent pi](https://play.golang.org/p/RdbPXQcZHi) sample.
17+
I wanted to see how much overhead using goroutines was (they were used to demonstrate how lightweight they are compared to threads, but it's still suboptimal for performance), so I converted the program to a purely iterative one.
18+
It looked like this:
1319

1420
```Go
1521
// Play with benchmarking a tight loop with many iterations and a func call, compare gc vs GopherJS performance.
@@ -55,9 +61,11 @@ approximating pi with 50000000 iterations.
5561
total time taken is: 8.358498915s
5662
```
5763

58-
8.35 seconds to perform 50 million iterations, not bad. Then I got curious how long it would take if compiled to JavaScript via GopherJS.
64+
8.35 seconds to perform 50 million iterations, not bad.
65+
Then I got curious how long it would take if compiled to JavaScript via GopherJS.
5966

60-
I realized that this is a very tight loop, so any overhead incurred by the conversion of Go to JavaScript would be multiplied and be very visible. Still, I was curious, so fired up GohperJS and ran the same program by compiling it to JavaScript and running it with node:
67+
I realized that this is a very tight loop, so any overhead incurred by the conversion of Go to JavaScript would be multiplied and be very visible.
68+
Still, I was curious, so fired up GohperJS and ran the same program by compiling it to JavaScript and running it with node:
6169

6270
```bash
6371
$ gopherjs run main.go
@@ -68,7 +76,12 @@ total time taken is: 2.317s
6876

6977
23 seconds, that's actually... wait, WHAT!?
7078

71-
2.3 seconds! That's 4 times faster than the native Go version. For a few minutes, I looked at the two numbers in disbelief. Then I decided to investigate what's going on. Is the same code running in both cases? Is the program correct? Is node doing something weird?
79+
2.3 seconds! That's 4 times faster than the native Go version.
80+
For a few minutes, I looked at the two numbers in disbelief.
81+
Then I decided to investigate what's going on.
82+
Is the same code running in both cases?
83+
Is the program correct?
84+
Is node doing something weird?
7285

7386
I tried running it in the [GopherJS Playground](http://www.gopherjs.org/playground/#/K7r0-q_Jwc), which you can also do:
7487

@@ -80,13 +93,22 @@ The calculated value of pi was the same, and after adding some debugging stateme
8093

8194
But how could it be that taking this Go program and compiling it to JavaScript and executing that would be 4 times faster? I had to get to the bottom of it.
8295

83-
The first thing I needed to ensure, was the same code being run in both cases? The entire code is plain Go, with the exception of `math.Pow`. So I looked at how [Go implements it](http://gotools.org/math#Pow). Pretty straightforward Go code. Now I knew GopherJS uses some JavaScript native APIs to implement parts of the standard library, so I checked how [it implemented `math.Pow`](https://github.com/gopherjs/gopherjs/blob/master/compiler/natives/math/math.go#L157). Aha! It's not the same code after all. GohperJS implements it by using [JavaScript's `Math` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math), so it translates to:
96+
The first thing I needed to ensure, was the same code being run in both cases? The entire code is plain Go, with the exception of `math.Pow`.
97+
So I looked at how [Go implements it](http://gotools.org/math#Pow).
98+
Pretty straightforward Go code.
99+
Now I knew GopherJS uses some JavaScript native APIs to implement parts of the standard library, so I checked how [it implemented `math.Pow`](https://github.com/gopherjs/gopherjs/blob/master/compiler/natives/math/math.go#L157).
100+
Aha! It's not the same code after all.
101+
GohperJS implements it by using [JavaScript's `Math` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math), so it translates to:
84102

85103
```JavaScript
86104
Math.pow(x, y)
87105
```
88106

89-
That's when it hit me. In this code, which was taken from a snippet that optimized for brevity and demonstration purposes rather than performance, `math.Pow` was being used with th first argument of -1, and the second argument are values 0, 1, 2, 3, etc., in sequence. The output of that is an alternating sequence of 1, -1, 1, -1, 1, -1, etc. But using `math.Pow` for that is extremely inefficient, since it's meant to work with arbitrary inputs that are much harder to calculate. This can be trivially rewritten with an if statement.
107+
That's when it hit me.
108+
In this code, which was taken from a snippet that optimized for brevity and demonstration purposes rather than performance, `math.Pow` was being used with th first argument of -1, and the second argument are values 0, 1, 2, 3, etc., in sequence.
109+
The output of that is an alternating sequence of 1, -1, 1, -1, 1, -1, etc.
110+
But using `math.Pow` for that is extremely inefficient, since it's meant to work with arbitrary inputs that are much harder to calculate.
111+
This can be trivially rewritten with an if statement.
90112

91113
So, in order to ensure the same code runs in both cases, I did that and tried this program:
92114

@@ -144,9 +166,13 @@ approximating pi with 1000000000 iterations.
144166
total time taken is: 6.585s
145167
```
146168

147-
I had to bump up the number of iterations to 1 billion, because this code runs so much faster than the naive `math.Pow`-using version, in both cases. But GopherJS version is still faster.
169+
I had to bump up the number of iterations to 1 billion, because this code runs so much faster than the naive `math.Pow`-using version, in both cases.
170+
But GopherJS version is still faster.
148171

149-
Aha, then I realized that [GopherJS emulates a 32-bit architecture](https://github.com/gopherjs/gopherjs#architecture). But I'm running native Go on a 64-bit machine. So the size of `int` is 32-bit for GopherJS code but 64-bit for Go code. Let's make it use `int32` consistently and try again:
172+
Aha, then I realized that [GopherJS emulates a 32-bit architecture](https://github.com/gopherjs/gopherjs#architecture).
173+
But I'm running native Go on a 64-bit machine.
174+
So the size of `int` is 32-bit for GopherJS code but 64-bit for Go code.
175+
Let's make it use `int32` consistently and try again:
150176

151177
```bash
152178
$ gopherjs run main.go
@@ -220,18 +246,30 @@ user 0m6.427s
220246
sys 0m0.004s
221247
```
222248

223-
Nice, it's the same time as the Go and GopherJS versions. So that means a few things.
249+
Nice, it's the same time as the Go and GopherJS versions.
250+
So that means a few things.
224251

225-
The V8 JavaScript Engine is incredible. It's able to take Go code that is compiled to JavaScript code, and just-in-time compile to it to machine instructions that are as efficient as the native Go compiler.
252+
The V8 JavaScript Engine is incredible.
253+
It's able to take Go code that is compiled to JavaScript code, and just-in-time compile to it to machine instructions that are as efficient as the native Go compiler.
226254

227-
The JavaScript `Math.pow` implementation clearly has a fast-path for when value of x is -1 and values of y are integers. So for those inputs, it's faster. Using `Pow` with such inputs is silly and should not be done, as you can see by the 50 million to 1 billion iteration increase when rewriting it with an equivalent if statement. Go chooses not to have a fast path for such inputs, which makes it faster in the general case, and I'm glad it makes that choice.
255+
The JavaScript `Math.pow` implementation clearly has a fast-path for when value of x is -1 and values of y are integers.
256+
So for those inputs, it's faster.
257+
Using `Pow` with such inputs is silly and should not be done, as you can see by the 50 million to 1 billion iteration increase when rewriting it with an equivalent if statement.
258+
Go chooses not to have a fast path for such inputs, which makes it faster in the general case, and I'm glad it makes that choice.
228259

229260
You can try the final optimized version of GopherJS in your browser via the GopherJS Playground:
230261

231262
http://www.gopherjs.org/playground/#/sDEYM2TwC7
232263

233-
It's fascinating to think about what happens when you do that. The GopherJS compiler, written in pure Go, has compiled itself to JavaScript, which runs in your browser. That compiler takes your input Go program, compiles it to JavaScript and runs it. The V8 engine (or whatever JavaScript engine your browser uses) takes the generated JavaScript and JITs it to the equivalent machine code as produced by a low-level C implementation compiled with -O3, the max optimization setting.
264+
It's fascinating to think about what happens when you do that.
265+
The GopherJS compiler, written in pure Go, has compiled itself to JavaScript, which runs in your browser.
266+
That compiler takes your input Go program, compiles it to JavaScript and runs it.
267+
The V8 engine (or whatever JavaScript engine your browser uses) takes the generated JavaScript and JITs it to the equivalent machine code as produced by a low-level C implementation compiled with -O3, the max optimization setting.
234268

235-
There are still cases where the code GopherJS generates does not translate to something JS engines can optimize really well. For example, in [issue 276](https://github.com/gopherjs/gopherjs/issues/276), GopherJS version runs an unusually 1000x slower than native version. But I'm sure with some work, significant performance improvements can be made there, and in most other cases the performance is much better.
269+
There are still cases where the code GopherJS generates does not translate to something JS engines can optimize really well.
270+
For example, in [issue 276](https://github.com/gopherjs/gopherjs/issues/276), GopherJS version runs an unusually 1000x slower than native version.
271+
But I'm sure with some work, significant performance improvements can be made there, and in most other cases the performance is much better.
236272

237-
With the prospects of asm.js and the upcoming WebAssembly, I think there's a bright future for having Go language as a viable choice for the browser. I suggest you give it a try for your next little frontend project, or play with compiling any pure Go package to run in the browser. You may end up being pleasantly surprised, like I was.
273+
With the prospects of asm.js and the upcoming WebAssembly, I think there's a bright future for having Go language as a viable choice for the browser.
274+
I suggest you give it a try for your next little frontend project, or play with compiling any pure Go package to run in the browser.
275+
You may end up being pleasantly surprised, like I was.

0 commit comments

Comments
 (0)