Skip to content

Instantly share code, notes, and snippets.

@apparentlymart
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apparentlymart/bbbf3bcf0b575ce66437 to your computer and use it in GitHub Desktop.
Save apparentlymart/bbbf3bcf0b575ce66437 to your computer and use it in GitHub Desktop.
JavaScript (V8) benchmarking strategies for cloning a known object

Fastest Way to Clone a Known Object in JavaScript (V8)

This is a benchmark of various different strategies for creating clones of an object whose structure is known ahead of time.

In this test we assume that we always want to produce a clone of the same object and that the object contains no circular references, outside pointers, or non-JSON-serializable objects.

The use-case for such cloning is to have a "template" data structure that then gets copied and mutated for each request, leaving the template untouched. This is useful if only a small part of the data structure gets altered on each request and most of it remains set to what is in the template.

In all cases the functions were written to do as much as possible ahead of time so that the copy function could execute as quickly as possible. (Some variants on pre-processing were tried and eliminated before arriving at this final test set. The rejected variants are not reflected in the results or the source code.)

Test Results

All results are over 10,000,000 iterations.

Test Copy Write Read
json 23210ms 145ms 173ms
manual 11669ms 194ms 304ms
inherit 11880ms 67ms 337ms
function 315ms 61ms 288ms

Copying by pre-compiling to a function is the clear winner, presumably because it reduces to the evaluation of an object literal which V8 is tuned to do fast due to it being a very common construct in idiomatic JavaScript.

Why does writing get faster and reading get slower?

Didn't get to the bottom of this exactly, but tried running the tests again over 100,000,000 iterations to get a bit more resolution, and the trend remained.

Reversed the order of running the tests and the progression did not reverse along with it, suggesting that those numbers have nothing to do with the copying strategy and more to do with how V8 optimizes (or rather, de-optimizes) the test code as we introduce more differently-shaped objects into the mix.

Thus the Write and Read times are not meaningful for the purpose of this test, except to say that there seems to be negligible difference between reading and writing the object structures produced by each of these functions.

To re-validate the "copy" results, re-ran those in reverse order too and found that in this case the trend remained, suggesting that the differences here are significant.

function makeJsonCopy(obj) {
var str = JSON.stringify(obj);
return function jsonCopy() {
return JSON.parse(str);
};
}
function makeManualCopy(obj) {
var srcAttribs = obj.attribs;
if (obj.attribs) {
var srcAttrNames = Object.keys(obj.attribs);
var srcAttrsLength = srcAttrNames.length;
var hasSrcAttribs = srcAttrsLength > 0;
}
else {
var srcAttrNames = [];
var srcAttrsLength = 0;
var hasSrcAttribs = false;
}
return function manualCopy() {
var ret = {
type: obj.type,
name: obj.name
};
if (hasSrcAttribs) {
var retAttribs = {};
for (var i = 0; i < srcAttrsLength; i++) {
var attrName = srcAttrNames[i];
retAttribs[attrName] = srcAttribs[attrName];
}
ret.attribs = retAttribs;
}
return ret;
};
}
function makeInheritCopy(obj) {
var attribs = obj.attribs;
return function inheritCopy() {
var ret = Object.create(obj, {});
if (attribs) {
ret.attribs = Object.create(attribs, {});
}
return ret;
};
}
function makeFunctionCopy(obj) {
var str = JSON.stringify(obj);
var func = new Function('return (' + str + ');');
return func;
}
var tests = {
json: makeJsonCopy,
manual: makeManualCopy,
inherit: makeInheritCopy,
function: makeFunctionCopy
};
var obj = {
type: 'tag',
name: 'div',
attribs: {
'class': 'foo bar baz',
'id': 'main',
'style': 'position: relative'
}
};
var testName;
for (testName in tests) {
tests[testName] = tests[testName](obj);
}
var iterations = 10000000;
console.log('All tests over', iterations, 'iterations');
var testSeq = [
'function', 'inherit', 'manual', 'json'
];
for (var q = 0; q < testSeq.length; q++) {
testName = testSeq[q];
console.log('Beginning', testName);
var test = tests[testName];
var result;
console.time('warm up');
for (var i = 0; i < iterations; i++) {
result = test();
}
console.timeEnd('warm up');
console.time('copy');
for (var i = 0; i < iterations; i++) {
result = test();
}
console.timeEnd('copy');
var testObj = test();
console.time('write');
for (var i = 0; i < iterations; i++) {
testObj.name = 'p';
}
console.timeEnd('write');
console.time('read');
for (var i = 0; i < iterations; i++) {
var tagName = testObj.name;
var className = testObj.attribs['class'];
}
console.timeEnd('read');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment