15

背景

在我维护的一个项目中,我们广泛使用 null 原型对象作为(仅限字符串键)Maps 的穷人替代品,许多旧的 ES6 之前的浏览器本身并不支持这些对象。

基本上,要动态创建一个空原型对象,可以使用:

var foo = Object.create(null);

这保证了新对象没有继承的属性,例如“toString”、“constructor”、“__proto__”,这些属性对于这个特定的用例来说是不可取的。

由于这种模式在代码中多次出现,我们想出了编写一个构造函数的想法,该构造函数将创建其原型具有空原型且没有自己的属性的对象。

var Empty = function () { };
Empty.prototype = Object.create(null);

然后创建一个没有自己或继承属性的对象,可以使用:

var bar = new Empty;

问题

为了提高性能,我编写了一个测试,发现Object.create在所有浏览器中,本机方法的执行速度出乎意料地比涉及具有临时原型的额外构造函数的方法慢得多:http: //jsperf.com/blank-object -创造

我天真地期望后一种方法会更慢,因为它涉及调用用户定义的构造函数,这在前一种情况下不会发生。

造成这种性能差异的原因可能是什么?

4

4 回答 4

20

您一直在研究高度依赖于您正在运行的浏览器的特定版本的东西。以下是我在运行 jsperf 测试时得到的一些结果:

  • 在 Chrome 47new Empty中以 63m ops/secObject.create(null)运行,而以 10m ops/sec 运行。

  • 在 Firefox 39new Empty中,运行速度为 733m ops/sec,而Object.create(null)运行速度为 1,685m ops/sec。

(上面的“m”表示我们谈论的是数百万。)

那你选哪一个?在一种浏览器中最快的方法在另一种浏览器中最慢。

不仅如此,我们在这里看到的结果很可能会随着新的浏览器版本而改变。举个例子,我检查了Object.createv8 中的实现。截至 2015 年 12 月 30 日,实现Object.create是用 JavaScript 编写的,但最近一次提交将其更改为 C++ 实现。一旦这进入 Chrome,比较的结果Object.create(null)就会new Empty发生变化。

但这并不是全部...

您只看到了用于创建将用作一种地图(伪地图)的对象的一个​​方面Object.create(null)。这个伪地图的访问时间呢?这是一个检查未命中性能的测试一个检查命中性能的测试。

  • 在 Chrome 47 上,使用Object.create(null).

  • 在 Firefox 39 上,命中案例的表现都是一样的。至于未命中的情况,使用创建的对象的性能Object.create(null)非常好,以至于 jsperf 告诉我 ops/sec 的数量是“Infinity”。

使用 Firefox 39 获得的结果是我真正期待的结果。JavaScript 引擎应该在对象本身中寻找字段。如果它是一个命中,那么搜索就结束了,不管对象是如何创建的。如果在对象本身中找不到字段,则 JavaScript 引擎必须检查对象的原型。对于使用 创建的对象Object.create(null),没有原型,因此搜索到此结束。对于使用 创建的对象new Empty,有一个原型,JavaScript 引擎必须在其中搜索。

现在,在伪地图的生命周期中,伪地图多久创建一次?它多久被访问一次?除非您处于非常特殊的情况,否则应该创建一次地图,但可以多次访问。 因此,命中和未命中的相对性能对于应用程序的整体性能将更为重要,然后是创建对象的各种方法的相对性能。

我们还可以查看从这些伪映射中添加和删除键的性能,我们会了解更多。再说一次,也许你有一些你从不删除键的地图(我有一些),所以删除性能对你的情况可能并不重要。

最终,为了提高应用程序的性能,您应该分析的是您的应用程序作为一个系统这样,您的实际应用程序中各种操作的相对重要性将反映在您的结果中。

于 2016-01-05T12:44:29.690 回答
5

性能差异与构造函数在大多数 JS 引擎中都经过高度优化这一事实有关。Object.create 不能像构造函数那样快并没有实际的原因,它只是一个依赖于实现的东西,随着时间的推移可能会改进。

话虽如此,所有性能测试都证明,您不应该根据性能来选择其中一个,因为创建对象的成本低得离谱。您要创建多少张这样的地图?即使在测试中最慢的 Object.create 实现仍然每秒输出超过 8,000,000 个对象,所以除非您有令人信服的理由来创建数百万张地图,否则我只会选择最明显的解决方案。

此外,考虑一个浏览器实现实际上可以比另一种实现快 100 倍这一事实。无论您选择哪个,这种差异都会存在,因此 Object.create 和构造函数之间的微小差异不应真正被视为在不同实现的更广泛上下文中的相关差异。

最终, Object.create(null) 是显而易见的解决方案。如果创建对象的性能成为瓶颈,那么也许可以考虑使用构造函数,但即便如此,在我求助于构造函数之类的东西之前,我也会去别处看看Empty

于 2016-01-05T02:04:19.760 回答
1

这个问题几乎是无效的,因为 jsperf 被破坏了,无论出于何种原因,它都会扭曲结果。当我制作自己的地图实现(基于整数的地图)时,我亲自检查了它。

这两种方法完全没有区别。

顺便说一句,我认为这是使用相同语法创建空对象的更简单方法:

var EmptyV2 = function() { return Object.create(null); };

我编写了自己的小测试,打印出创建这 3 种方法的任意数量的时间。

这里是:

<!DOCTYPE html>
<html>
    <head>
        <style>
            html
            {
                background-color: #111111;
                color: #2ECC40;
            }
        </style>
    </head>
    <body>
    <div id="output">

    </div>

    <script type="text/javascript">
        var Empty = function(){};
        Empty.prototype = Object.create(null);

        var EmptyV2 = function() { return Object.create(null); };

        var objectCreate = Object.create;

        function createEmpties(iterations)
        {           
            for(var i = 0; i < iterations; i++)
            {           
                var empty = new Empty();
            }
        }

        function createEmptiesV2(iterations)
        {       
            for(var i = 0; i < iterations; i++)
            {
                var empty = new EmptyV2();
            }
        }

        function createNullObjects(iterations)
        {       
            for(var i = 0; i < iterations; i++)
            {
                var empty = objectCreate(null);
            }
        }

        function addResult(name, start, end, time)
        {           
            var outputBlock = document.getElementsByClassName("output-block");

            var length = (!outputBlock ? 0 : outputBlock.length) + 1;
            var index = length % 3;

            console.log(length);
            console.log(index);

            var output = document.createElement("div");
            output.setAttribute("class", "output-block");
            output.setAttribute("id", ["output-block-", index].join(''));
            output.innerHTML = ["|", name, "|", " started: ", start, " --- ended: ", end, " --- time: ", time].join('');

            document.getElementById("output").appendChild(output);

            if(!index)
            {
                var hr = document.createElement("hr");
                document.getElementById("output").appendChild(hr);
            }
        }

        function runTest(test, iterations)
        {
            var start = new Date().getTime();

            test(iterations);

            var end = new Date().getTime();

            addResult(test.name, start, end, end - start);
        }

        function runTests(tests, iterations)
        {
            if(!tests.length)
            {
                if(!iterations)
                {
                    return;
                }

                console.log(iterations);

                iterations--;

                original = [createEmpties, createEmptiesV2, createNullObjects];

                var tests = [];

                for(var i = 0; i < original.length; i++)
                {
                    tests.push(original[i]);
                }
            }

            runTest(tests[0], 10000000000/8);

            tests.shift();

            setTimeout(runTests, 100, tests, iterations);
        }

        runTests([], 10);
    </script>
    </body>
</html>

对不起,有点生硬。只需将其粘贴到 index.html 中并运行。我认为这种测试方法远远优于jsperf。

这是我的结果:

