lodash some 方法性能为什么比 js 原生方法 还高? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
lqzhgood
V2EX    Javascript

lodash some 方法性能为什么比 js 原生方法 还高?

  •  2
     
  •   lqzhgood 2022-08-01 18:30:09 +08:00 6102 次点击
    这是一个创建于 1175 天前的主题,其中的信息可能已经有所发展或是发生改变。

    今天写一个性能敏感的函数发现的这个有趣结果,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 some

    按我理解 js RunTime 应该是更高性能语言的实现(如 C 等),那么原生 some 方法性能应该更高呀。

    [].some --> some() { [native code] } 

    lodash some

    lodash 的 some 源码在这 https://github.com/lodash/lodash/blob/master/some.js,也仅仅是很普通的 while 遍历,不知道为啥性能这么好。

    第 1 条附言    2022-08-02 09:47:34 +08:00

    根据 @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 几乎一致了。

    这些类型检查我看了下,大部分对于底层泛用性来说是绝对必要的,但是对于确定的场景很多是不必要的。 所以对于 确定的场景 性能方面也不能盲目确信原生最佳。最针对的代码性能最优泛用性也最低。

    完结撒花~

    第 2 条附言    2022-08-02 09:55:18 +08:00
    上面附言的结论 100% 成立的前提是,js RumTime 下的很多实现应该还是 js ,例如 Array 的 some 函数,只是标记为了 [native code]

    感谢 @codehz

    如果 some 是真 [native code] 实现的(如 C 等),就需要额外测试讨论了。
    27 条回复    2022-08-02 16:46:53 +08:00
    hangbale
        1
    hangbale  
       2022-08-01 18:39:48 +08:00
    有没有可能 lodash 的 some 只实现了原生 some 的一部分功能
    noe132
        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
    Leviathann
        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 就是很慢的
    lqzhgood
        4
    lqzhgood  
    OP
       2022-08-01 18:47:40 +08:00
    @noe132 我也是 node 16.15.1

    es: 738.817ms
    lodash: 205.519ms
    lingly02
        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 复杂
    mxT52CRuqR6o5
        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 影响了
    mxT52CRuqR6o5
        7
    mxT52CRuqR6o5  
       2022-08-01 19:06:02 +08:00
    在 chrome 各种试验,结果很不稳定,一会儿 es 快一会儿 lodash 快
    firefox 的性能比较稳定,哪个在前面哪个耗时多,es 和 lodash 切换一下顺序速度就不一样了
    lqzhgood
        8
    lqzhgood  
    OP
       2022-08-01 20:04:31 +08:00
    @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
    ragnaroks
        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 ,看来以后要改观了
    mxT52CRuqR6o5
        10
    mxT52CRuqR6o5  
       2022-08-01 20:35:22 +08:00
    @lqzhgood 你在 codesandbox 里跑跑看,不要打开控制台,打开控制台浏览器会有一些额外工作影响测量准确性
    codehz
        11
    codehz  
       2022-08-01 20:46:46 +08:00   1
    其实 v8 的很多数组方法都是 js 写的 - 只是标记成 native ,毕竟无论如何都要做各种类型检查和转换(以及按 es 语义调用 proxy getter setter ),加上 es 语义允许数组方法在非数组类型上调用,native 写不会有多少优势 - 反而可能会漏掉一些语义保证)
    lizhenda
        12
    lizhenda  
       2022-08-01 21:06:05 +08:00
    平常还真不会注意,原生居然慢 ...
    Huelse
        13
    Huelse  
       2022-08-01 22:56:11 +08:00
    我反复跑了几次的确 lodash 成绩稍好,第一次跑的话 lodash 只要 92ms

    lodash: 228.31201171875 ms

    es: 292.26806640625 ms

    从表现上来看我觉得和申请内存有关
    vace
        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
    autoxbc
        15
    autoxbc  
       2022-08-02 04:15:14 +08:00
    引擎底层敢用 JS 解释 JS 说明 JIT 性能够好,这是好事
    caisanli
        16
    caisanli  
       2022-08-02 08:00:39 +08:00 via iPhone
    while 比 for 执行更快?
    loolac
        17
    loolac  
       2022-08-02 08:30:11 +08:00
    es: 215.10009765625 ms
    loolac
        18
    loolac  
       2022-08-02 08:30:17 +08:00
    lodash: 173.812744140625 ms
    bthulu
        19
    bthulu  
       2022-08-02 09:14:31 +08:00
    i5 8300H, lodash 比原生快 4-5 倍的样子
    lqzhgood
        20
    lqzhgood  
    OP
       2022-08-02 09:50:34 +08:00
    @ragnaroks 我也是一样,能用原生实现尽量用原生(一是洁癖,二是觉得原生性能最优),现在在一些性能优先的函数可能要额外考虑考虑了。
    lelouchjoshua
        21
    lelouchjoshua  
       2022-08-02 09:57:08 +08:00
    lodash 才是 js 标准库
    cjh1095358798
        22
    cjh1095358798  
       2022-08-02 10:29:03 +08:00
    @vace v8 中的数组方法也是 js 写的吗,为啥不用 c++写呢
    hangbale
        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)实现
    Mutoo
        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
    DICK23
        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
    libook
        26
    libook  
       2022-08-02 16:35:52 +08:00
    曾经很长一段时间,Bluebird 的卖点之一都是比 V8 原生 ES6 的 Promise 性能好,那时候 V8 每次更新我就会跑一下 benchmark ,见证了原生 Promise 实现的性能越来越好,直至超过 Bluebird 。

    估计一些 ES 新特性在引擎里可能会先用 JS 代码简单实现,后面才会再逐渐优化,甚至用 C++重写。
    lujiaosama
        27
    lujiaosama  
       2022-08-02 16:46:53 +08:00
    我用原生不是因为性能, 是因为不想引入 lodash 这个库 , 花里胡哨的 api 一顿操作然后老是被人吐槽看不懂看查文档. 现在就老实用最基础的 map,filter,reduce 来实现功能了.
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3865 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 10:28 PVG 18:28 LAX 03:28 JFK 06:28
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86