17

Background

In a project I'm maintaining we make extensive use of null prototype objects as a poor man's alternative to (string key only) Maps, which are not natively supported in many older, pre-ES6 browsers.

Basically, to create a null prototype object on the fly, one would use:

var foo = Object.create(null);

This guarantees that the new object has no inherited properties, such as "toString", "constructor", "__proto__" which are not desirable for this particular use case.

Since this pattern appears multiple times in code, we came up with the idea of writing a constructor that would create objects whose prototype has a null prototype and no own properties.

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

Then to create an object with no own or inherited properties one can use:

var bar = new Empty;

The problem

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: http://jsperf.com/blank-object-creation.

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.

What could be the cause of such a performance difference?

GOTO 0
  • 42,323
  • 22
  • 125
  • 158

4 Answers4

23

You've been investigating something which is highly dependent on the specific version of the browser you are running. Here are some results I get here when I run your jsperf test:

  • In Chrome 47 new Empty runs at 63m ops/sec whereas Object.create(null) runs at 10m ops/sec.

  • In Firefox 39 new Empty runs at 733m ops/sec whereas Object.create(null) runs at 1,685m ops/sec.

("m" above means we're talking about millions.)

So which one do you pick? The method which is fastest in one browser is slowest in the other.

Not only this, but the results we are looking at here are very likely to change with new browser releases. Case in point, I've checked the implementation of Object.create in v8. Up to December 30th 2015, the implementation of Object.create was written in JavaScript, but a commit recently changed it to a C++ implementation. Once this makes its way into Chrome, the results of comparing Object.create(null) and new Empty are going to change.

But this is not all...

You've looked at only one aspect of using Object.create(null) to create an object that is going to be used as a kind of map (a pseudo-map). What about access times into this pseudo-map? Here is a test that checks the performance of misses and one that checks the performance of hits.

  • On Chrome 47 both the hits and miss cases are 90% faster with an object created with Object.create(null).

  • On Firefox 39, the hit cases all perform the same. As for the miss cases, the performance of an object created with Object.create(null) is so great that jsperf tells me the number of ops/sec is "Infinity".

The results obtained with Firefox 39 are those I was actually expecting. The JavaScript engine should seek the field in the object itself. If it is a hit, then the search is over, no matter how the object was created. If there is a miss on finding the field in the object itself, then the JavaScript engine must check in the object's prototype. In the case of objects created with Object.create(null), there is no prototype so the search ends there. In the case of objects created with new Empty, there is a prototype, in which the JavaScript engine must search.

Now, in the life-time of a pseudo-map how often is the pseudo-map created? How often is it being accessed? Unless you are in a really peculiar situation the map should be created once, but accessed many times. So the relative performance of hits and misses is going to be more important to the overall performance of your application, then the relative performance of the various means of creating the object.

We could also look at the performance of adding and deleting keys from these pseudo-maps, and we'd learn more. Then again, maybe you have maps from which you never remove keys (I've got a few of those) so deletion performance may not be important for your case.

Ultimately, what you should be profiling to improve the performance of your application is your application as a system. In this way, the relative importance of the various operations in your actual application is going to be reflected in your results.

Louis
  • 146,715
  • 28
  • 274
  • 320
  • As a note, setting `Empty2.prototype = null` as you do in your tests causes objects created by `new Empty2` to have prototype `Object.prototype` rather than `null`, which is exactly what I was trying to avoid. Otherwise well done. – GOTO 0 Jan 05 '16 at 21:22
  • Thanks for the bounty and the comment. Regarding `Empty2.prototype = null`, I see that given `var empty2 = new Empty2`, then `empty2 instanceof Object === true`. Whereas given `var empty = new Empty`, then `empty instanceof Object === false`. However, the difference seems academic, because in every single test I've run (including creation tests, see [here](https://jsperf.com/blank-object-creation/3)), using `Empty` or `Empty2` showed roughly the same performance. Is there some consideration I'm missing? – Louis Jan 05 '16 at 21:35
  • 1
    Well, precisely that fact that `Empty2` objects inherit properties like `toString`, `valueOf`, etc. (try `"constructor" in empty` vs. `"constructor" in empty2`). Avoiding predefined key-value pairs in a pseudo-map was the motivation for designing the `Empty` constructor. But the only point of my question here was performance. Thanks for you answer. – GOTO 0 Jan 05 '16 at 22:10
  • Ah, yes that's a fatal flaw. I was misinformed about what `.prototype = null` would do. That's why I tested it. – Louis Jan 05 '16 at 22:54
