2019-08-26

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

The Haskell code was compiled with -O2.