-
-
Save Salakar/6d7b84f7adf1f3bc62a754752a6e5d0e to your computer and use it in GitHub Desktop.
/** | |
Platform info: | |
Darwin 15.6.0 x64 | |
Node.JS 6.6.0 | |
V8 5.1.281.83 | |
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz × 8 | |
var vs let | |
922,009,444 op/s » with var | |
19,823,034 op/s » with let | |
Suites: 1 | |
Benches: 2 | |
Elapsed: 5,900.32 ms | |
*/ | |
// benchmark below uses 'matcha' - feel free to adapt to which ever benchmark system you use. | |
console.log('\r\n'); | |
function withLet(str) { | |
let test = 'abcd' + 'efgh'; | |
test += str; | |
return test; | |
} | |
function withVar(str) { | |
var test = 'abcd' + 'efgh'; | |
test += str; | |
return test; | |
} | |
// testing | |
withVar('PING'); | |
withVar('PONG'); | |
withLet('PING'); | |
withLet('PONG'); | |
// suite | |
suite('var vs let', function () { | |
set('mintime', 2000); | |
set('concurrency', 500); | |
bench('with var', function () { | |
return withVar('PING'); | |
}); | |
bench('with let', function () { | |
return withLet('PING'); | |
}); | |
}); |
The += to test + is called a compound let assignment, that's a known issue. Again this is all microptimization. In the original example you're seeing a bad benchmark framework prioritizing the first statement executed. In the second example, you're seeing a known optimization in v8. You can read about others here,.
We're still microoptimizing.
@jakepusateri @EvanCarroll @tswaters I know a lot of you think that this kind of 'micro optimisation' is unnecessary and you're partly right, however; considering this de-opts the entire function that uses the 'let compound assignment' it therefore affects all the contained code within that function - which, in my opinion is where it no longer becomes 'micro'.
There is an issue with the matcha benchmarking lib, but apart from that the difference is still substantial. Consider the following real world example. (work in progress)
This is a redis parser I'm working on, namely the writable part of converting a cmd and it's args into the resp protocol.
'use strict';
const cmdCache = {};
const cmdCachePartial = {};
const newLine = '\r\n';
const zeroArg = `$1\r\n0\r\n`;
const oneArg = `$1\r\n1\r\n`;
const nullArg = `$4\r\nnull\r\n`;
const symbolArg = `$8\r\n[Symbol]\r\n`;
const undefArg = `$9\r\nundefined\r\n`;
const functionArg = `$10\r\n[Function]\r\n`;
const objectArg = `$15\r\n[object Object]\r\n`;
/**
* Faster for short strings less than 1kb to manually loop over
* Larger strings use Buffer.byteLength
* @param str
* @returns {*}
*/
function byteLength(str) {
var s = str.length;
if (s > 1023) return Buffer.byteLength(str, 'utf8');
var i = s - 1;
var code;
while (i--) {
code = str.charCodeAt(i);
if (code > 0x7f && code <= 0x7ff) s++;
else if (code > 0x7ff && code <= 0xffff) s += 2;
if (code >= 0xDC00 && code <= 0xDFFF) i--; // trail surrogate
}
return s;
}
/**
* Commands with no args - this increases ops/s by ~150k
* @param cmd
* @returns {*}
*/
function cmdWritable(cmd) {
return cmdCache[cmd] || (cmdCache[cmd] = `*1\r\n$${ cmd.length }\r\n${ cmd }\r\n`);
}
/**
* Caches a cmd partial (without the *argLength)
* @param cmd
* @returns {*}
*/
function cmdPartial(cmd) {
return cmdCachePartial[cmd] || (cmdCachePartial[cmd] = '\r\n$' + cmd.length + newLine + cmd + newLine);
}
/**
*
* @param arg
* @returns {string}
*/
function argWritable(arg) {
switch (typeof arg) {
case 'undefined':
return undefArg;
case 'object':
if (arg == null) return nullArg;
else return objectArg;
case 'function':
return functionArg;
case 'symbol':
return symbolArg;
case 'number':
if (arg == 0) return zeroArg;
else if (arg == 1) return oneArg;
return '$' + byteLength('' + arg) + newLine + arg + newLine;
case 'string':
case 'boolean':
default:
return '$' + byteLength('' + arg) + newLine + arg + newLine;
}
}
/**
* Convert a CMD and args to a redis writable
* @param cmd
* @param args
* @returns {string}
*/
function toWritable(cmd, args) {
if (!args || !args.length) return cmdWritable(cmd);
switch (args.length) {
case 1:
return '*2' + cmdPartial(cmd) + argWritable(args[0]);
case 2:
return '*3' + cmdPartial(cmd) + argWritable(args[0]) + argWritable(args[1]);
default:
const l = args.length;
var writable = `*${ l + 1 }${cmdPartial(cmd)}`;
for (var i = 0; i < l; i++) {
writable += argWritable(args[i]);
}
return writable;
}
}
// ------------------------------------------------------
// funcs that use let instead for benchmark example
// ------------------------------------------------------
function byteLengthLet(str) {
let s = str.length;
if (s > 1023) return Buffer.byteLength(str, 'utf8');
let i = s - 1;
let code;
while (i--) {
code = str.charCodeAt(i);
if (code > 0x7f && code <= 0x7ff) s++;
else if (code > 0x7ff && code <= 0xffff) s += 2;
if (code >= 0xDC00 && code <= 0xDFFF) i--; // trail surrogate
}
return s;
}
function argWritableLet(arg) {
switch (typeof arg) {
case 'undefined':
return undefArg;
case 'object':
if (arg == null) return nullArg;
else return objectArg;
case 'function':
return functionArg;
case 'symbol':
return symbolArg;
case 'number':
if (arg == 0) return zeroArg;
else if (arg == 1) return oneArg;
return '$' + byteLengthLet('' + arg) + newLine + arg + newLine;
case 'string':
case 'boolean':
default:
return '$' + byteLengthLet('' + arg) + newLine + arg + newLine;
}
}
function toWritableLet(cmd, args) {
if (!args || !args.length) return cmdWritable(cmd);
switch (args.length) {
case 1:
return '*2' + cmdPartial(cmd) + argWritableLet(args[0]);
case 2:
return '*3' + cmdPartial(cmd) + argWritableLet(args[0]) + argWritableLet(args[1]);
default:
const l = args.length;
let writable = `*${ l + 1 }${cmdPartial(cmd)}`;
for (let i = 0; i < l; i++) {
writable += argWritableLet(args[i]);
}
return writable;
}
}
// ------------------
// Benchmarks
// ------------------
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
suite
.add('let', () => {toWritableLet('PING', ['foo'])})
.add('var', () => {toWritable('PONG', ['foo'])})
.on('start', function() {
console.log('\r\n');
console.log("Starting at " + (new Date));
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Complete at ' + (new Date));
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({
async: true
});
Results
Benchmarked using 'benchmark' rather than match.
Starting at Wed Sep 28 2016 12:40:54 GMT+0100 (BST)
let x 4,358,340 ops/sec ±0.64% (91 runs sampled)
var x 11,413,777 ops/sec ±0.77% (87 runs sampled)
Complete at Wed Sep 28 2016 12:41:06 GMT+0100 (BST)
Fastest is var
4x vs 50x is a different result. 4x says to me that it is soundly in the realm of things that don't actually affect performance. Profile first. This should be viewed as a minor v8 bug report, not a scary warning to stop using let.