Skip to content

Instantly share code, notes, and snippets.

@Salakar
Last active October 17, 2016 18:43
Show Gist options
  • Save Salakar/6d7b84f7adf1f3bc62a754752a6e5d0e to your computer and use it in GitHub Desktop.
Save Salakar/6d7b84f7adf1f3bc62a754752a6e5d0e to your computer and use it in GitHub Desktop.
var vs let performance comparison in v8 - 'let' with --trace-deopt logs 'Unsupported let compound assignment'
/**
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');
});
});
@Salakar
Copy link
Author

Salakar commented Sep 27, 2016

See my comment down below.

@laggingreflex
Copy link

laggingreflex commented Sep 27, 2016

Try changing test += str to test = test + str, in my (slightly modified) testing changing just that brought them back to same level.
Edit: also using numbers instead of string made them perform same too.

@tswaters
Copy link

I ran it against a "decent" benchmarking tool and the results were similar with var blowing the water out of let in the case where the += operator is used.

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite
suite
  .add('letperf1', () => {withLet('PING')})
  .add('varperf1', () => {withVar('PONG')})
  .on('start', function(event) {
    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
  });
Starting at Tue Sep 27 2016 17:48:41 GMT-0700 (Pacific Daylight Time)
letperf1 x 15,704,048 ops/sec ±1.26% (88 runs sampled)
varperf1 x 82,499,281 ops/sec ±1.24% (88 runs sampled)
Complete at Tue Sep 27 2016 17:48:53 GMT-0700 (Pacific Daylight Time)
Fastest is varperf1

Switching it from test += str to test = test + str brings them both up to ~82M for me.

So @EvanCarroll I don't think this can be chalked up to just the benchmarking tool. There's no reason += in this case should be an order of magnitude slower, micro-optimization or not.

Throwing in Math.random() * Date.now() to each function made the disparity less, but with that let is still twice as slow (switching test += to test = test + made them both about the same speed.

@jakepusateri
Copy link

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.

@EvanCarroll
Copy link

EvanCarroll commented Sep 28, 2016

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.

@Salakar
Copy link
Author

Salakar commented Sep 28, 2016

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment