【问题标题】:Why is Object.create so much slower than a constructor?为什么 Object.create 比构造函数慢得多?
【发布时间】:2015-12-27 13:10:48
【问题描述】:

背景

在我维护的一个项目中,我们广泛使用 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-creation

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

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

【问题讨论】:

标签: javascript performance constructor object-create


【解决方案1】:

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

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

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

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

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

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

但这还不是全部……

您只了解了使用Object.create(null) 创建将用作一种地图(伪地图)的对象的一个​​方面。这个伪地图的访问时间呢?这是一个检查misses 性能的测试和一个检查hits 性能的测试。

  • 在 Chrome 47 上,使用 Object.create(null) 创建的对象的命中和未命中情况都快了 90%。

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

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

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

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

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

【讨论】:

  • 请注意,在测试中设置Empty2.prototype = null 会导致new Empty2 创建的对象具有原型Object.prototype 而不是null,这正是我试图避免的.否则做得很好。
  • 感谢赏金和评论。关于Empty2.prototype = null,我看到给定var empty2 = new Empty2,然后是empty2 instanceof Object === true。而给定var empty = new Empty,然后是empty instanceof Object === false。但是,差异似乎是学术性的,因为在我运行的每个测试中(包括创建测试,请参阅here),使用EmptyEmpty2 显示大致相同的性能。有什么我遗漏的考虑吗?
  • 嗯,正是Empty2 对象继承了toStringvalueOf 等属性的事实(尝试"constructor" in empty"constructor" in empty2)。避免在伪映射中预定义键值对是设计Empty 构造函数的动机。但我这里唯一的问题是性能。谢谢你的回答。
  • 啊,是的,这是一个致命的缺陷。我被误导了 .prototype = null 会做什么。这就是我测试它的原因。
【解决方案2】:

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

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

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

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

【讨论】:

  • 通过查看他的测试用例的特定于浏览器的测试结果,您关于“浏览器差异很大”的注释清楚地说明了这一点。 Firefox 43 以大致相同(快得离谱)的速度运行这两项测试,比任何其他测试的浏览器快 20 倍至 200 倍。奇怪的是,Firefox 的分配器恰好比大多数处理创建和删除琐碎对象的情况更好(或者它只是识别冗余工作并将其优化为单个分​​配,避免刚刚丢弃的工作);毕竟,这么简单的测试太容易优化不存在了。
  • 因此,构造函数更快,因为它们在 JS 引擎中得到了更好的优化,好的。但是为什么它们会更好地优化,是因为它们被使用得更频繁,所以它们被认为是更好的优化目标吗?如果没有实际原因 Object.create 不能像构造函数一样快,那为什么不呢?在这一点上,你的论点变成了重言式。另外,您能否详细说明在使用Empty 之前,除了Object.create(null) 还会寻找什么?我可以想到一两个替代方案,但我没有看到任何好处。
【解决方案3】:

这个问题几乎是无效的,因为 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
|创建空对象|开始:1451996564148 --- 结束:1451996564980 --- 时间:832


|createEmpties|开始:1451996565085 --- 结束:1451996565926 --- 时间:841
|createEmptiesV2|开始:1451996566035 --- 结束:1451996566863 --- 时间:828
|创建空对象|开始:1451996566980 --- 结束:1451996567872 --- 时间:892

|createEmpties|开始:1451996567986 --- 结束:1451996568839 --- 时间:853
|createEmptiesV2|开始:1451996568953 --- 结束:1451996569786 --- 时间:833
|创建空对象|开始:1451996569890 --- 结束:1451996570713 --- 时间:823

|createEmpties|开始:1451996570825 --- 结束:1451996571666 --- 时间:841
|createEmptiesV2|开始:1451996571776 --- 结束:1451996572615 --- 时间:839
|创建空对象|开始:1451996572728 --- 结束:1451996573556 --- 时间:828

|createEmpties|开始:1451996573665 --- 结束:1451996574533 --- 时间:868
|createEmptiesV2|开始:1451996574646 --- 结束:1451996575476 --- 时间:830
|创建空对象|开始:1451996575582 --- 结束:1451996576427 --- 时间:845

|createEmpties|开始:1451996576535 --- 结束:1451996577361 --- 时间:826
|createEmptiesV2|开始:1451996577470 --- 结束:1451996578317 --- 时间:847
|创建空对象|开始:1451996578422 --- 结束:1451996579256 --- 时间:834

|createEmpties|开始:1451996579358 --- 结束:1451996580187 --- 时间:829
|createEmptiesV2|开始:1451996580293 --- 结束:1451996581148 --- 时间:855
|创建空对象|开始:1451996581261 --- 结束:1451996582098 --- 时间:837

|createEmpties|开始:1451996582213 --- 结束:1451996583071 --- 时间:858
|createEmptiesV2|开始:1451996583179 --- 结束:1451996583991 --- 时间:812
|创建空对象|开始:1451996584100 --- 结束:1451996584948 --- 时间:848

|createEmpties|开始:1451996585052 --- 结束:1451996585888 --- 时间:836
|createEmptiesV2|开始:1451996586003 --- 结束:1451996586839 --- 时间:836
|创建空对象|开始:1451996586954 --- 结束:1451996587785 --- 时间:831

|createEmpties|开始:1451996587891 --- 结束:1451996588754 --- 时间:863
|createEmptiesV2|开始:1451996588858 --- 结束:1451996589702 --- 时间:844
|创建空对象|开始:1451996589810 --- 结束:1451996590640 --- 时间:830

【讨论】:

  • 我想你不需要新接线员来电EmptyV2()。为了使性能比较可靠,它们应该在不同的上下文中运行,例如隐藏框架、网络工作者或类似的。除此之外,您可能是正确的 jsPerf 已损坏,但即使不知道您用于运行测试的浏览器,也很难验证您的结果。
  • 我很确定你是对的,你不需要 new 运算符,但你可以使用它,它工作得很好。我正在使用 Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0。当然,你是对的,我粘贴的代码并没有展示真实世界的性能,但它比 jsperf 更好地展示了它。您也可以自己运行该代码,以查看获得的结果。它会自行启动并持续 10 次迭代,直到你得到和我一样多的结果。顺便说一句,我也几乎完全确定 chrome 和 firefox 之间的性能差距是 BS。
  • Louis 说的非常正确,但他似乎信任 jsperf,jsperf 真的错了,我已经测试过那些东西,它疯了。 Hits and Misses 确实很重要,但还有另一个重要因素,那就是地图的大小。在 FF 或 CH 中超过 1000 的任何东西都会慢 100 倍左右,就像那样,你应该考虑到这一点。我可以给你一个解决方案,但我得走了,两天后回来。哦,是的,IE 对任何这种方法的表现都很棒。
【解决方案4】:

为了提高性能,我写了一个测试,发现 本机 Object.create 方法出乎意料地执行比 该方法涉及具有临时原型的额外构造函数,在 所有浏览器

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-10
    • 1970-01-01
    • 2021-04-01
    • 1970-01-01
    • 2017-12-26
    • 2011-01-25
    • 2012-12-20
    相关资源
    最近更新 更多