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

时间:2015-12-27 13:10:48

标签: javascript performance constructor object-create

背景

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

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

var foo = Object.create(null);

这可以保证新对象没有继承属性,例如“toString”,“constructor”,“__ proto__”,这些特定用例不适用。

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

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

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

var bar = new Empty;

问题

为了提高性能,我编写了一个测试,发现在所有浏览器中,本机Object.create方法的意外执行速度比涉及带有ad hoc原型的额外构造函数的方法慢得多:{{3 }}

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

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

4 个答案:

答案 0 :(得分:14)

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

  • 在Chrome 47 new Empty以63米运算/秒运行,而Object.create(null)以10米运算/秒运行。

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

(上面的“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告诉我操作数/秒是“无限”。

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

现在,在伪地图的生命周期中,创建伪地图的频率是多少?它被访问的频率是多少?除非你处于一个非常特殊的情况,否则地图应该创建一次,但是可以多次访问。 所以点击和未命中的相对表现对你的整体表现来说更重要应用程序,然后是创建对象的各种方法的相对表现。

我们还可以查看从这些伪地图添加和删除键的性能,我们将了解更多信息。然后,也许你有地图,你从来没有删除密钥(我有一些),所以删除性能可能对你的情况不重要。

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

答案 1 :(得分:4)

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

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

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

最终,Object.create(null)是显而易见的解决方案。如果创建对象的性能成为瓶颈,那么也许会考虑使用构造函数,但即便如此,在使用类似 jQuery(document).ready(function() { $("#RefreshButton").click(function() { $("#dynaTree").dynatree("option", "initAjax", { url: "refreshTree", dataType: "json" }); $("#dynaTree").dynatree("option", "children", null); $("#dynaTree").dynatree("getTree").reload(); }); }); 构造函数之类的东西之前,我会先查看其他地方。

答案 2 :(得分:0)

这个问题几乎无效,因为jsperf被破坏了,无论出于何种原因,它都会导致结果出现偏差。我在制作自己的地图实现时(我的一个基于整数)进行了亲自检查。

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

BTW我认为这是一种使用相同语法创建空对象的更简单方法:

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

我写了我自己的小测试,打印时间来创建这三种方法中的任何数量。

这是:

<!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

答案 3 :(得分:0)

  在努力提高性能的同时,我写了一个测试,并发现了   原生Object.create方法意外地执行速度比   涉及带有ad hoc原型的额外构造函数的方法   所有浏览器

     

我非常期待后一种方法比较慢   涉及调用用户定义的构造函数,但不会发生   前一种情况。

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

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

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

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