JavaScript中Array.map的性能表现

17

我在jsperf中编写了一些测试用例,来测试使用Array.map和其他替代方法时命名函数和匿名函数之间的区别。

http://jsperf.com/map-reduce-named-functions

(请原谅这个网址名称,这里没有测试Array.reduce,我在完全确定我想要测试什么之前就已经给测试命名了)

一个简单的for/while循环显然是最快的,但我还是对Array.map慢了10倍以上感到惊讶...

然后我尝试了Mozilla的polyfillhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Polyfill

Array.prototype.map = function(fun /*, thisArg */)
{
    "use strict";

    if (this === void 0 || this === null)
        throw new TypeError();

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== "function")
        throw new TypeError();

    var res = new Array(len);
    var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
    for (var i = 0; i < len; i++)
    {
        // NOTE: Absolute correctness would demand Object.defineProperty
        //       be used.  But this method is fairly new, and failure is
        //       possible only if Object.prototype or Array.prototype
        //       has a property |i| (very unlikely), so use a less-correct
        //       but more portable alternative.
        if (i in t)
            res[i] = fun.call(thisArg, t[i], i, t);
    }

    return res;
};

然后我尝试了一个我自己编写的简单实现...
Array.prototype.map3 = function(callback /*, thisArg */) {
    'use strict';
    if (typeof callback !== 'function') {
        throw new TypeError();
    }

    var thisArg = arguments.length >= 2 ? arguments[1] : void 0;

    for (var i = 0, len = this.length; i < len; i++) {
        this[i] = callback.call(thisArg, this[i], i, this);
    };
};

结果概述:

从最快到最慢:

  1. 对于简单/while循环(大致相同)
  2. Map3(我自己的实现)
  3. Map2(Mozilla polyfill)
  4. Array.map
  5. for in

观察结果

有趣的是,命名函数通常比匿名函数快一点(约为5%)。但我注意到在Firefox中,使用命名函数时polyfill比较慢,但在Chrome中更快,但Chrome自己的map实现使用命名函数更慢...我进行了大约10次测试,因此即使这不是非常强烈的测试(jsperf已经完成),除非我的运气那么好,否则应该足以作为指导方针。

此外,Chrome的map函数在我的机器上比Firefox慢多达2倍。真没想到。

还有... Firefox自己的Array.map实现比Mozilla Polyfill慢...哈哈

我不确定ECMA-262规范为什么要说明map可以用于除数组之外的对象(http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.19)。这使得整个map函数变慢了3-4倍(如我的测试所示),因为您需要在每个循环中检查属性是否存在...

结论

如果考虑到不同的浏览器表现略有不同,那么命名函数和匿名函数之间并没有太大区别。

归根结底,我们不应该过于微观优化,但我发现这很有趣 :)


1
有趣的一点是,命名函数通常比使用匿名函数要快一点。当然,代码会多次创建匿名函数。因此,您的性能测试并不正确,因为它们具有一些额外的噪声。 - zerkms
1
这里有一个更好的测试:http://jsperf.com/map-reduce-named-functions/2。 - zerkms
1
“噪音会从哪里产生” --- “运行地图” vs “运行地图 + 创建匿名函数”。后者预计会更慢。 - zerkms
1
因为测试用例运行多次。如果你将函数定义从其中拿出来 - 它只被定义一次。所以你会测量不可比较的情况。这就好像你拿两辆完全相同的汽车,从其中一辆上取下所有的轮子,然后用错误的颜色解释它的速度较慢(而不是解释为缺少轮子)。 - zerkms
2
@SanderElias 感谢您的观察。但即使使用您修改后的测试,使用 Array.map 仍然比简单的 for 循环慢得多。无论如何,正如之前的评论所提到的,我的测试偏离了我的原始意图,因此请以此为参考 :P - Populus
显示剩余13条评论
2个回答

2

首先,这并不是一个公平的比较。正如您所说,适当的 JavaScript Map 可以使用对象,而不仅仅是数组。因此,您基本上正在比较两个完全不同的函数,具有不同的算法/结果/内部工作方式。

当然,适当的 JavaScript Map 更慢 - 它被设计成在比简单的 for 循环遍历数组更大的领域中工作。


1

我想分享一些关于IT技术的研究结果,这对您很有用。我曾经遇到过使用.map时速度非常慢的问题。将它切换为哈希表后,速度大大提高。

我们需要对200k个小对象进行映射操作,将map函数转换为一个哈希对象并重新创建数组,从而将运行时间从20分钟降低到0.4秒

第一种方法(20分钟):

const newArr = arr1.map((obj) => {
  const context1 = arr2.find(o => o.id === obj.id)
  const context2 = arr3.find(o => o.id === obj.id)
  return { ...obj, context1, context2 }
})

新方法

const newArrObj = {}
arr1.forEach(o => newArrObj[o.id] = o)
arr2.forEach(o => newArrObj[o.id].context1 = o)
arr3.forEach(o => newArrObj[o.id].context2 = o)

const users = []
arr1.forEach(o => users[users.length] = newArrObj[o.id])

1
你从O(N^2)的解法转变为了O(N)的解法,然后又在每次迭代中重新声明了两个匿名函数,这使得第一种方法本来就不够优化。为了使它更相关于仅使用map的不同方法,你可以将回调函数更改为预先声明(命名)的函数,并查看是否有很大的区别。你展示的是算法上的改变,而不是map的不同实现/用法,这仍然非常重要,也是你的情况下的正确解决方案。 - Populus
@Populus 很好的观点,我最终确实简化了查找函数,只是在字符串数组中查找一个字符串,没有想到真正将其作为预声明对象,我相信这会有很大的改进。我确实看到重新创建对象需要一些时间,所以第二种方法中仅通过添加修改newArrObj仍然比减少O(N^2)复杂度要好。但是非常好的观点! - Kevin Danikowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接