Live data from Hacker News

Surprises in GopherJS Performance

gopherjs.org

11–20 of 29 posts

Re: Surprises in GopherJS Performance

#11
post #5

These micro programs are very easy to JIT, so nearly identical performance is to be expected. It's when you get larger programs that C compilers with function inlining and better cache locality and whole program optimization leave JIT compiled languages in the dust. When you have a loop of a billion iterations the JIT compiler can instantly tell that it's worth optimizing whatever is in the loop body. When you have a…

When the users complain that the app is 'too slow' and the devs say "we've done everything we can", I'm usually the guy who goes and finds another 30% without doing anything crazy.

Two things I see pretty consistently. First is that when there are no tall tent poles (20 functions take 5% of the time each), people don't know what to do, so they don't do anything. Second, and possibly easier to fix, is that people believe the perf analysis tool (the breakdown of where time is spent) is telling them the objective truth. Often it's wrong, which is why we try things, benchmark them, and revert changes if things get worse.

When there are no tall tent poles I switch to invocation count, which is the best secondary indicator of hotspots. There was one method that the perf tool told me was taking 10% of the run time. But the call count was fishy. Due to a bad call structure it was being called far more often than necessary. In the worst spot in the code two sequential calls were calling this function, so I flipped the code around so they could take the answer as an argument (memoization).

I reran the benchmarks. I had removed 50% of calls to a function that took 10% of our time, and the code overall was now 20% (twenty percent!) faster. Why?

Functions allocate memory. They evict cache lines in the data and instruction caches. They might even access constrained resources, like disk. And as you said, they change how the JIT decides to optimize things.

Sometimes, the symptom is that the code that runs immediately afterward gets blamed for problems they didn't create, and the profiler has no way of following the problem back to the root cause, so it assigns blame at the point of contention, not at the start of the contention.

The only tools I've found that helps with this are clean coding practices, and figuring out if your invocation counts match your expectations (I will run a call tree 100 times and then compare the call counts of everything to find things that were called 2+ times as often as they strictly should have been called)

Re: Surprises in GopherJS Performance

#12
post #11
post #5

These micro programs are very easy to JIT, so nearly identical performance is to be expected. It's when you get larger programs that C compilers with function inlining and better cache locality and whole program optimization leave JIT compiled languages in the dust. When you have a loop of a billion iterations the JIT compiler can instantly tell that it's worth optimizing whatever is in the loop body. When you have a…

When the users complain that the app is 'too slow' and the devs say "we've done everything we can", I'm usually the guy who goes and finds another 30% without doing anything crazy. Two things I see pretty consistently. First is that when there are no tall tent poles (20 functions take 5% of the time each), people don't know what to do, so they don't do anything. Second, and possibly easier to fix, is that people beli…

> I reran the benchmarks. I had removed 50% of calls to a function that took 10% of our time, and the code overall was now 20% (twenty percent!) faster.

This reminds me on another comment I read on HN, that I consider something of a "performance paradox": https://news.ycombinator.com/item?id=9895531

Re: Surprises in GopherJS Performance

#13
post #9
post #5

These micro programs are very easy to JIT, so nearly identical performance is to be expected. It's when you get larger programs that C compilers with function inlining and better cache locality and whole program optimization leave JIT compiled languages in the dust. When you have a loop of a billion iterations the JIT compiler can instantly tell that it's worth optimizing whatever is in the loop body. When you have a…

Are you sure? According to [1] the V8 Javascript engine also includes function inlining. Additionally cache locality is still achievable in JS, if the user designs their data structures/code-patterns with the goal of maximising locality [2]. As the engine does not know the types of variables, or the layout of types immediately there is definitely a slower startup than pure C code, but it looks like the V8 engine does…

Yes, v8 and all other modern JS engines perform function inlining and type profiling. Still, it is extremely common to see modern JS engines be competitive with C on small microbenchmarks, but lag behind on large, realistic codebases. There are several reasons for this:

1. JS engines must make tradeoffs between what to inline and what not - each inlining requires a recompilation. Offline C compilers don't have such concerns.

2. Inlining creates large functions, and large codebases tend to have large functions anyhow, and large functions take longer to JIT, making realistic codebases much more challenging for JS engines.

3. It's fairly easy for modern JS engines to figure out types at runtime in a small loop. However, when figuring out types in a large program spread over many functions, it takes substantial overhead to try to do a holistic solution, and instead, JS engines generally just do a local analysis and hope that what really needs to be optimized is inlined anyhow - but see 1 and 2. And when not inlining, function call arguments generally do not happen in an optimized type, but in boxed form.

Overall, it is not surprising at all that the article saw C-like performance on a small micro-benchmark, on a modern browser. But in a realistic codebase, there almost certainly would be a large slowdown. That's why asm.js exists and why WebAssembly is on the way.

