为什么 Object.create 比构造函数慢这么多?

Why is Object.create so much slower than a constructor?

背景

在我维护的一个项目中,我们广泛使用 null 原型对象作为(仅字符串键)地图的穷人替代品,许多旧的 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.

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

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

性能差异与构造函数在大多数 JS 引擎中高度优化的事实有关。 Object.create 确实没有构造函数那么快的实际原因,它只是一个 implementation-dependent 可能会随着时间的推移而改进的东西。

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

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

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

这个问题几乎是无效的,因为 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


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

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

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

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

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

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

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

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

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

您一直在调查高度依赖于您 运行 的特定浏览器版本的内容。这是我 运行 你的 jsperf 测试时得到的一些结果:

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

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

(上面的"m"意味着我们谈论的是数百万。)

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

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

但这还不是全部...

您只看了一个方面 使用 Object.create(null) 创建将用作一种地图的对象(pseudo-map).这个 pseudo-map 的访问时间如何?这是检查 misses and one that checks the performance of 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 引擎必须在其中搜索。

现在,在 pseudo-map 的 life-time 中,pseudo-map 的创建频率是多少?它多久被访问一次?除非您处于非常特殊的情况,否则地图应该 创建一次,但访问多次。 因此,命中和未命中的相对性能对于您的应用程序的整体性能,然后是创建对象的各种方法的相对性能。

我们还可以查看在这些 pseudo-map 中添加和删除键的性能,我们会了解更多信息。话又说回来,也许您的地图从未删除过键(我有一些),因此删除性能对您的情况可能并不重要。

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

In a strive to improve performance, I wrote a test, and found that the native Object.create approach unexpectedly performs much slower than the method involving an extra constructor with an ad hoc prototype, in all browsers

I was ingenuously expecting the latter method to be slower as it involves invoking a user defined constructor, which doesn't happen in the former case.

您的推理假设 new 运算符和 Object.create 必须使用相同的内部 "object creation" 代码,并额外调用 new 的自定义构造函数。这就是为什么你觉得测试结果令人惊讶的原因,因为你认为你是在比较 A+B 和 A。

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

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

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