Lodash核心优化浅析

从何而来

lodash是一个JS遵从函数式思维的工具库,作者John-David Dalton描述其动机是underscore在可读性(易理解)以及性能上是不能满足需求[1], 并且融合函数式编程思维。
作为新兴的工具库,还提供了lodash/fp作为可单独工作的核心模块, 压缩体积约占总体积的1/6,此外还提供了UMD module和lodash-webpack-plugin[2]等周边工具的支持。

Note: 本篇主要讲解分析lodash对JS的核心优化方式,如何使用请参考官方文档[3]

深入实现

1. Lazy Evaluation

1.1 Shortcut Fusion

非严格模式下,优化不需要的执行动作。
首先对于没有shortcut fusion[4]优化的underscore我们定义一个定长度的数组,然后进行mapfiltertake三个操作

// underscore.js
_.chain(_.range(4)).map(v => { console.log('map ---> ', v); return v;}).filter(v => { console.log('filter ---> ', v); return !!(v % 2)}).take(3).value();
// results:
// map --->  0
// map --->  1
// map --->  2
// map --->  3
// filter --->  0
// filter --->  1
// filter --->  2
// filter --->  3

得到的结果是:函数遍历的次数是4+4
接下来,我们看一下进行了short fusing优化的lodash是如何处理的:

// lodash.js
_.chain(_.range(4)).map(v => { console.log('map ---> ', v); return v;}).filter(v => { console.log('filter ---> ', v); return !!(v % 2)}).take(3).value();
// map --->  0
// filter --->  0
// map --->  1
// filter --->  1
// map --->  2
// filter --->  2
// map --->  3
// filter --->  3

同样的操作,函数遍历的次数是3+3,后者会优化掉不必要的函数调用(如map 4 和 filter 4),在数据量比较大并且take的数量比较小的情况效果非常明显。更多的性能对比,可以参考jsPerf上的例子[5]

性能测试结果对比图片

如果,你觉得数据不直观的话,这里有两个动画很好的展示了什么是shortcut fusion(from Filip Zawada's blog[6]和中文对照版[7])

Before Shortcut Fusion

After Shortcut Fusion

实现逻辑:lodash的shortcut核心类是LazyWrapper, 然后通过判断是否使用了可shortcut的函数(Filter,Map),然后在.value()执行时进行优化。
Step 1: 初始化LazyWrapper

// Add `LazyWrapper` methods that accept an `iteratee` value.
    arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {
      var type = index + 1,
        isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;

      LazyWrapper.prototype[methodName] = function(iteratee) {
        var result = this.clone();
        result.__iteratees__.push({
          'iteratee': getIteratee(iteratee, 3),
          'type': type
        });
        result.__filtered__ = result.__filtered__ || isFilter;
        return result;
      };
    });

Step 2: 当使用chain.filter时,标志当前LodashWrapper.filtered为true

// Add `LazyWrapper` methods that accept an `iteratee` value.
    arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {
      var type = index + 1,
        isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;

      LazyWrapper.prototype[methodName] = function(iteratee) {
        var result = this.clone();
        result.__iteratees__.push({
          'iteratee': getIteratee(iteratee, 3),
          'type': type
        });
        result.__filtered__ = result.__filtered__ || isFilter;
        return result;
      };
    });

Step 3: 当使用.take()时,将LodashWrapper转换为LazyWrapper

arrayEach(['drop', 'take'], function(methodName, index) {
      LazyWrapper.prototype[methodName] = function(n) {
        n = n === undefined ? 1 : nativeMax(toInteger(n), 0);

        var result = (this.__filtered__ && !index)
          ? new LazyWrapper(this)
          : this.clone();

        if (result.__filtered__) {
          result.__takeCount__ = nativeMin(n, result.__takeCount__);
        } else {
          result.__views__.push({
            'size': nativeMin(n, MAX_ARRAY_LENGTH),
            'type': methodName + (result.__dir__ < 0 ? 'Right' : '')
          });
        }
        return result;
      };

      LazyWrapper.prototype[methodName + 'Right'] = function(n) {
        return this.reverse()[methodName](n).reverse();
      };
    });

