Understanding V8

Vyacheslav Egorov

[email protected], @mraleph

navigate with cursor keys ←→ or space bar
nodecampeu 2011

Care about performance?

Before you optimize

Example: protocol parser

 

data length\u0000data bytes
11\u0000nodecamp.eu

 

Parser.prototype.parse = function (s) {
  var l = '';
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
      l = Number(l);
      this.emit('data', s.substr(i + 1, l));
      return this.parse(s.substr(i + 1 + l));
    } else {
      l += s[i];
    }
  }
  return s;
};

Prepare fake input data

function makeFakePacket() { 
  var l = randomBetween(MIN_PACKET_LENGTH,
                        MAX_PACKET_LENGTH);
  var p = l + '\u0000';
  for (var i = 0; i < l; ++i) p += ' ';
  return p;
}

var fakeInput = '';
for (var i = 0; i < NOF_PACKETS; i++) {
  fakeInput += makeFakePacket();
}

Prepare fake input data

Stress it

var p = new Parser();

var start = Date.now();
for (var j = 0; j < NOF_RUNS; j++) {
  p.parse(fakeInput);
}
var end = Date.now();

var timeSpent = end - start;
var totalBytes = fakeInput.length * NOF_RUNS;

console.log(timeSpent + ' ms');
console.log(totalBytes/timeSpent + ' bytes/ms');

Stress it

var MIN_PACKET_LENGTH = 10;
var MAX_PACKET_LENGTH = 100;
var NOF_PACKETS = 1000;
var NOF_RUNS = 10;

Stress it

% node parser.js
806 ms
711.4267990074442 bytes/ms

Profile it

% node --prof parser.js
806 ms
711.4267990074442 bytes/ms

Builtin sampling profiler

Profile it

% node --prof parser.js
806 ms
711.4267990074442 bytes/ms
% deps/v8/tools/mac-tick-processor

 [GC]:
   ticks  total  nonlib   name
    576   79.0%

 

GC cost is proportional to the number of long lived objects

 

Parser.prototype.parse = function (s) {
  var l = '';
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
      l = Number(l);  
      this.emit('data', s.substr(i + 1, l));
      return this.parse(s.substr(i + 1 + l));
    } else {
      l += s[i];
    }
  }
  return s;
};

 

Parser.prototype.parse = function (s) {
  var l = '';
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
      l = Number(l);
      this.emit('data', s.substr(i + 1, l));
s = s.substr(i + 1 + l); i = 0; l = '';
} else { l += s[i]; } } return s; };

 

% node --prof parser.js
183 ms
3133.3879781420765 bytes/ms
% deps/v8/tools/mac-tick-processor

 [JavaScript]:
   ticks  total  nonlib   name
    124   64.2%   87.3%  Stub: SubString

 [GC]:
   ticks  total  nonlib   name
      8    4.1%

String representations

 

Don't mix indexing and concatenation

while (smth) {
  s = s + s.substr(x, y);
}

Use arrays instead.

 

Substring cost is proportional to it's length

 

Parser.prototype.parse = function (s) {
  var l = '';
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
      l = Number(l);
      this.emit('data', s.substr(i + 1, l));
s = s.substr(i + 1 + l); i = 0;
l = ''; } else { l += s[i]; } } return s; };

 

Parser.prototype.parse = function (s) {
  var l = '', j = 0;
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
      l = Number(l);
      this.emit('data', s.substr(i + 1, l));
i += l; j = i + 1;
l = ''; } else { l += s[i]; } }
return s.substr(j);
};

 

% node --prof parser.js
6 ms
95568.33333333333 bytes/ms
% deps/v8/tools/mac-tick-processor
[JavaScript]:
4    9.8%    9.8%  Stub: StringAdd

NOF_RUNS = 10

% node --prof parser.js
6 ms
95568.33333333333 bytes/ms
% deps/v8/tools/mac-tick-processor
[JavaScript]:
4    9.8%    9.8%  Stub: StringAdd

NOF_RUNS = 1000

% node --prof parser.js
194 ms
295572.1649484536 bytes/ms
% deps/v8/tools/mac-tick-processor
[JavaScript]:
47 20.4% 20.4% LazyCompile: *Parser.parse
33 14.3% 14.3% Stub: SubString 21 9.1% 9.1% LazyCompile: *substr native 14 6.1% 6.1% LazyCompile: ~ToNumber native 14 6.1% 6.1% LazyCompile: b native 12 5.2% 5.2% Stub: StringAdd

