Small benchmark for functional languages targeting web browsers

Someone made a blog post about their experience with performance of the generated code using various functional-for-web languages here.

I have a couple of questions:

  1. What could be the reason for the increased code size and the increased time for execution?
  2. Are there effective strategies one could take to bring the performance closer to vanilla JavaScript? Or is this worrying unnecessarily?
2 Likes

It’s challenging to say the reason behind the increased execution time because there are no Js artifacts in the repo, and I don’t want to build it locally to analyze.

But I have a benchmark of runtime parsing libraries where you can see that libraries written in ReScript show much better results than JavaScript ones. Even rescript-jzon without performance optimizations twice faster than the hyping zod, and several times faster than other popular libraries like yup, superstruct, runtypes.

In my experience of authoring the third fastest parsing library rescript-struct and using ReScript for a product at work, general code in ReScript will be faster than general code in JavaScript. Although ReScript will never be faster than super-optimized JavaScript code. But you can write ReScript in a way, so it generates super-optimized JavaScript code, and it will be even much more readable and maintainable.

As for increased code size, I have a few guesses. Generated ReScript code has a lot of spaces, so if it’s minified/formatted, it’ll shrink quite a lot. It usually happens when you build a bundle for a browser. I’m not sure whether the benchmark shows the source code size or the minified bundle size. Another thing that usually causes code size increase is complex pattern matching, but I don’t see anything like this in the code, so that is probably not the reason.

As I said before, ReScript is fast, and you shouldn’t worry about performance. It’s only crucial when working on a tool that will be called million times per second. I can give you some recommendations if you have a case like this.

2 Likes

Interesting article and fun questions about it regarding the code size and execution times.

I did go ahead and build it locally to see what is going on.

Differences in size

Regarding @DZakh mentioning the minified size, all the *.bs.js files together minified was 13.7kb, and the JS implementation was 6.9kb. So still longer. (Btw, I didn’t do anything fancy, just catted the files if necessary and pasted them here: https://skalman.github.io/UglifyJS-online/.)

As to why it is more bytes…take a look at some of the generated code:

    var bit = function (v) {
      var a = regA.contents;
      setFlag(zero, Caml_obj.caml_equal(Int8.band(a, v), Int8.int8_of_int(0)));
      setFlag(negative, Caml_obj.caml_notequal(Int8.band(v, Int8.int8_of_int(128)), Int8.int8_of_int(0)));
      return setFlag(overflow, Caml_obj.caml_notequal(Int8.band(v, Int8.int8_of_int(64)), Int8.int8_of_int(0)));
    };

Not terse! Lots of casting code that isn’t present in the JS implementation. Also you see Caml_obj.caml_equal and Caml_obj.caml_notequal, which is a lot more characters than the couple of characters for the operator in the JS implementation. You’ve got function calls for bitwise operations versus the inline bitwise ops for the JS implementation. You’ve got a lot of currying increasing the size as well. About 100 or so lines looking like this:

eor(Curry._1(funarg.readMem, Curry._1(zp, undefined)));

You could imagine writing some of the things mentioned above a bit differently and saving a lot of size.

Differences in exec time

I didn’t bother profiling things so obviously just speculation, but…a lot of the things above leading to code size increase will also not favor well against direct JS. E.g., the currying, the casting functions, writing bitwise operations as functions rather than just doing them inline like in the JS version. If you wrote the rescript differently, you could avoid some of this and possibly make a difference in runtime.

Also, I didn’t see the rescript compiler inlining too many function calls in the code output. Not sure why that is as I would need to look at it longer, but it’s interesting. The main module MOS6502 is basically a big functor and all the main types (Int16, and Int8) are abstract, so that may be part of it.


Anyway, really cool blog post and pretty neat that the original author implemented the same thing in a few different compile-to-JS languages.

It would be fun to take the JS implementation and convert it to ReScript using this method and see how much you could improve the ReScript generated code. I imagine someone could make a sizable (pun intended) difference if that was the aim.

3 Likes