4

Disclaimer: I am ReasonML beginner.

I have started playing with ReasonML lately and I have noticed a big difference in performance in contrast to vanilla JavaScript. Here's my examples against a simple puzzle solving function (puzzle taken from: https://adventofcode.com/2015/day/1)

ReasonML

let head = str =>
  switch (str) {
  | "" => ""
  | _ => String.sub(str, 0, 1)
  };

let tail = str =>
  switch (str) {
  | "" => ""
  | _ => String.sub(str, 1, String.length(str) - 1)
  };

let rec stringToList = str =>
  switch (str) {
  | "" => []
  | _ => [[head(str)], tail(str) |> stringToList] |> List.concat
  };

let rec findFloor = code =>
  switch (head(code)) {
  | "" => 0
  | "(" => 1 + findFloor(tail(code))
  | ")" => (-1) + findFloor(tail(code))
  | _ => 0
  };

let findFloorFold = code => {
  let calc = (a, b) =>
    switch (b) {
    | "" => a
    | "(" => a + 1
    | ")" => a - 1
    | _ => a
    };

  code |> stringToList |> List.fold_left(calc, 0);
};

JavaScript

const solve = code => {
  let level = 0;
  for (let i = 0, l = code.length; i < l; i++) {
    const el = code[i];
    if (el === "(") {
      ++level;
      continue;
    }
    if (el === ")") {
      --level;
      continue;
    }
  }
  return level;
};

Results

  • JavaScript: 5ms
  • ReasonML (recursive): 470ms
  • ReasonML (non-recursive): 7185ms

Is this expected or are my ReasonML function implementations too slow?

Thanks in advance for clarifications/suggestions.

Admittedly the second solution (non-rec) is slow due to the string to array conversion but this is because in ReasonML string is not represented by a list of chars.

glennsl
  • 28,186
  • 12
  • 57
  • 75
skay-
  • 1,546
  • 3
  • 16
  • 26
  • In your non-recursive implementation, instead of converting the string to a list and then walking the list, could you use the [String.iter](https://reasonml.github.io/api/String.html) method to apply your function to each character? – pstrjds Sep 15 '18 at 12:31
  • `String.iter` returns a `unit` type though so I want be able to accumulate the result of operations (maybe I misunderstood your suggestion..). – skay- Sep 15 '18 at 12:41
  • I think it is expected in general for JavaScript produced by another language to be less optimized than manually tweaked JavaScript, but the difference does seem too big. The produced JavaScript also seems to have a lot of overhead from loading libraries, `List.` methods, and a lot of function calls – Slai Sep 15 '18 at 12:50
  • I was just kind of throwing it out there. I have never written any ReasonML, and it has been nearly 20 years since I wrote any Standard ML, but I seem to recall there was a way to carry an accumulator as part of the function that you would pass to the iterator. – pstrjds Sep 15 '18 at 13:02
  • Your `stringToList` conversion seems horribly inefficient. I don't know ReasonML, I would have expected a native conversion function in the standard library (or at least a `string` to `array(char)` one). I'd go with `Str.split(regex "")` though – Bergi Sep 15 '18 at 14:53

1 Answers1

11

You can write the same imperative code in Reason as you did in JS, and actually have it be faster (by 30% on my computer):

let findFloorImperative = code => {
  let level = ref(0);
  for (i in 0 to String.length(code) - 1) {
    switch (code.[i]) {
    | '(' => level := level^ + 1
    | ')' => level := level^ - 1
    | _   => failwith("invalid code")
    }
  };
  level^
};

And this recursive solution is almost as fast:

let findFloorRecursiveNoList = code => {  
  let rec helper = (level, i) =>
    if (i < String.length(code)) {
      switch (code.[i]) {
      | '(' => helper(level + 1, i + 1)
      | ')' => helper(level - 1, i + 1)
      | _   => failwith("invalid code")
      }
    } else {
      level
    };

  helper(0, 0)
};

Benchmark results:

Reason, imperative:            19,246,043 ops/sec
Reason, recursive, no list:    18,113,602 ops/sec
JavaScript, imperative:        13,761,780 ops/sec
Reason, recursive, list:       481,426 ops/sec
Reason, folding, list:         239,761 ops/sec

Source: re:bench

glennsl
  • 28,186
  • 12
  • 57
  • 75
  • Hmm, if you stress test the functions you will see that the JavaScript is faster. (see reddit reply: https://www.reddit.com/r/reasonml/comments/9g10m2/reasonml_performance_against_imperative_vanilla/e62h8yx) This is interesting, we went from the Js version being 15% slower to Reason imperative to be 3% faster. Also as we scaled, the Reason recursive although was the slowest in both case it gained a 27% performance increase. – skay- Sep 16 '18 at 11:17
  • @wegry "default currying"? – glennsl Oct 12 '18 at 12:13
  • @glennsl I meant using [@bs] to force uncurrying. It looks like the codegen is unaffected in this case though. I guess I'm not sure when uncurrying kicks in from bsb. – wegry Oct 16 '18 at 15:40
  • 1
    Yeah, fi the compiler knows both the implementation and its use it can do optimizations like this. And on the module level, not just in a function. – glennsl Oct 16 '18 at 16:20