5

The performance difference has to do with the fact that constructor functions are highly optimized in most JS engines. There's really no practical reason that Object.create couldn't be as fast as constructor functions, it's just an implementation-dependent thing that will likely improve as time goes on.

That being said, all the performance test proves is that you shouldn't be choosing one or the other based on performance because the cost of creating an object is ridiculously low. How many of these maps are you creating? Even the slowest implementation of Object.create on the tests is still chugging out over 8,000,000 objects per second, so unless you have a compelling reasons to create millions of maps, I'd just choose the most obvious solution.

Furthermore, consider the fact that one browser implementation can literally be 100s of times faster than another implementation. This difference is going to exists regardless of which you pick, so the small difference between Object.create and constructors shouldn't really be considered a relevant difference within broader context of different implementations.

Ultimately, Object.create(null) is the obvious solution. If the performance of creating objects becomes a bottleneck, then maybe consider using constructors, but even then I would look elsewhere before I resorted to using something like Empty constructors.

Ethan Lynn
  • 1,009
  • 6
  • 13
  • 1
    From looking at the browser specific test results for his test case, your note about "Browser differences are huge" makes this pretty clear. Firefox 43 runs both tests with roughly the same (absurdly fast) speed, 20x-200x faster than any other browser tested. Odds are, Firefox's allocator just happens to handle the case of creating and deleting trivial objects better than most (or it's just recognizing the redundant work and optimizing it to a single assignment avoiding work that is just discarded); a test this simple is too easy to optimize out of existence after all. – ShadowRanger Jan 05 '16 at 02:30
  • So, constructors are faster because they are better optimized in JS engines, ok. But why are they better optimized, is it because they are used more frequently, so they are considered a better target of optimization? If there's no practical reason that `Object.create` couldn't be as fast as constructor functions, then why is it not? At this point, your argument becomes a tautology. Also, could you elaborate a bit on what else than `Object.create(null)` one would look for before resorting to `Empty`? I can think of one or two alternatives, but I don't see any benefits. – GOTO 0 Jan 05 '16 at 10:08
1

This question is pretty much invalid, because jsperf is broken, it skews results for whatever reason. I checked it personally when I was making my own map implementation ( one based on integers ).

There is purely no difference between these two methods.

BTW I think this an easier way to create an empty object with the same syntax:

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

I wrote my little own test that prints the time to create whatever amount of these 3 methods.

Here it is:

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

I am sorry, it is a bit rigid. Just paste it into an index.html and run. I think this method of testing is far superior to jsperf.

Here are my results:

|createEmpties| started: 1451996562280 --- ended: 1451996563073 --- time: 793
|createEmptiesV2| started: 1451996563181 --- ended: 1451996564033 --- time: 852
|createNullObjects| started: 1451996564148 --- ended: 1451996564980 --- time: 832


|createEmpties| started: 1451996565085 --- ended: 1451996565926 --- time: 841
|createEmptiesV2| started: 1451996566035 --- ended: 1451996566863 --- time: 828
|createNullObjects| started: 1451996566980 --- ended: 1451996567872 --- time: 892

|createEmpties| started: 1451996567986 --- ended: 1451996568839 --- time: 853
|createEmptiesV2| started: 1451996568953 --- ended: 1451996569786 --- time: 833
|createNullObjects| started: 1451996569890 --- ended: 1451996570713 --- time: 823

