[Javascript Performance] Optimisation and deoptimization
The optimizing compiler optimizes for what it’s seen. If it sees something new, that’s problematic.
Seleting properties has some strange implications on performacne.
We have seen that runing this code, interpertor can optimze js runtime:
const { performance } = require('perf_hooks');
let iterations = 1e7;
const a = 1;
const b = 2;
const add = (x, y) => x + y;
performance.mark('start');
while (iterations--) {
add(a, b);
}
performance.mark('end');
performance.measure('My Special Benchmark', 'start', 'end');
const [ measure ] = performance.getEntriesByName('My Special Benchmark');
console.log(measure);
So what if we do:
performance.mark("start");
while (iterations--) {
add(a, b);
}
add("foo", "bar"); // added this line
performance.mark("end");
So when we call add('foo', 'bar')
we see deoptimizing happens.
If we do:
performance.mark("start");
while (iterations--) {
add(a, b);
}
iterations = 1e7;
while (iterations--) {
add(a, b);
}
performance.mark("end");
We can see the result:
entryType: 'measure',
startTime: 41.05515700019896,
duration: 15.343078000470996,
If we add:
performance.mark("start");
while (iterations--) {
add(a, b);
}
add('foo', 'bar') // add this back
iterations = 1e7;
while (iterations--) {
add(a, b);
}
performance.mark("end");
Result:
entryType: 'measure',
startTime: 40.564633999951184,
duration: 32.49434900004417,
It doubles the runtime from 18ms to 32ms.
If we add:
performance.mark("start");
%NeverOptimizeFunction(add); // add this line
while (iterations--) {
add(a, b);
}
add("foo", "bar");
iterations = 1e7;
while (iterations--) {
add(a, b);
}
performance.mark("end");
Run: node --allow-natives-syntax benchmark.js
We can see the result:
entryType: 'measure',
startTime: 44.788716999813914,
duration: 143.67414100002497,
If we don't allow JS engine to optimize our code, then it would be pretty slow
function add(x, y) {
return x + y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(3, '4');
Run: node --trace-opt --trace-deopt --allow-natives-syntax add.js
[manually marking 0x28a977194161 <JSFunction add (sfi = 0x28a97f871b41)> for non-concurrent optimization]
[compiling method 0x28a977194161 <JSFunction add (sfi = 0x28a97f871b41)> (target TURBOFAN) using TurboFan]
[optimizing 0x28a977194161 <JSFunction add (sfi = 0x28a97f871b41)> (target TURBOFAN) - took 0.278, 0.635, 0.037 ms]
[bailout (kind: deopt-soft, reason: Insufficient type feedback for binary operation): begin. deoptimizing 0x28a977194161 <JSFunction add (sfi = 0x28a97f871b41)>, opt id 0, bytecode offset 2, deopt exit 0, FP to SP delta 32, caller SP 0x7ff7b125e810, pc 0x000113c86050]
How does this works?
- We use an interpreter because the optimizing compiler is slow to get started
- Also: it needs some information before it knows what work it can either optimize or skip out on all together
- So, the interpreter starts gathering feedback about what it sees as the function is used.
But what if a string slips in there?
The optimizing compiler optimizes for what it's seen. If it sees something new, that's problematic.
Let's mesure this code:
const { performance } = require("perf_hooks");
let iterations = 1000000;
class Point {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
}
performance.mark("start");
while (iterations--) {
var point = new Point(1, 2, 3);
//point.x = undefined;
//delete point.x;
//point.y = undefined
//delete point.y
//point.z = undefined
//delete point.z
}
performance.mark("end");
performance.measure("My Special Benchmark", "start", "end");
const [measure] = performance.getEntriesByName("My Special Benchmark");
console.log(measure);
Run it to get baseline:
without | point.x = undefined | delete point.x | point.y = undefined | delete point.y | point.z = undefined | delete point.z |
290.2347559928894 | 269.04967099428177 | 709.9430350065231 | 266.5608630031347 | 704.3972899913788 | 267.9879929870367 | 305.1125900000334 |
- Deleting x & y, it is slow
- Deleting z, it is faster