Code starts unoptimized

Parser.parse

String.substr

makeFakePacket

randomBetween

V8 profiles for hotspots

Parser.parse

String.substr

makeFakePacket

randomBetween

Optimizes hot functions

*Parser.parse

*String.substr

makeFakePacket

randomBetween

 

--trace-opt


log names of optimized functions to stdout

 

Not all constructs are supported by optimizing compiler

 

--trace-bailout


log optimizing compiler bailouts

 

Optimizations are:

 

--trace-deopt


log deoptimizations

 

% node --prof parser.js
194 ms
295572.1649484536 bytes/ms
% deps/v8/tools/mac-tick-processor
[JavaScript]:
47  20.4%   20.4%  LazyCompile: *Parser.parse
33  14.3%   14.3%  Stub: SubString
21   9.1%    9.1%  LazyCompile: *substr native
14   6.1%    6.1%  LazyCompile: ~ToNumber native
14   6.1%    6.1%  LazyCompile: b native 
12 5.2% 5.2% Stub: StringAdd

 

Parser.prototype.parse = function (s) {
  var l = '', j = 0;
  for (var i = 0; i < s.length; i++) {
    if (s[i] == '\u0000') {
l = Number(l);
this.emit('data', s.substr(i + 1, l)); i += l; j = i + 1; l = ''; } else {
l += s[i];
} } return s.substr(j); };

 

Parser.prototype.parse = function (s) {
var l = 0, j = 0, ch0 = "0".charCodeAt(0);
for (var i = 0; i < s.length; i++) {
var ch = s.charCodeAt(i); if (ch === 0) {
this.emit('data', s.substr(i + 1, l)); i += l; j = i + 1;
l = 0;
} else {
l = l * 10 + (ch - ch0);
} } return s.substr(j); };

 

% node parser.js
125 ms
458728 bytes/ms

vs.

% node parser.js
806 ms
711.4267990074442 bytes/ms

Understanding Numbers

Number representations

 

Use WebGL typed arrays

Float32Array

Float64Array

Understanding Objects

function Point(x, y) {
this.x = x; this.y = y; }  
var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
 
function Point(x, y) {
this.x = x;
this.y = y; } var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
  this.x = x;
this.y = y;
} var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
  this.x = x;
  this.y = y;
}
var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
this.x = x; this.y = y; } var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
 
function Point(x, y) {
this.x = x;
this.y = y; } var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
  this.x = x;
this.y = y;
} var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
  this.x = x;
  this.y = y;
}
var p1 = new Point(11, 22); var p2 = new Point(33, 44);
function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
p2.z = 55; 
// p1 and p2 now have // different hidden classes
v = obj.x
v = Runtime_GetProperty(obj, 'x');
function Runtime_GetProperty(obj, f) {
  var clazz = HiddenClass(obj);
  return obj[clazz.IndexOfField(f)];
}
v = obj.x
if (HiddenClass(obj) == cache.clazz) {
  v = obj[cache.index];
} else {
  v = Runtime_LoadCache_Miss(cache, obj, 'x');
}
function Runtime_LoadCache_Miss(cache, obj, f) {
  var clazz = HiddenClass(obj);
  var index = clazz.IndexOfField(f);
  cache.clazz = clazz;
  cache.index = index;
  return obj[index];
}

 

Monomorphic sites are better than polymorphic

Dictionary mode

For hot objects:

arguments object

Variables&Scopes

Two types of variables

function f() {
  var v1;  // real local variable
  var v2;  // context allocated
  var v3;  // context allocated

  function g() { use(v2); }
  function h() { use(v3); }
}

 

Contexts are created eagerly

function foo(arg1, arg2, arg3) {
  /* V8 allocates context, copies arg2 to it */

  if (almostAlwaysFalse()) {
    return function () { return arg2; }
  }

  /* Context is not used */
}

In hot code

Avoid global eval

Use Function constructor

In hot code

Avoid with

and try {} catch (e) {} for now

In hot code

Avoid const

 

Thank you!

Questions?