JavaScript 函数式编程的性能问题

来源:互联网 发布:手机淘宝首页如何装修 编辑:程序博客网 时间:2024/05/19 04:56

clean-code-javascript 是 github 上的一个项目,旨在教 javascript 程序员写出简洁易于维护的代码,其中有一条是 Favor functional programming over imperative programming。作者以两段代码说明:

Bad:

  1. const programmerOutput = [

  2.  {

  3.    name: 'Uncle Bobby',

  4.    linesOfCode: 500

  5.  }, {

  6.    name: 'Suzie Q',

  7.    linesOfCode: 1500

  8.  }, {

  9.    name: 'Jimmy Gosling',

  10.    linesOfCode: 150

  11.  }, {

  12.    name: 'Gracie Hopper',

  13.    linesOfCode: 1000

  14.  }

  15. ];

  16. let totalOutput = 0;

  17. for (let i = 0; i < programmerOutput.length; i++) {

  18.  totalOutput += programmerOutput[i].linesOfCode;

  19. }

Good:

  1. const programmerOutput = [

  2.  {

  3.    name: 'Uncle Bobby',

  4.    linesOfCode: 500

  5.  }, {

  6.    name: 'Suzie Q',

  7.    linesOfCode: 1500

  8.  }, {

  9.    name: 'Jimmy Gosling',

  10.    linesOfCode: 150

  11.  }, {

  12.    name: 'Gracie Hopper',

  13.    linesOfCode: 1000

  14.  }

  15. ];

  16. const INITIAL_VALUE = 0;

  17. const totalOutput = programmerOutput

  18.  .map((programmer) => programmer.linesOfCode)

  19.  .reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE);

有人在知乎对函数式编程的性能提出了疑问:

毋庸置疑函数式的写法更加清晰,但是先 map 再 reduce 把数据遍历了两次,而原来的写法只需要遍历一次,在 list 非常大的场景下,或者链式过程更长一些,遍历 N 次显得太蠢了吧?是否有必要为了提升一点可读性来牺牲性能?

由于最近正打算写 javascript 性能和 V8 GC 的专题,于是简单分析一下 javascript 的性能。

其实原作者已经在文中写了:

Don't over-optimize

Modern browsers do a lot of optimization under-the-hood at runtime. A lot of times, if you are optimizing then you are just wasting your time. There are good resources for seeing where optimization is lacking. Target those in the meantime, until they are fixed if they can be.

避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。

在 jsperf 网站中,可以看到:

forEach 对比 for:

只知道 forEach 慢,没想到居然这么慢。足足 20 倍的差距。

那我们再看看 lodash 库提供的map、forEach 和原生 for 的对比:

reduce 和双循环的对比:

使用原生 for 循环,性能提升了近 10 倍。

另外,即使是原生循环,不同的写法,性能也不一样。换句话说:越是使用奇技淫巧,性能就越高

上周末我还创建了一个 ES6 中 rest 参数对比数组参数的测试:fp rest vs array

数组参数比 rest 快了 14%。

说到函数式编程,大家都会提到 Lo-Dash 和 RxJS。去年我翻译了一篇文章:如何百倍加速 Lo-Dash?引入惰性计算,开头是这样写的:

我一直以为像 Lo-Dash 这样的库已经不能再快了,毕竟它们已经足够快了。 Lo-Dash 几乎完全混合了各种 JavaScript 奇技淫巧来压榨出最好的性能。

但是 Lo-Dash 却使用了更深的技巧,把性能提升了不只百倍,基准测试:

我的观念是:这些事应该交给编译器去优化,而 V8 也确实做的非常出色。记得之前 C 语言有一个关键字 register,开发者可以使用 register 定义变量,告诉编译器这个变量要放到寄存器里面。我学 C 语言的时候电脑还是 30386,30586 时代,好像有 4 个还是 6 个寄存器可以使用,而现在的主流 C 编译器都会默认忽略这个关键词。

如果你是一个库开发者,你应该多研究研究 V8 的运行机制,比如 JIT,GC,等。如果你是一个普通开发者,还是那句老话,过早优化是万恶之源。你应该把可读性,可维护性,可测试性放到首位。

在原文评论中 @beeplin 说 forEach 居然这么慢。再补充点 V8 的相关知识。

大部分开发者认为 javascript 是脚本语言,所以应该是解释执行的。但是 V8 并没有 JS 解释器它有 2 个不同的编译器,分别是通用编译器和优化编译器。javascript 是直接被编译为机器码执行的。

JIT 编译器相比传统解释器也有一个很大的优势就是可以找出热代码,对于频繁使用的代码进行编译和优化。

但是并不是所有的 JavaScript 代码都能被优化:

目前暂时不能被优化的有:

  • Generator functions

  • Functions that contain a for-of statement

  • Functions that contain a try-catch statement

  • Functions that contain a try-finally statement

  • Functions that contain a compound let assignment

  • Functions that contain a compound const assignment

  • Functions that contain object literals that contain _proto_, or get or set declarations.

永远不可能被优化的有:

  • Functions that contain a debugger statement

  • Functions that call literally eval()

  • Functions that contain a with statement

第一个 debugger用在开发环境,线上环境中千万不要包含 debugger 代码。

第二个 eval 是万恶之源,不要用。

第三个 不要使用 with 语句。

一旦使用这些代码,将导致这个函数无法被优化。

chromium 源码中定义了所有导致性能的原因 bailout:

https://cs.chromium.org/chromium/src/v8/src/bailout-reason.h

那 forEach 为什么会这么慢呢?因为完全可以在优化的时候 JIT 为 for-loop,而且可以展开为性能最高的 loop。我们可以看一下 V8 的源码:

forEach 需要判断参数是否为一个回掉函数,而且还判断了元素是否为 undefined。每次循环都是一次函数调用并且没有做 ICs 优化。

而 lodash 的 foreach 直接使用了 while 循环。而 lodash 的 map 比原生 for-loop 还快,因为它提前分配了一个 array https://github.com/lodash/lodash/blob/37d4e23edc265e1950b9552dc71a6e3a1167082a/lodash.js#L3514。

ES 规范标准中数组 foreach 做了很多额外工作,评论中朴灵大大也提到了,写个优化过的不符合规范的 foreach,分分钟秒了原生数组的 foreach,其实各大第三方库都是这么做的。

原创粉丝点击