Open MikeHolman opened 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.
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.
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.
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):