今天写一个性能敏感的函数发现的这个有趣结果,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 1
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 1
// 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 1
你去 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 1
其实 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 5
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 来实现功能了.
|