ReasonML 对抗命令式纯 JavaScript 的性能表现

4

免责声明: 我是ReasonML的初学者。

最近我开始尝试使用ReasonML,并发现与纯JavaScript相比性能有很大的差异。以下是我对一个简单的解谜函数的示例(谜题来自: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;
};

结果

  • JavaScript: 5毫秒
  • ReasonML(递归): 470毫秒
  • ReasonML(非递归): 7185毫秒

这是预期的结果吗?还是我的ReasonML函数实现得太慢了?

感谢您提供澄清/建议。

不可否认,第二个解决方案(非递归)由于字符串到数组的转换而变慢,但这是因为在ReasonML中,字符串不是由字符列表表示的。


在您的非递归实现中,您可以使用String.iter方法将函数应用于每个字符,而不是将字符串转换为列表然后遍历该列表吗? - pstrjds
String.iter 返回 unit 类型,所以我无法累加操作的结果(也许我误解了你的建议..)。 - skay-
我认为通常情况下,由其他语言生成的JavaScript比手动调整的JavaScript优化程度要低,但差异似乎太大了。生成的JavaScript还似乎有很多来自加载库、List.方法和许多函数调用的开销。 - Slai
我只是随口提了一下。我从未编写过任何ReasonML代码,而且距离我上次编写Standard ML代码已经将近20年了,但我似乎记得有一种方法可以将累加器作为函数的一部分传递给迭代器。 - pstrjds
你的 stringToList 转换看起来非常低效。我不了解 ReasonML,但我希望标准库中有本地转换函数(或至少是 stringarray(char) 的函数)。我会选择 Str.split(regex "") - Bergi
1个回答

11

你可以在 Reason 中编写与 JS 相同的命令式代码,并且实际上它会更快(在我的电脑上快了30%):

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^
};

这个递归解决方案几乎和原来的一样快:

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)
};

基准测试结果:

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

源码: re:bench


嗯,如果你对这些函数进行压力测试,你会发现JavaScript更快。(参见Reddit回复:https://www.reddit.com/r/reasonml/comments/9g10m2/reasonml_performance_against_imperative_vanilla/e62h8yx) 这很有趣,我们从Js版本比命令式的Reason慢15%到Reason命令式变得比它快3%。此外,随着规模的扩大,Reason递归虽然在两种情况下都是最慢的,但它获得了27%的性能提升。 - skay-
@wegry "默认柯里化"? - glennsl
@glennsl 我的意思是使用[@bs]来强制取消柯里化。不过看起来代码生成在这种情况下并不受影响。我想我不确定什么时候取消柯里化从bsb开始。 - wegry
1
是的,如果编译器知道实现和使用方式,它可以进行此类优化。而且不仅限于函数级别,还可以在模块级别上进行。 - glennsl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接