|createEmpties| started: 1451996570825 --- ended: 1451996571666 --- time: 841
|createEmptiesV2| started: 1451996571776 --- ended: 1451996572615 --- time: 839
|createNullObjects| started: 1451996572728 --- ended: 1451996573556 --- time: 828

|createEmpties| started: 1451996573665 --- ended: 1451996574533 --- time: 868
|createEmptiesV2| started: 1451996574646 --- ended: 1451996575476 --- time: 830
|createNullObjects| started: 1451996575582 --- ended: 1451996576427 --- time: 845

|createEmpties| started: 1451996576535 --- ended: 1451996577361 --- time: 826
|createEmptiesV2| started: 1451996577470 --- ended: 1451996578317 --- time: 847
|createNullObjects| started: 1451996578422 --- ended: 1451996579256 --- time: 834

|createEmpties| started: 1451996579358 --- ended: 1451996580187 --- time: 829
|createEmptiesV2| started: 1451996580293 --- ended: 1451996581148 --- time: 855
|createNullObjects| started: 1451996581261 --- ended: 1451996582098 --- time: 837

|createEmpties| started: 1451996582213 --- ended: 1451996583071 --- time: 858
|createEmptiesV2| started: 1451996583179 --- ended: 1451996583991 --- time: 812
|createNullObjects| started: 1451996584100 --- ended: 1451996584948 --- time: 848

|createEmpties| started: 1451996585052 --- ended: 1451996585888 --- time: 836
|createEmptiesV2| started: 1451996586003 --- ended: 1451996586839 --- time: 836
|createNullObjects| started: 1451996586954 --- ended: 1451996587785 --- time: 831

|createEmpties| started: 1451996587891 --- ended: 1451996588754 --- time: 863
|createEmptiesV2| started: 1451996588858 --- ended: 1451996589702 --- time: 844
|createNullObjects| started: 1451996589810 --- ended: 1451996590640 --- time: 830

  • 1
    I think you don't need the new operator to call `EmptyV2()`. For performance comparisons to be reliable, they should run in separate contexts, like hidden frames, web workers or similar. Other than that, you may be right that jsPerf is broken, but without even knowing the browser you used to run your test, it will be difficult to validate your results. – GOTO 0 Jan 05 '16 at 15:22
  • I am pretty sure you are right, that you do not need new operator, but you can use it, and it works just fine. I am using Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0. Of course, you're right that the code I pasted does not demonstrate real world performance, but it demonstrates it better than jsperf. You can run that code yourself too, to see the results you get. It will launch by itself and continue for 10 iterations, until you get as many results as me. BTW, I am also almost entirely sure that the performance gap between chrome and firefox is BS. – user1234141515134321321513 Jan 05 '16 at 15:34
  • What Louis is saying is pretty much correct, but he seems to trust jsperf, jsperf is really wrong, I have tested those things, it goes insane. Hits and Misses do matte quite a lot, but there is yet another big factor, and it is the size of the map. Anything that goes above a 1000 in either FF or CH is going to be around 100x slower, just like that, you should consider that. I could give you a solution to that, but I gotta go, be back in two days. OH YEAH, IE's perfomance of any of this methods IS AWFUL. – user1234141515134321321513 Jan 05 '16 at 15:51
0

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.

Your reasoning postulates that the new operator and Object.create have to use the same inner "object creation" code, with an extra call to the custom constructor for new. That's why you find the test result surprising, because you think you're comparing A+B with A.

But that's not true, you shouldn't assume that much about the implementations of new and Object.create. Both can resolve to different JS or "native" (mostly C++) and your custom constructor can easily be optimized away by the parser.

Beside curiosity, as others have well explained, the empty object creation is a bad focus point for optimizing the entire application - unless you've got some full scale profiling data proving otherwise.

If you're really worried about objet creation time, add a counter for the number of objects created, increment it in your Empty constructor, log the number of objects created in the lifetime of the program, multiply by the slowest browser execution time, and see (most probably) how negligible creation time is.

Ilya
  • 5,377
  • 2
  • 18
  • 33