Search code examples
javascriptv8

Escape analysis for class instances


Does v8 do escape analysis for class instances, or is there some fundamental roadblock, which makes this excessively harder than for objects? In an example:

class P {
  x;
  y;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  add(other) {
    return new P(this.x + other.x, this.y + other.y);
  }
  multiply(s) {
    return new P(this.x * s, this.y * s);
  }
}

const f = (a, b) => a.add(b).multiply(5).x;
// These allocations -^^^^^^-^^^^^^^^^^^

Are temporary instances allocated here, and is this avoidable? It is a simplified example for a naive vector implementation.


Following are checks i did, which make me suspect a difference in the first place. I am moderately new to the topic, and also not too proficient in asm. It is likely I made some silly mistake. Comparing the following, I see that the resulting code:

  • with plain objects, is considerably shorter, and has only one multiplication (expected)

  • exchanging with class instances, is around four times as long (just for the function f), and has two multiplications

    vmulsd xmm1,xmm1,xmm2
    -- no jumps here --
    vmulsd xmm0,xmm0,xmm2
    

    and to my naive eyes looks like everything is being allocated.

(put in snippets to collapse)

const add = (p1, p2) => ({ x: p1.x + p2.x, y: p1.y + p2.y });
const multiply = (p1, s) => ({ x: p1.x * s, y: p1.y * s });

const f = (a, b) => multiply(add(a, b), 5).x;

const rnd = () => Math.random() * 1000000;
const rndP = () => ({ x: rnd(), y: rnd() });

%PrepareFunctionForOptimization(f);
for (let i = 0; i < 10000; i++) f(rndP(), rndP());
%OptimizeFunctionOnNextCall(f);
f(rndP(), rndP());

class P {
  x;
  y;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

const add = (p1, p2) => new P(p1.x + p2.x, p1.y + p2.y);
const multiply = (p1, s) => new P(p1.x * s, p1.y * s);

const f = (a, b) => multiply(add(a, b), 5).x;

const rnd = () => Math.random() * 1000000;
const rndP = () => new P(rnd(), rnd());

%PrepareFunctionForOptimization(f);
for (let i = 0; i < 10000; i++) f(rndP(), rndP());
%OptimizeFunctionOnNextCall(f);
f(rndP(), rndP());

node --allow-natives-syntax --print-opt-code --code-comments class.js > classAsm.txt (not on the newest node, but unless something changed in a recent v8 update, it shouldn't matter)

This in itself doesn't prove anything, and i am not proficient enough in the topic to understand the full code, especially with all the ICs etc I already have trouble identifying. I am aware that for most applications, the allocations are likely not going to pose a bottleneck, especially as they will be gen0 for GC. I am simply curious, to push my understanding of the topic, in case it does eventually matter.


Solution

  • Does v8 do escape analysis for class instances

    Yes.

    Are temporary instances allocated here

    No.

    with class instances, [the code] has two multiplications

    I see only one (for x; the operations on y are all dead-code-eliminated).

    (This is with recent V8; I haven't checked older versions.)