You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _posts/blog/2015-09-27-surprises-in-gopherjs-performance.mdown
+54-16Lines changed: 54 additions & 16 deletions
Original file line number
Diff line number
Diff line change
@@ -5,11 +5,17 @@ date: 2015-09-27
5
5
author: Dmitri Shuralyov
6
6
---
7
7
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).
9
11
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!
11
15
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:
13
19
14
20
```Go
15
21
// 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.
55
61
total time taken is: 8.358498915s
56
62
```
57
63
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.
59
66
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:
61
69
62
70
```bash
63
71
$ gopherjs run main.go
@@ -68,7 +76,12 @@ total time taken is: 2.317s
68
76
69
77
23 seconds, that's actually... wait, WHAT!?
70
78
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?
72
85
73
86
I tried running it in the [GopherJS Playground](http://www.gopherjs.org/playground/#/K7r0-q_Jwc), which you can also do:
74
87
@@ -80,13 +93,22 @@ The calculated value of pi was the same, and after adding some debugging stateme
80
93
81
94
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.
82
95
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:
84
102
85
103
```JavaScript
86
104
Math.pow(x, y)
87
105
```
88
106
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.
90
112
91
113
So, in order to ensure the same code runs in both cases, I did that and tried this program:
92
114
@@ -144,9 +166,13 @@ approximating pi with 1000000000 iterations.
144
166
total time taken is: 6.585s
145
167
```
146
168
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.
148
171
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:
150
176
151
177
```bash
152
178
$ gopherjs run main.go
@@ -220,18 +246,30 @@ user 0m6.427s
220
246
sys 0m0.004s
221
247
```
222
248
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.
224
251
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.
226
254
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.
228
259
229
260
You can try the final optimized version of GopherJS in your browser via the GopherJS Playground:
230
261
231
262
http://www.gopherjs.org/playground/#/sDEYM2TwC7
232
263
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.
234
268
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.
236
272
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