Step 4: 当调用.value()时,对可以shortcut的函数进行优化,优化的方式主要是标签语句[8]

 /**
     * Extracts the unwrapped value from its lazy wrapper.
     *
     * @private
     * @name value
     * @memberOf LazyWrapper
     * @returns {*} Returns the unwrapped value.
     */
    function lazyValue() {
      var array = this.__wrapped__.value(),
        dir = this.__dir__,
        isArr = isArray(array),
        isRight = dir < 0,
        arrLength = isArr ? array.length : 0,
        view = getView(0, arrLength, this.__views__),
        start = view.start,
        end = view.end,
        length = end - start,
        index = isRight ? end : (start - 1),
        iteratees = this.__iteratees__,
        iterLength = iteratees.length,
        resIndex = 0,
        takeCount = nativeMin(length, this.__takeCount__);

      if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
        return baseWrapperValue(array, this.__actions__);
      }
      var result = [];

      outer:
        while (length-- && resIndex < takeCount) {
          index += dir;

          var iterIndex = -1,
            value = array[index];

          while (++iterIndex < iterLength) {
            var data = iteratees[iterIndex],
              iteratee = data.iteratee,
              type = data.type,
              computed = iteratee(value);

            if (type == LAZY_MAP_FLAG) {
              value = computed;
            } else if (!computed) {
              if (type == LAZY_FILTER_FLAG) {
                continue outer;
              } else {
                break outer;
              }
            }
          }
          result[resIndex++] = value;
        }
      return result;
    }

1.2 Shortcut Fusion Functions

