今天写一个性能敏感的函数发现的这个有趣结果,lodash some
的性能是 js some
性能的几倍。 我觉得标题加个 [震惊] 都不为过~ /dogo
const testArr = new Array(50_000_000).fill({ a: 1, b: 2, c: 3 }); console.time('es'); const x = testArr.some(v => v.a === 9 && v.b === 9 && v.c === 9); console.timeEnd('es'); console.time('lodash'); const y = _.some(testArr, v => v.a === 9 && v.b === 9 && v.c === 9); console.timeEnd('lodash'); // es: 590.248046875 ms // lodash: 219.496826171875 ms
可以在 https://lodash.com/ 的 F12 中直接测试,我在 node16 环境下结果也一致,lodash-some 性能是 js-some 的几倍
按我理解 js RunTime 应该是更高性能语言的实现(如 C 等),那么原生 some 方法性能应该更高呀。
[].some --> some() { [native code] }
lodash
的 some 源码在这 https://github.com/lodash/lodash/blob/master/some.js
,也仅仅是很普通的 while 遍历,不知道为啥性能这么好。
根据 @vace 给出的 v8 源码。
// Executes the function once for each element present in the // array until it finds one where callback returns true. function ArraySome(f, receiver) { CHECK_OBJECT_COERCIBLE(this, "Array.prototype.some"); // Pull out the length so that modifications to the length in the // loop will not affect the looping and side effects are visible. var array = ToObject(this); var length = TO_UINT32(array.length); if (!IS_SPEC_FUNCTION(f)) { throw MakeTypeError('called_non_callable', [ f ]); } var needs_wrapper = false; if (IS_NULL_OR_UNDEFINED(receiver)) { receiver = %GetDefaultReceiver(f) || receiver; } else { needs_wrapper = SHOULD_CREATE_WRAPPER(f, receiver); } var stepping = DEBUG_IS_ACTIVE && %DebugCallbackSupportsStepping(f); for (var i = 0; i < length; i++) { if (i in array) { var element = array[i]; // Prepare break slots for debugger step in. if (stepping) %DebugPrepareStepInIfStepping(f); var new_receiver = needs_wrapper ? ToObject(receiver) : receiver; if (%_CallFunction(new_receiver, element, i, array, f)) return true; } } return false; }
去掉里面的各种参数的类型检查,简化为以下代码后
function jsSome(array, receiver) { let length = array.length; for (let i = 0; i < length; i++) { let element = array[i]; if (receiver(element, i, array)) return true; } return false; }
跑出来的成绩就和 lodash 几乎一致了。
这些类型检查我看了下,大部分对于底层泛用性来说是绝对必要的,但是对于确定的场景很多是不必要的。 所以对于 确定的场景 性能方面也不能盲目确信原生最佳。最针对的代码性能最优泛用性也最低。
完结撒花~
1 hangbale 2022-08-01 18:39:48 +08:00 有没有可能 lodash 的 some 只实现了原生 some 的一部分功能 |
![]() | 2 noe132 2022-08-01 18:44:57 +08:00 我的测试结果跟你不太一样 node v16.15.1 > a() es: 206.54ms lodash: 240.6ms > a() es: 211.843ms lodash: 245.908ms > a() es: 212.926ms lodash: 245.313ms > a() es: 210.621ms lodash: 241.171ms > a() es: 212.199ms lodash: 239.314ms |
3 Leviathann 2022-08-01 18:45:45 +08:00 ![]() https://selfrefactor.github.io/rambda/#/?id=%e2%9d%af-benchmarks https://mobily.github.io/ts-belt/benchmarks/v3.12.0/macbook-pro-2021 这两个大部分函数比 lodash 又快了不少 js 很多原生的 api 就是很慢的 |
5 lingly02 2022-08-01 18:47:50 +08:00 via iPhone ![]() // Production steps of ECMA-262, Edition 5, 15.4.4.17 // Reference: http://es5.github.io/#x15.4.4.17 if (!Array.prototype.some) { Array.prototype.some = function(fun/*, thisArg*/) { 'use strict'; if (this == null) { throw new TypeError('Array.prototype.some called on null or undefined'); } if (typeof fun !== 'function') { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { if (i in t && fun.call(thisArg, t[i], i, t)) { return true; } } return false; }; } 这是原生代码的逻辑,是要比 lodash 复杂 |
6 mxT52CRuqR6o5 2022-08-01 18:55:15 +08:00 ![]() 你去 lodash 官网,在控制台,先运行 const testArr = new Array(50_000_000).fill({ a: 1, b: 2, c: 3 }); 再运行 console.time('es'); const x = testArr.some(v => v.a === 9 && v.b === 9 && v.c === 9); console.timeEnd('es'); console.time('lodash'); const y = _.some(testArr, v => v.a === 9 && v.b === 9 && v.c === 9); console.timeEnd('lodash'); 就会发现 es 跑得更快了,放一起运行估计是受 gc 影响了 |
7 mxT52CRuqR6o5 2022-08-01 19:06:02 +08:00 在 chrome 各种试验,结果很不稳定,一会儿 es 快一会儿 lodash 快 firefox 的性能比较稳定,哪个在前面哪个耗时多,es 和 lodash 切换一下顺序速度就不一样了 |
![]() | 8 lqzhgood OP @mxT52CRuqR6o5 你说的 GC 确实有可能 因此按你说的两步执行。 第一步执行 testArr 然后第二步改成 setTime 延迟执行 ``` js setTimeout(() => { console.time('lodash'); const y = _.some(testArr, v => v.a === 9 && v.b === 9 && v.c === 9); console.log('y', y); console.timeEnd('lodash'); }, 10 * 1000); setTimeout(() => { console.time('es'); const x = testArr.some(v => v.a === 9 && v.b === 9 && v.c === 9); console.log('x', x); console.timeEnd('es'); }, 20 * 1000); ``` 无论 lodash 放前放后 lodash 都比 js 快 2~3 倍 不知道是不是和平台有关系 i7-7700HQ Chrome 103.0.5060.134 |
![]() | 9 ragnaroks 2022-08-01 20:34:47 +08:00 先内置再 lodash: es: 280.18994140625 ms lodash: 86.681884765625 ms CTRL+R 后交换顺序: lodash: 219.944091796875 ms es: 285.6279296875 ms 说实话写了这么多年 js/ts 从没在意过这个,而且我一向是能用内置就不用 lodash ,看来以后要改观了 |
10 mxT52CRuqR6o5 2022-08-01 20:35:22 +08:00 @lqzhgood 你在 codesandbox 里跑跑看,不要打开控制台,打开控制台浏览器会有一些额外工作影响测量准确性 |
![]() | 11 codehz 2022-08-01 20:46:46 +08:00 ![]() 其实 v8 的很多数组方法都是 js 写的 - 只是标记成 native ,毕竟无论如何都要做各种类型检查和转换(以及按 es 语义调用 proxy getter setter ),加上 es 语义允许数组方法在非数组类型上调用,native 写不会有多少优势 - 反而可能会漏掉一些语义保证) |
![]() | 12 lizhenda 2022-08-01 21:06:05 +08:00 平常还真不会注意,原生居然慢 ... |
13 Huelse 2022-08-01 22:56:11 +08:00 我反复跑了几次的确 lodash 成绩稍好,第一次跑的话 lodash 只要 92ms lodash: 228.31201171875 ms es: 292.26806640625 ms 从表现上来看我觉得和申请内存有关 |
![]() | 14 vace 2022-08-02 00:36:30 +08:00 ![]() lodash 不用考虑各种参数的类型检查,默认用户传入的参数都是有效的。 可以看 v8 实现的源码细节: https://chromium.googlesource.com/external/v8/+/refs/heads/master/src/array.js |
![]() | 15 autoxbc 2022-08-02 04:15:14 +08:00 引擎底层敢用 JS 解释 JS 说明 JIT 性能够好,这是好事 |
![]() | 16 caisanli 2022-08-02 08:00:39 +08:00 via iPhone while 比 for 执行更快? |
17 loolac 2022-08-02 08:30:11 +08:00 es: 215.10009765625 ms |
18 loolac 2022-08-02 08:30:17 +08:00 lodash: 173.812744140625 ms |
19 bthulu 2022-08-02 09:14:31 +08:00 i5 8300H, lodash 比原生快 4-5 倍的样子 |
![]() | 20 lqzhgood OP @ragnaroks 我也是一样,能用原生实现尽量用原生(一是洁癖,二是觉得原生性能最优),现在在一些性能优先的函数可能要额外考虑考虑了。 |
21 lelouchjoshua 2022-08-02 09:57:08 +08:00 lodash 才是 js 标准库 |
![]() | 22 cjh1095358798 2022-08-02 10:29:03 +08:00 @vace v8 中的数组方法也是 js 写的吗,为啥不用 c++写呢 |
23 hangbale 2022-08-02 10:46:24 +08:00 这两个 some 没有可比性,原生实现考虑的东西比 lodash 多太多, lodash 的 some 比起标准的 some 功能是残缺的,具体可以看 mdn 的文档, 还有其他 lodash 函数比原生性能好的现象,都是类似的情况。 js 数组原生方法的实现,v8 用的是 Torque ,语法类似 typescript ,生成的是 c++代码,历史上也有用手写汇编,C++,self-hosted(用 js 实现 js)实现 |
![]() | 24 Mutoo 2022-08-02 13:07:31 +08:00 关掉 Chrome 的 JIT 测出来的结果: $ open -a Google\ Chrome --args --js-flags="--jitless" VM136:3 es: 1210.88330078125 ms VM152:3 lodash: 2583.97802734375 ms |
25 DICK23 2022-08-02 15:26:08 +08:00 M1 node v16.15.0 es: 356.664 ms lodash: 104.072ms manual: 104.843ms |
![]() | 26 libook 2022-08-02 16:35:52 +08:00 曾经很长一段时间,Bluebird 的卖点之一都是比 V8 原生 ES6 的 Promise 性能好,那时候 V8 每次更新我就会跑一下 benchmark ,见证了原生 Promise 实现的性能越来越好,直至超过 Bluebird 。 估计一些 ES 新特性在引擎里可能会先用 JS 代码简单实现,后面才会再逐渐优化,甚至用 C++重写。 |
![]() | 27 lujiaosama 2022-08-02 16:46:53 +08:00 我用原生不是因为性能, 是因为不想引入 lodash 这个库 , 花里胡哨的 api 一顿操作然后老是被人吐槽看不懂看查文档. 现在就老实用最基础的 map,filter,reduce 来实现功能了. |