|createEmpties| 开始:1451996562280 --- 结束:1451996563073 --- 时间:793
|createEmptiesV2| 开始:1451996563181 ---结束:1451996564033 ---时间:852
|createNullObjects| 开始:1451996564148 ---结束:1451996564980 ---时间:832


|createEmpties| 开始:1451996565085 --- 结束:1451996565926 --- 时间:841
|createEmptiesV2| 开始:1451996566035 ---结束:1451996566863 ---时间:828
|createNullObjects| 开始:1451996566980 ---结束:1451996567872 ---时间:892

|createEmpties| 开始:1451996567986 --- 结束:1451996568839 --- 时间:853
|createEmptiesV2| 开始:1451996568953 --- 结束:1451996569786 --- 时间:833
|createNullObjects| 开始:1451996569890 ---结束:1451996570713 ---时间:823

|createEmpties| 开始:1451996570825 --- 结束:1451996571666 --- 时间:841
|createEmptiesV2| 开始:1451996571776 --- 结束:1451996572615 --- 时间:839
|createNullObjects| 开始:1451996572728 ---结束:1451996573556 ---时间:828

|createEmpties| 开始:1451996573665 --- 结束:1451996574533 --- 时间:868
|createEmptiesV2| 开始:1451996574646 ---结束:1451996575476 ---时间:830
|createNullObjects| 开始:1451996575582 ---结束:1451996576427 ---时间:845

|createEmpties| 开始:1451996576535 --- 结束:1451996577361 --- 时间:826
|createEmptiesV2| 开始:1451996577470 --- 结束:1451996578317 --- 时间:847
|createNullObjects| 开始:1451996578422 ---结束:1451996579256 ---时间:834

|createEmpties| 开始:1451996579358 --- 结束:1451996580187 --- 时间:829
|createEmptiesV2| 开始:1451996580293 ---结束:1451996581148 ---时间:855
|createNullObjects| 开始:1451996581261 ---结束:1451996582098 ---时间:837

|createEmpties| 开始:1451996582213 --- 结束:1451996583071 --- 时间:858
|createEmptiesV2| 开始:1451996583179 ---结束:1451996583991 ---时间:812
|createNullObjects| 开始:1451996584100 ---结束:1451996584948 ---时间:848

|createEmpties| 开始:1451996585052 ---结束:1451996585888 ---时间:836
|createEmptiesV2| 开始:1451996586003 --- 结束:1451996586839 --- 时间:836
|createNullObjects| 开始:1451996586954 ---结束:1451996587785 ---时间:831

|createEmpties| 开始:1451996587891 --- 结束:1451996588754 --- 时间:863
|createEmptiesV2| 开始:1451996588858 ---结束:1451996589702 ---时间:844
|createNullObjects| 开始:1451996589810 ---结束:1451996590640 ---时间:830

于 2016-01-05T12:43:01.883 回答
0

为了提高性能,我编写了一个测试,发现在所有浏览器中,原生 Object.create 方法的执行速度比涉及带有临时原型的额外构造函数的方法要慢得多

我天真地期望后一种方法会更慢,因为它涉及调用用户定义的构造函数,这在前一种情况下不会发生。

您的推理假定new运算符Object.create必须使用相同的内部“对象创建”代码,并额外调用new. 这就是为什么您会发现测试结果令人惊讶,因为您认为您正在将 A+B 与 A 进行比较。

new但这不是真的,你不应该对and的实现做太多假设Object.create。两者都可以解析为不同的 JS 或“本机”(主要是 C++),并且您的自定义构造函数可以很容易地被解析器优化掉。

除了好奇之外,正如其他人已经很好解释的那样,创建空对象是优化整个应用程序的一个不好的焦点——除非你有一些完整的分析数据证明不是这样。

如果您真的担心对象的创建时间,请为创建的对象数量添加一个计数器,在Empty构造函数中将其递增,记录程序生命周期中创建的对象数量,乘以最慢的浏览器执行时间,然后查看(很可能)创建时间可以忽略不计。

于 2016-01-05T18:09:36.277 回答