以进行shortcut fusion的函数列表, 目前版本v4.17.15

 * The wrapper methods that support shortcut fusion are:
 * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,
 * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,
 * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`

1.3 Pipelining & Deferred Execution

Pipeling好处是避免在链式方法执行中创建中间数组,减少内存压力。
Deferred Execution允许在真正执行操作(如.value()操作)之前并不进行真正执行,这里有利于在执行之前对代码进行优化,特别是哪些多次组合pipeline的数组。

下面我们来看两个例子:

  1. Pipeline严格模式和非严格模式
    Note: 严格模式与非严格模式的区别,请参考我的文章lazy-evaluation[9]
// 定义操作
var fun1, fun2, fun3;
var result = _(source).map(func1).map(func2).map(func3).value();

// 严格模式下可能的代码运行方式
var result = [], temp1 = [], temp2 = [], temp3 = [];
for(var i = 0; i &lt; source.length; i++) {
   temp1[i] = func1(source[i]);
}
for(i = 0; i &lt; source.length; i++) {
   temp2[i] = func2(temp1[i]);
}
for(i = 0; i &lt; source.length; i++) {
   temp3[i] = func3(temp2[i]);
}
result = temp3;

// 非严格模式下可能的代码运行方式
var result = [];
for(var i = 0; i &lt; source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}

不难看出,对于非严格模式下的代码优化,并没有产生中间数组,这对性能提升会大有帮助,尤其是在数据量比较大的情况下。
NOTE: 这里提出另外一个假设,如果func1、func2、func3都为相同操作类型(如Map)时是否可以优化为一次,目前的结果是否定的[10]

  1. Deferred Execution
    lodash为例,在执行.value()操作之前生成的都是chainable的中间变量,这一点与lazyJS的Sequence概念十分相似[11]
_.chain([1, 2, 3]).map(i => i * 2)
// 执行结果: On {__wrapped__: Un, __actions__: Array(0), __chain__: true, __index__: 0, __values__: undefined}
_.chain([1, 2, 3]).map(i => i * 2).value()
// 执行结果:[2, 4, 6]

两次执行的效率差异非常大,如果将过个结果集合并之后在求值,性能提升会非常明显。

var a = _.chain([1, 2, 3]).map(i => i * 2);
var b = (a) => _.chain(a).filter(i => i > 2)
b(a).value();
// 执行结果:[4, 6]

2. Cheap check vs Expansive check

通过先在某些条件下有限使用cheap check来进行优化性能

function baseMatchesProperty(path, srcValue) {
  if (isKey(path) && isStrictComparable(srcValue)) {
    return matchesStrictComparable(toKey(path), srcValue);
  }
  return function(object) {
    var objValue = get(object, path);
    return (objValue === undefined && objValue === srcValue)
      ? hasIn(object, path)
      : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG);
  };
}

3. Inline Caching(ICs)

inline-cahcing内联缓存[12],享受编译器提供的热加载性能提升, 对arrayEach进行缓存[13], 关于V8 ICs的文章也描述了这一点[14]

// ROUDN I
function forEach(collection, iteratee) {
  var func = arrayEach;
  return func(collection, getIteratee(iteratee, 3));
}
function arrayEach(array, iteratee) {
    var index = -1,
      length = array == null ? 0 : array.length;

    while (++index < length) {
      if (iteratee(array[index], index, array) === false) {
        break;
      }
    }
    return array;
}

// ROUND II
function forEach(collection, iteratee) {
  var func = function(array, iteratee) {
       var index = -1,
            length = array == null ? 0 : array.length;
      
          while (++index < length) {
            if (iteratee(array[index], index, array) === false) {
              break;
            }
          }
          return array;
  };
  return func(collection, getIteratee(iteratee, 3));
}

这里比较了两种写法在执行效率上的差异,证明了inline-caching的正确性[13]。但是目前本地使用Nodejs执行benchmark测试失败[15]

4. Do not use Build-Ins

这里举一个使用Array的例子

var arr = Array.apply(null, { length: 100000 }).fill({ id: 1 });

// 测试条件
// 循环次数:10000 次

// 测试结果
// JS#Array x 26.96 ops/sec ±1.46% (48 runs sampled)
// JS#BuildIn-Array x 26.12 ops/sec ±1.26% (47 runs sampled)
// Fastest is JS#Array

// 添加测试
suite
  .add('JS#Array', function() {
    let index = -1,
      length = arr.length,
      result = Array(length);
    while (++index < length) {
      result[index] = iteratee(arr[index]);
    }
    return result;
  })
  .add('JS#BuildIn-Array', function() {
    let index = -1,
      length = arr.length,
      result = [];
    while (++index < length) {
      result.push(iteratee(arr[index]));
    }
    return result;
  })
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ async: true });

const iteratee = function(i) {
  i.id = +i.id.toString() / 2 + '1';
};

引用


  1. https://www.youtube.com/watch?v=cD9utLH3QOk/

  2. https://github.com/lodash/lodash-webpack-plugin#readme

  3. https://lodash.com/

  4. https://kseo.github.io/posts/2016-12-18-short-cut-fusion.html

  5. https://jsperf.com/lodash-lazy-lx

  6. http://filimanjaro.com/blog/2014/introducing-lazy-evaluation/

  7. https://blog.csdn.net/u011511587/article/details/52654998

  8. https://www.cnblogs.com/walls/p/9499454.html

  9. https://www.yansong.fun/2020/03/07/lazy-evaluation/

  10. https://github.com/Cuiyansong/my-react-experiment/blob/master/src/Javascript-Performance/lodash-chain-lazy-evaluation.perf.js

  11. https://www.yansong.fun/2020/03/07/amazing-lazy-js/

  12. http://iacoma.cs.uiuc.edu/iacoma-papers/pldi19_2.pdf

  13. https://jsperf.com/array-length-caching

  14. https://mathiasbynens.be/notes/shapes-ics

  15. https://github.com/Cuiyansong/my-react-experiment/blob/master/src/Javascript-Performance/js-inline-caching.perf.js

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容