Re: Surprises in GopherJS Performance

#14
post #12
post #11

Earlier quoted context omitted.

When the users complain that the app is 'too slow' and the devs say "we've done everything we can", I'm usually the guy who goes and finds another 30% without doing anything crazy. Two things I see pretty consistently. First is that when there are no tall tent poles (20 functions take 5% of the time each), people don't know what to do, so they don't do anything. Second, and possibly easier to fix, is that people beli…

> I reran the benchmarks. I had removed 50% of calls to a function that took 10% of our time, and the code overall was now 20% (twenty percent!) faster. This reminds me on another comment I read on HN, that I consider something of a "performance paradox": https://news.ycombinator.com/item?id=9895531

In this case I only made one change and remeasured. Runtime dropped more than the cumulative call time of that function.

But the thread you reference is a whole other kettle of fish, and I would call that observation #3 about failing at math.

Your boss says the app needs to run 3x as fast. Not "try to make it 3x faster" but "the customer isn't going to buy unless it's 3x faster because competitors". With targets like that anything taking more than 3% of run time is a target for improvement, because they are taking 10% of the goal run time.

People will adamantly refuse to look at the 4th slowest function until they've done something brilliant with the others, even if it's the easiest to fix. That function is only taking 10% of the time, they'll say, so it's not important. But it's taking 30% of the goal run time, and that's huge.

Re: Surprises in GopherJS Performance

#16
Great article on the complexities of investigating performance issues! Another very interesting surprise is when I run this locally on my macbook, clang is much faster

  mac >> go run main.go
  approximating pi with 1000000000 iterations.
  3.1415926545880506
  total time taken is: 9.706911232s

  mac >> clang++ -O3 -ffast-math -march=native main.cpp
  mac >> ./a.out
  3.1415926545864963
  total time taken is: 2.14196s

Re: Surprises in GopherJS Performance

#17
post #14
post #12

Earlier quoted context omitted.

> I reran the benchmarks. I had removed 50% of calls to a function that took 10% of our time, and the code overall was now 20% (twenty percent!) faster. This reminds me on another comment I read on HN, that I consider something of a "performance paradox": https://news.ycombinator.com/item?id=9895531

In this case I only made one change and remeasured. Runtime dropped more than the cumulative call time of that function. But the thread you reference is a whole other kettle of fish, and I would call that observation #3 about failing at math. Your boss says the app needs to run 3x as fast. Not "try to make it 3x faster" but "the customer isn't going to buy unless it's 3x faster because competitors". With targets like…

Sometimes, I think Amdahl's Law is the most useful thing I learned about during my CS degree.

Re: Surprises in GopherJS Performance

#18
What I find the most surprising here is how optimised the v8 engine is. Javascript code running on v8 is as performant as native go code or as optimised C code. Obviously it's just a use case but still, mind blowing.

Re: Surprises in GopherJS Performance

#19
post #11
post #5

These micro programs are very easy to JIT, so nearly identical performance is to be expected. It's when you get larger programs that C compilers with function inlining and better cache locality and whole program optimization leave JIT compiled languages in the dust. When you have a loop of a billion iterations the JIT compiler can instantly tell that it's worth optimizing whatever is in the loop body. When you have a…

When the users complain that the app is 'too slow' and the devs say "we've done everything we can", I'm usually the guy who goes and finds another 30% without doing anything crazy. Two things I see pretty consistently. First is that when there are no tall tent poles (20 functions take 5% of the time each), people don't know what to do, so they don't do anything. Second, and possibly easier to fix, is that people beli…

> When there are no tall tent poles I switch to invocation count

Oh, yes! It's so useful to keep an eye on invocation counts. For example, one "problematic" service in a team I managed was being analyzed by an engineer on my team. He was focused on % time culprits, but I spied a highly suspicious entry a fair ways down the list taking many, many orders of magnitude more invocations than anything else. Off I went to have a look...

It turns out the original dev had meant to put a blocking call in in the code, but had gotten mixed up and used a polling version instead. (Both were non-obviously named and signature interchangeable. Sigh.) The baseline CPU hit in the perf run wasn't significant, but in context of this service, it produced a very severe performance cliff characteristic under high site load.

A one-line change corrected that error, and suddenly the operations team stopped talking about that service (with evil glares ;-) at every opportunity.

Re: Surprises in GopherJS Performance

#20
post #8

What's the JS interop story with GopherJS? Is it possible to import native JS libraries and use them?

GopherJS compiles to "readable" JavaScript, so yes. You have to do something like js.Global.Call("functionname", arg1)

Sort of readable, imo. I mean, a few simple function calls are nicely readable, but blocking calls quickly turn into an unreadable mess in my experience.
Post reply on HN