chakra-core / ChakraCore

ChakraCore is an open source Javascript engine with a C API.
MIT License
9.1k stars 1.19k forks source link

Optimize class constructor calls #3869

Open MikeHolman opened 6 years ago

MikeHolman commented 6 years ago

We have seen in some benchmarks that class constructor calls can be extremely slow compared to object literal assignment.

For example, replacing calls to new Point() with {x:0, y:0} improves perf by 50% in the benchmark below (brought up by #3828):

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

class Param {
  constructor(x1 = 0, x2 = 0, a = 0, b = 0, c = 0, d = 0) {
    this.x1 = x1;
    this.x2 = x2;
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
  };
}

class Spline {
  constructor(input) {
    const clen = input.length - 1;

    this.last = 0;
    this.params = [];

    let ds = new Float64Array(clen);
    let ms = new Float64Array(clen);

    for (let i = 0; i < clen; i++) {
      let c = input[i];
      let n = input[i + 1];

      const dx = n.x - c.x;
      const dy = n.y - c.y;

      ds[i] = dx;
      ms[i] = dy / dx;

      const p = new Param();

      if (i) {
        c = ms[i - 1];
        n = ms[i];

        if ((c * n) <= 0) {
          p.c = 0.0;
        } else {
          const pdx = ds[i - 1];
          const com = pdx + dx;

          p.c = 3.0 * com / ((com + dx) / c + (com + pdx) / n);
        }
      } else {
        p.c = ms[0];
      }

      this.params[i] = p;
    }

    this.params[clen] = new Param();
    this.params[clen].c = ms[clen - 1];

    for (let i = 0; i < clen; i++) {
      const p = this.params[i];
      const c = input[i];
      const n = input[i + 1];
      const m = ms[i];

      p.x1 = c.x;
      p.x2 = n.x;
      p.d  = c.y;

      const inv = 1.0 / ds[i];
      const com = p.c + this.params[i + 1].c - m - m;

      p.b = (m - p.c - com) * inv;
      p.a = com * inv * inv;
    }

    this.params[clen - 1].x2 = Number.MAX_VALUE;
  };

  evaluate(x) {
    let c = this.params[this.last];

    if (x < c.x1 || x > c.x2) {
      for (let i = 0, len = this.params.length; i < len; i++) {
        if (x <= this.params[i].x2) {
          this.last = i;
          c = this.params[i];
          break;
        }
      }
    }

    const dif = x - c.x1;
    const out = new Point();

    out.x = ((c.a * dif + c.b) * dif + c.c) * dif + c.d;
    out.y = (3.0 * c.a * dif + 2.0 * c.b) * dif + c.c;

    return out;
  };
}

class Dac {
  constructor(bits = 8) {
    this.dac = new Float32Array(bits);
  };

  kinked(model) {
    const len = this.dac.length;
    const inf = 1e6;

    let r2 = 2.20;
    let term = false;
    let vsum = 0;

    if (model == 2) {
      r2 = 2.00;
      term = true;
    }

    for (let i = 0; i < len; i++) {
      let vn = 1;
      let rn = (term) ? r2 : inf;
      let b, ti;

      for (b = 0; b < i; b++) {
        rn = (rn == inf) ? (1 + r2) : (1 + r2 * rn / (r2 + rn));
      }

      if (rn == inf) {
        rn = r2;
      } else {
        rn = r2 * rn / (r2 + rn);
        vn = vn * rn / r2;
      }

      for (++b; b < len; b++) {
        rn++;
        ti = vn / rn;
        rn = r2 * rn / (r2 + rn);
        vn = rn * ti;
      }

      vsum += vn;
      this.dac[i] = vn;
    }

    vsum /= (1 << len);

    for (let i = 0; i < len; i++) {
      this.dac[i] /= vsum;
    }
  };

  output(input) {
    let value = 0;

    for (let i = 0, len = this.dac.length; i < len; i++) {
      if (input & (1 << i)) {
        value += this.dac[i];
      }
    }

    return value;
  };
}

class OpAmp {
  constructor(opamp, kvddt) {
    this.opamp = new Spline(opamp);
    this.kvddt = kvddt;
    this.vmin  = opamp[0].x;
    this.vmax  = opamp[opamp.length - 1].x;

    this.x = 0;
  };

  reset() {
    this.x = this.vmin;
  };

  solve(n, vi) {
    let ak = this.vmin;
    let bk = this.vmax;

    const a = n + 1;
    const b = this.kvddt;

    let bvi = b - vi;
    if (bvi < 0) { bvi = 0; }

    const c = n * (bvi * bvi);

    do {
      const xk = this.x;

      let out = this.opamp.evaluate(xk);
      let bvx = b - xk;
      let bvo = b - out.x;

      if (bvx < 0) { bvx = 0; }
      if (bvo < 0) { bvo = 0; }

      const f = a * (bvx * bvx) - c - (bvo * bvo);

      this.x -= (f / (2 * (bvo * out.y - a * bvx)));

      if (Math.abs(this.x - xk) < 1e-8) {
        out = this.opamp.evaluate(this.x);
        return out.x;
      }

      if (f < 0) {
        bk = xk;
      } else {
        ak = xk;
      }

      if (this.x <= ak || this.x >= bk) {
        this.x = (ak + bk) * 0.5;
      }
    } while (true);
  };
}

class Filter {
  constructor() {
    this.gain      = null;
    this.mixer     = null;
    this.resonance = null;
    this.summer    = null;

    this.enabled   = true;
    this.filter    = 0;
    this.volume    = 0;
    this.vhp       = 0;
    this.vbp       = 0;
    this.vlp       = 0;
    this.ve        = 0;
    this.fc        = 0;
    this.hp        = false;
    this.bp        = false;
    this.lp        = false;
    this.filt1     = false;
    this.filt2     = false;
    this.filt3     = false;
    this.filtE     = false;
    this.voice3off = false;

    this.setEnable = (value) => {
      this.enabled = value;

      if (value) {
        this.resFilt = this.filter;
      } else {
        this.filt1 = false;
        this.filt2 = false;
        this.filt3 = false;
        this.filtE = false;
      }
    }
  };

  set fcLo(value) {
    this.fc = (this.fc & 0x7f8) | (value & 7);
    this.updateCenterFreq();
  };

  set fcHi(value) {
    this.fc = ((value << 3) & 0x7f8) | (this.fc & 7);
    this.updateCenterFreq();
  };

  set modeVol(value) {
    this.volume = value & 0x0f;

    this.lp = (value & 0x10) != 0;
    this.bp = (value & 0x20) != 0;
    this.hp = (value & 0x40) != 0;

    this.voice3off = (value & 0x80) != 0;

    this.updateMixing();
  };

  set resFilt(value) {
    this.filter = value;
    this.updateResonance((value >> 4) & 0x0f);

    if (this.enabled) {
      this.filt1 = (value & 1) != 0;
      this.filt2 = (value & 2) != 0;
      this.filt3 = (value & 4) != 0;
      this.filtE = (value & 8) != 0;
    }

    this.updateMixing();
  };

  reset() {
    this.fc = 0;
    this.updateCenterFreq();

    this.modeVol = 0;
    this.resFilt = 0;
  };
}

class Model6581 {
  constructor() {
    const voltage = [
      new Point( 0.81, 10.31),
      new Point( 2.40, 10.31),
      new Point( 2.60, 10.30),
      new Point( 2.70, 10.29),
      new Point( 2.80, 10.26),
      new Point( 2.90, 10.17),
      new Point( 3.00, 10.04),
      new Point( 3.10,  9.83),
      new Point( 3.20,  9.58),
      new Point( 3.30,  9.32),
      new Point( 3.50,  8.69),
      new Point( 3.70,  8.00),
      new Point( 4.00,  6.89),
      new Point( 4.40,  5.21),
      new Point( 4.54,  4.54),
      new Point( 4.60,  4.19),
      new Point( 4.80,  3.00),
      new Point( 4.90,  2.30),
      new Point( 4.95,  2.03),
      new Point( 5.00,  1.88),
      new Point( 5.05,  1.77),
      new Point( 5.10,  1.69),
      new Point( 5.20,  1.58),
      new Point( 5.40,  1.44),
      new Point( 5.60,  1.33),
      new Point( 5.80,  1.26),
      new Point( 6.00,  1.21),
      new Point( 6.40,  1.12),
      new Point( 7.00,  1.02),
      new Point( 7.50,  0.97),
      new Point( 8.50,  0.89),
      new Point(10.00,  0.81),
      new Point(10.31,  0.81)
    ];

    const ut    = 26.0e-3;
    const vdd   = 12.18;
    const vth   = 1.31;
    const wlvcr = 9;

    this.voltrange = 1.5;
    this.dcvoltage = 5;
    this.c         = 470e-12;
    this.k         = 1;
    this.kvddt     = this.k * (vdd - vth);
    this.ucox      = 20e-6;
    this.wlsnake   = 1 / 115;

    this.vmin = voltage[0].x;
    this.vmax = voltage[0].y;

    if (this.kvddt > this.vmax) {
      this.vmax = this.kvddt;
    }

    this.denorm  = this.vmax - this.vmin;
    this.norm    = 1 / this.denorm;
    this.n16     = this.norm * 65535;
    this.voiceDc = (this.n16 * (this.dcvoltage - this.vmin)) >> 0;
    this.scale14 = ((this.norm * 16383) * this.voltrange) >> 0;

    this.gain = [];
    this.mixer = [];
    this.summer = [];

    this.dacbits  = 11;
    this.daczero  = 6.65;
    this.dacscale = 2.63;

    this.dac = new Dac(this.dacbits);
    this.dac.kinked(1);

    this.opampRev = new Uint16Array(65536);
    this.vcrkvg   = new Uint16Array(65536);
    this.vcrnTerm = new Uint16Array(65536);

    const scaled = [];

    for (let i = 0, len = voltage.length; i < len; i++) {
      let p = new Point();
      let o = voltage[i];

      p.x = (this.n16 * (o.x - o.y) + 65536) / 2;
      p.y =  this.n16 * (o.x - this.vmin);

      scaled[i] = p;
    }

    const s = new Spline(scaled);

    for (let i = 0; i < 65536; i++) {
      const o = s.evaluate(i);
      if (o.x < 0) { o.x = 0; }
      this.opampRev[i] = o.x + 0.5;
    }

    const model = new OpAmp(voltage, this.kvddt);

    for (let i = 0; i < 5; i++) {
      const idiv = 2 + i;
      const size = idiv << 16;
      const narr = new Uint16Array(size);

      model.reset();

      for (let vi = 0; vi < size; vi++) {
        const vin = this.vmin + vi / this.n16 / idiv;
        narr[vi] = ((model.solve(idiv, vin) - this.vmin) * this.n16) + 0.5;
      }

      this.summer[i] = narr;
    }

    for (let i = 0; i < 8; i++) {
      let idiv = 1;
      let size = 1;

      if (i) {
        idiv = i;
        size = i << 16;
      }

      const narr = new Uint16Array(size);
      const n = i * 8 / 6;

      model.reset();

      for (let vi = 0; vi < size; vi++) {
        const vin = this.vmin + vi / this.n16 / idiv;
        narr[vi] = ((model.solve(n, vin) - this.vmin) * this.n16) + 0.5;
      }

      this.mixer[i] = narr;
    }

    for (let i = 0; i < 16; i++) {
      const narr = new Uint16Array(65536);
      const n = i / 8;

      model.reset();

      for (let vi = 0; vi < 65536; vi++) {
        const vin = this.vmin + vi / this.n16;
        narr[vi] = ((model.solve(n, vin) - this.vmin) * this.n16) + 0.5;
      }

      this.gain[i] = narr;
    }

    const nkvddt = this.n16 * this.kvddt;
    const nvmin  = this.n16 * this.vmin;

    const kvt = this.k * vth;
    const is  = 2 * this.ucox * ut * ut / this.k * wlvcr;
    const n15 = this.norm * 32767;
    const nis = n15 * 1.0e-6 / this.c * is;
    const ut2 = 2 * ut;

    for (let i = 0; i < 65536; i++) {
      const log = Math.log1p(Math.exp((i / this.n16 - kvt) / ut2));
      this.vcrnTerm[i] = (nis * log * log) + 0.5;

      this.vcrkvg[i] = (this.k * (nkvddt - Math.sqrt(i << 16)) - nvmin) + 0.5;
    }
  };
}

console.time("worker");
const sid6581 = new Model6581();
console.timeEnd("worker");
rhuanjl commented 6 years ago

Upfront disclaimer I have minimal knowledge of ChakraCore's internals so I may say something completely wrong.

I'm not sure if anyone's looking at this but I tried doing a little profiling just to understand what was going on here, I compared the the profile trace generated by Instruments (macOS profiling tool) when running the following two test scripts in ch using a test-build without lto of the latest chakra-core master.

Test 1: ES6

function testing()
{
    let output = new Array(100000);
    let len = output.length;
    for(let i = 0; i < len; ++i)
    {
        output[i] = new foo(Math.random(), Math.random());
    }
    return output;
}

class foo
{
    constructor(x, y)
    {
        this.x = x;
        this.y = y;
    }
    fun()
    {
        return this.x + this.y;
    }
}

let start = Date.now();
for(let i = 0; i < 1000; ++i)
{
    testing();
}
print(Date.now()-start);

Test 2 ES5

function testing()
{
    let output = new Array(100000);
    let len = output.length;
    for(let i = 0; i < len; ++i)
    {
        output[i] = new foo(Math.random(), Math.random());
    }
    return output;
}

function foo(x, y)
{
    this.x = x;
    this.y = y;
}

foo.prototype.fun = function()
{
    return this.x + this.y;
}

let start = Date.now();
for(let i = 0; i < 1000; ++i)
{
    testing();
}
print(Date.now()-start);

Firstly the raw time taken, the ES6 one took 5549 ms, the ES5 one 3410.

For the ES6 trace 2.04 seconds are spent in JsOperators::NewScObjectCommon comprised of: 1.55 seconds in JsDynamicObject::NewObject 332 ms in DynamicObject::InitSlots and 207ms in Memory::HeapBucketT Then a variety of smaller calls

For the ES5 trace there is as far as I can see no appearance of NewScObjectCommon - which is surprising to me these two scripts should be doing the same thing and yet the heaviest function on one is not on the other. The heaviest call in the ES5 test is 1.46s in JsOperators::JitRecyclerAlloc of which 981ms is Recycler::RealAlloc and the rest is a variety of smaller items.

As far as I can deduce from reading the traces and looking at the code ES6 class constructers (in this test case at least) are implemented via a completely different and significantly slower mechanism within Chakra to ES5 constructors. Though looking at the comments in Recycler.inl the mechanism being used for the ES5 class here may only work for "small" objects, so perhaps the ES6 call is using a general mechanism that works for "large" objects.

I don't know where to begin to actually fix/improve this but I hope the above is useful to someone - if no one does pick this up I may have a go at it in a month or two.

MikeHolman commented 6 years ago

Yes I believe there is a totally different code path for ES5 constructors. I don't know this code too well, but the JIT does its object creation work (at least for ES5 objects) in LowerNewScObject, and you can see it does work to avoid some of these slower calls. Probably the place to start is finding the difference between that and the ES6 case.

rhuanjl commented 6 years ago

Thought I'd take another look at this now I know a bit more of how CC works. Still don't know enough to improve it (yet).

But update for future reference: ES6 class constructors and ES5 constructors (in the above examples at least) BOTH go through LowerNewScObject.

BUT when ES6 class constructors get there: HasBailOutInfo() AND IsProfiledInstr() are both false -> which results in LowerNewScObject emitting calls to helper functions.

Whereas when an ES5 constructor gets there these are both true which results in the emission of a fast path.