Search code examples
javascriptnode.jsv8

Degraded performance of a overridden getter in node/v8


I ran into this strange scenario where overriding a property getter is severely impacting performance (which will only be noticeable with lot of computation like sorting in the example below).

In the following example code, I have 3 classes, VP, F1 and F2. F1 has a property called #vp with a corresponding getter that returns this private member. F2 extends F1 and overrides the vp getter which simply delegates to super.vp. I actually don't need to do this but because I have to override the setter I also need to implement the getter as per JavaScript spec.

In the following code, I sort a million random numbers and the difference in performance using F1 object vs F2 object is around 1 second vs 5 seconds.

import { performance } from 'perf_hooks';

class VP {
    #idx = 0;
    getValue(row) { return row[this.#idx]; }
}

class F1 {
    #vp;
    set vp(vp) { this.#vp = vp; }
    get vp() { return this.#vp; }
}

class F2 extends F1 {
    #o;
    set vp(vp) { super.vp = vp; this.#o = 1; }
    get vp() { return super.vp; }
}

const vp = new VP();
const f1 = new F1();
f1.vp = vp;
const f2 = new F2();
f2.vp = vp;

function getData(size) {
        const data = new Array();
        for(let i=0;i<size;i++) data[i] = [Math.random()*1000000];
        return data;
}

const data = getData(1000000);
const t1 = performance.now();
// data.sort((a,b) => f1.vp.getValue(a) - f1.vp.getValue(b)); // 940ms
// data.sort((a,b) => { let vp = f1.vp; return vp.getValue(a) - vp.getValue(b); }); // 945ms
data.sort((a,b) => f2.vp.getValue(a) - f2.vp.getValue(b)); // 4990ms
// data.sort((a,b) => { let vp = f2.vp; return vp.getValue(a) - vp.getValue(b); }); // 3146ms
const t2 = performance.now();
console.debug(t2-t1);

At the moment I got away by getting rid of the setter/getter in F2 and instead using a separate setVP API to set the value. But I am curious as to why overriding the getter is making it so slow.


Solution

  • (V8 developer here.)

    You didn't say which version of V8 you're using, so I have to guess a bit, which in this particular case is not too hard...

    The reason for the slowdown you're seeing is not the fact that you're overriding the getter; it's the fact that you're using super.vp in the overriding getter.

    With Node 15.11 (V8 8.6) I can reproduce your results; with current V8 (9.0/9.1) I cannot. One difference between those versions is the work described here: https://v8.dev/blog/fast-super, which very much fits this scenario.

    So if you simply update (to Chrome 90+, or current Node nightly builds which I guess will become Node 16) when updates are available, your code will magically speed up.


    If you need a workaround that works today, find a way to avoid using super on the hot path. For example by using a different name in the subclass, and getting to the inherited getter via regular property lookup, like so:

    class F2 extends F1 {
      #o;
      set vp2(vp) { this.vp = vp; this.#o = 1; }
      get vp2() { return this.vp; }
    }
    

    which I guess is similar to the "setVP API" you hinted at. Or you could stop using private fields and just access _vp (instead of #vp) directly from F2, skipping the super accessor:

    class F1 {
      _vp;
      set vp(vp) { this._vp = vp; }
      get vp() { return this._vp; }
    }
    
    class F2 extends F1 {
      #o;
      set vp(vp) { this._vp = vp; this.#o = 1; }
      get vp() { return this._vp; }
    }