JavaScript 函数式编程存在性能问题么?

最近在看 clean-code-javascript,看到关于函数式编程的描述时有些困惑。 毋庸置疑函数式的写法更加清晰,但是先map再reduce把数据遍历了两次,而原来的写法只需要遍历一次,在list非常大的场景下,或者链式过程更长一些,遍历N次显得太蠢了吧?是否有必要为了提升一点可读性来牺牲性能,求解惑~ 引入ramdajs这种库又觉得没有必要,太重了...
关注者
639
被浏览
42,540

最近正打算写 javascript 性能和 V8 GC 的专题,今天就看到了这个问题。


原作者写到:

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 vs loop · jsPerf


慢了 96%。


Lodash map vs forEach vs native for loop



Flatten an array - loop vs reduce


使用最原始的 for 循环,性能提升了近一倍。


JS loop comparison

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



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

数组参数比 rest 快了 14%。


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

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

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

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

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

---------- 更 2017-01-11 13:39:56 ------

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

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

上面一段已经过时了,如今 V8 已经有了解释器,也更换了新的引擎。详见我的 V8 专栏:使用 D8 分析 javascript 如何被 V8 引擎优化的

对于编译和执行相关的 javascript 文章没有找到,姑且用一篇鸟哥 @Laruence 的 PHP 文章凑数吧: 关于PHP的编译和执行分离 | 风雪之隅

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 语句。

<del>一旦使用这些代码,将导致这个函数无法被优化</del>(已经可以了)

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

cs.chromium.org/chromiu

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

github.com/v8/v8/blob/m


<del>每次循环都要判断参数是否为一个回掉函数,而且还判断了元素是否为 undefined</del>,(谢谢评论区各位大大提醒,此段有误)

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