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 · 5484 次点击
    这是一个创建于 843 天前的主题,其中的信息可能已经有所发展或是发生改变。

    今天写一个性能敏感的函数发现的这个有趣结果,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   ·   实用小工具   ·   2090 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:13 · PVG 00:13 · LAX 08:13 · JFK 11:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.