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?
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 whereasObject.create(null)
runs at 10m ops/sec.In Firefox 39
new Empty
runs at 733m ops/sec whereasObject.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.
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.
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
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.
来源:https://stackoverflow.com/questions/34480709/why-is-object-create-so-much-slower-than-a-constructor