Comparing native GHC versus GHCJS performance on a roguelike field-of-view algorithm

Some months ago I was writing a text-graphics-puzzle game that could be played on the web and also natively. The web version is compiled with GHCJS and the native version with GHC.

Baconist screenshot

The game never got anywhere near that I would call a complete game but at least it’s playable right here. (Move with WASD, arrow keys or hjlkyubn, the game has no other keys).

The big problem I’ve had with GHCJS is that the performance of compute-heavy code is pretty bad.

Unless you have a modern computer with a nice CPU, the time between you pressing the key and the game reacting to it is quite long. If I compile the game natively with GHC, then the performance is more than good enough.

I knew that GHCJS-produced code is slow, but how slow? Compared to native code? That’s what this post is about.

Field of view in The Baconist

One gimmick that this game has is portals. I can place a portal in the world and you can look through it and see on the other side.

Now you are thinking with portals

Computing the field of view is computationally heavy. Especially in my game that has portals. And in particular, I have to minimize the amount of glitches, corner cases and artifacts and turns out that’s not so easy when you allow these portal things to be around. It’s complicated enough that it might one day be another blog post in itself!

I thought the algorithm used for field of view computation would be a good benchmark:

It’s real code, not a toy written just for benchmarking.

It’s important code: I would lose my wonderful portal gimmicks if I couldn’t have it.

Its slowness makes my game not perform so well on the web (on the other hand this function is not solely responsible for slowness). Well, that and me not being very good at writing high-performant Haskell code.

When my game is compiled to native code, it works well enough.

If you are curious, the particular function used in The Baconist is here.

“Criterion” benchmarks.

It turns out criterion , the usual benchmarking library, does not work on GHCJS at this time.

So I wrote a bare-minimum-crappy criterion replacement that implements just enough to do some measurements.

I used this replacement even for natively compiled code to make sure it’s an apples-to-apples comparison.

The benchmarks include different field-of-view algorithms and some level traversal functions.

Results

All times are in microseconds. The most important benchmark is the 80x24 Beam FoV; that corresponds to the function actually used in the web Baconist. The methodology is to run the benchmark as many times as it can run in 10 seconds and then divide that by the number of times it could run, with 10 second warm-up period.

Benchmark Native node js60 firefox chromium 100x100 Raycast FoV 6693 28011 26809 45662 45248 80x24 Raycast FoV 826 3923 3877 6765 6297 100x100 Beam FoV 3759 45248 40983 45200 42918 80x24 Beam FoV 999 13351 12269 13513 13157 100x100 Simple FoV 262 1664 2347 3454 2398 80x24 Simple FoV 37 326 453 662 453 10000 tile traverse 35 1003 1445 2570 1160 10000 tile traverse filled 141 2163 4553 4861 2586 10000 array level traverse 22 468 737 717 532 10000 array level traverse filled 24 373 494 745 452

(js60 is the command-line tool version of Mozilla’s SpiderMonkey JavaScript engine).

Node seems to be doing quite a good job. I thought Chromium uses the same JavaScript engine but perhaps they do not use the same version.

Firefox is not doing a good job. Anecdotally, it does seem that the game runs pretty bad on Firefox compared to other browsers I tried.

Unsurprisingly, native GHC compiled code wipes the floor with everyone else.

Verdict

Depending on what you are doing, GHCJS-compiled code is 5-20 times slower than GHC-natively compiled code.

I don’t think my dream of building web/native portable games with Haskell is going to work out except for games that require little computation :(

I would like to also try the two WebAssembly-powered Haskell compilers: WebGHC and Asterius. I tried to get Asterius up and running but ran into issues trying to get my project compile successfully. I will need to wait until Asterius is in a more mature state.

For The Baconist in particular, I think my slowness comes from two sources: the field of view computation and an inefficient implementation of the graphics: there is a 80x24 grid of span-elements where I modify their CSS-classes when something needs to change in the display.

Software and hardware used

Hardware

I used a Dell XPS 15 7590.

$ uname -a Linux hek 5.2.9-arch1-1-ARCH #1 SMP PREEMPT Fri Aug 16 11:29:43 UTC 2019 x86_64 GNU/Linux $ cat /proc/cpuinfo | grep 'model name' | head -n 1 model name : Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz

Software:

GHC 8.6.5

GHCJS ghc-8.6 branch, commit c3922ede190a7dda6c0b673a3b7441cf41afcc9c

Firefox 68.0.2

Chromium 76.0.3809.100 Arch Linux

Node v11.15.0

JavaScript-C60.8.0