Commit 4b9eb7f7 authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[runtime] Better instance pre-sizing with transpiled classes.

For instances created via constructors and `new` we try to pre-size
the instances such that ideally all the data properties can be
allocated as in-object properties (and we don't need to allocate the
out-of-object PropertyArray backing store). This is accomplished with
the helper of the Parser, which counts the property assignments to
`this` in the constructor, and we use that as the starting point for
pre-sizing logic (a mechanism called *slack tracking* is used to
dynamically shrink the objects based on the real memory usage, and
eventually compute the final starting size for instances of the
individual constructors).

This works well even with class hierarchies, since for a derived class
constructor we just include the current constructor plus all the base
constructors. I.e. with

```js
class A {
  constructor() {
    this.x00 = null;
    this.x01 = null;
    this.x02 = null;
    this.x03 = null;
    this.x04 = null;
    this.x05 = null;
    this.x06 = null;
    this.x07 = null;
    this.x08 = null;
    this.x09 = null;
    this.x10 = null;
    this.x11 = null;
    this.x12 = null;
    this.x13 = null;
    this.x14 = null;
    this.x15 = null;
    this.x16 = null;
    this.x17 = null;
    this.x18 = null;
    this.x19 = null;
  }
}

class B extends A {
  constructor() {
    super();
  }
}
```

we will eventually learn that instances of `B` need 20 in-object
properties. However this breaks with transpiled code (i.e. as
generated via TypeScript or Babel), even when the constructors are
properly chained.

```js
function A() {
  this.x00 = null;
  this.x01 = null;
  this.x02 = null;
  this.x03 = null;
  this.x04 = null;
  this.x05 = null;
  this.x06 = null;
  this.x07 = null;
  this.x08 = null;
  this.x09 = null;
  this.x10 = null;
  this.x11 = null;
  this.x12 = null;
  this.x13 = null;
  this.x14 = null;
  this.x15 = null;
  this.x16 = null;
  this.x17 = null;
  this.x18 = null;
  this.x19 = null;
}

function B() {
  A.call(this);
}
Object.setPrototypeOf(B, A);
```

Here we will always have 10 in-object properties for instances of
`B` (due to the generic over-allocation logic), and the other 10
properties have to be allocated in the out-of-object PropertyArray.

This is unfortunate and actually not necessary. Instead we could just
do the same [[Prototype]] walk on the constructor for regular function
constructors that we perform for derived (native) class constructors.
This CL changes that, such that we give the same treatment to transpiled
class that we have for native classes.

R=verwaest@chromium.org

Bug: v8:8764, v8:8765
Doc: https://bit.ly/v8-instance-presizing-with-transpiled-classes
Change-Id: Iac54391e41c9a39101751a678b3a647269fb009d
Reviewed-on: https://chromium-review.googlesource.com/c/1442643
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarToon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/master@{#59214}
parent 3c3233e6
......@@ -13425,19 +13425,10 @@ void JSFunction::EnsureHasInitialMap(Handle<JSFunction> function) {
instance_type = JS_OBJECT_TYPE;
}
// The constructor should be compiled for the optimization hints to be
// available.
int expected_nof_properties = 0;
IsCompiledScope is_compiled_scope(function->shared()->is_compiled_scope());
if (is_compiled_scope.is_compiled() ||
Compiler::Compile(function, Compiler::CLEAR_EXCEPTION,
&is_compiled_scope)) {
DCHECK(function->shared()->is_compiled());
expected_nof_properties = function->shared()->expected_nof_properties();
}
int instance_size;
int inobject_properties;
int expected_nof_properties =
CalculateExpectedNofProperties(isolate, function);
CalculateInstanceSizeHelper(instance_type, false, 0, expected_nof_properties,
&instance_size, &inobject_properties);
......@@ -13461,6 +13452,7 @@ void JSFunction::EnsureHasInitialMap(Handle<JSFunction> function) {
}
namespace {
bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
Handle<JSFunction> constructor,
Handle<Map> constructor_initial_map) {
......@@ -13486,21 +13478,19 @@ bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
int in_object_properties;
int embedder_fields =
JSObject::GetEmbedderFieldCount(*constructor_initial_map);
bool success = JSFunction::CalculateInstanceSizeForDerivedClass(
new_target, instance_type, embedder_fields, &instance_size,
&in_object_properties);
Handle<Map> map;
if (success) {
int pre_allocated = constructor_initial_map->GetInObjectProperties() -
constructor_initial_map->UnusedPropertyFields();
CHECK_LE(constructor_initial_map->UsedInstanceSize(), instance_size);
int unused_property_fields = in_object_properties - pre_allocated;
map = Map::CopyInitialMap(isolate, constructor_initial_map, instance_size,
in_object_properties, unused_property_fields);
} else {
map = Map::CopyInitialMap(isolate, constructor_initial_map);
}
int expected_nof_properties =
JSFunction::CalculateExpectedNofProperties(isolate, new_target);
JSFunction::CalculateInstanceSizeHelper(
instance_type, true, embedder_fields, expected_nof_properties,
&instance_size, &in_object_properties);
int pre_allocated = constructor_initial_map->GetInObjectProperties() -
constructor_initial_map->UnusedPropertyFields();
CHECK_LE(constructor_initial_map->UsedInstanceSize(), instance_size);
int unused_property_fields = in_object_properties - pre_allocated;
Handle<Map> map =
Map::CopyInitialMap(isolate, constructor_initial_map, instance_size,
in_object_properties, unused_property_fields);
map->set_new_target_is_base(false);
Handle<Object> prototype(new_target->instance_prototype(), isolate);
JSFunction::SetInitialMap(new_target, map, prototype);
......@@ -14321,6 +14311,41 @@ int SharedFunctionInfo::FindIndexInScript(Isolate* isolate) const {
return FunctionLiteral::kIdTypeInvalid;
}
// static
int JSFunction::CalculateExpectedNofProperties(Isolate* isolate,
Handle<JSFunction> function) {
int expected_nof_properties = 0;
for (PrototypeIterator iter(isolate, function, kStartAtReceiver);
!iter.IsAtEnd(); iter.Advance()) {
Handle<JSReceiver> current =
PrototypeIterator::GetCurrent<JSReceiver>(iter);
if (!current->IsJSFunction()) break;
Handle<JSFunction> func = Handle<JSFunction>::cast(current);
// The super constructor should be compiled for the number of expected
// properties to be available.
Handle<SharedFunctionInfo> shared(func->shared(), isolate);
IsCompiledScope is_compiled_scope(shared->is_compiled_scope());
if (is_compiled_scope.is_compiled() ||
Compiler::Compile(func, Compiler::CLEAR_EXCEPTION,
&is_compiled_scope)) {
DCHECK(shared->is_compiled());
int count = shared->expected_nof_properties();
// Check that the estimate is sane.
if (expected_nof_properties <= JSObject::kMaxInObjectProperties - count) {
expected_nof_properties += count;
} else {
return JSObject::kMaxInObjectProperties;
}
} else {
// In case there was a compilation error for the constructor we will
// throw an error during instantiation.
break;
}
}
return expected_nof_properties;
}
// static
void JSFunction::CalculateInstanceSizeHelper(InstanceType instance_type,
bool has_prototype_slot,
int requested_embedder_fields,
......@@ -14354,48 +14379,6 @@ void JSFunction::CalculateInstanceSizeHelper(InstanceType instance_type,
static_cast<unsigned>(JSObject::kMaxInstanceSize));
}
// static
bool JSFunction::CalculateInstanceSizeForDerivedClass(
Handle<JSFunction> function, InstanceType instance_type,
int requested_embedder_fields, int* instance_size,
int* in_object_properties) {
Isolate* isolate = function->GetIsolate();
int expected_nof_properties = 0;
for (PrototypeIterator iter(isolate, function, kStartAtReceiver);
!iter.IsAtEnd(); iter.Advance()) {
Handle<JSReceiver> current =
PrototypeIterator::GetCurrent<JSReceiver>(iter);
if (!current->IsJSFunction()) break;
Handle<JSFunction> func(Handle<JSFunction>::cast(current));
// The super constructor should be compiled for the number of expected
// properties to be available.
Handle<SharedFunctionInfo> shared(func->shared(), isolate);
IsCompiledScope is_compiled_scope(shared->is_compiled_scope());
if (is_compiled_scope.is_compiled() ||
Compiler::Compile(func, Compiler::CLEAR_EXCEPTION,
&is_compiled_scope)) {
DCHECK(shared->is_compiled());
int count = shared->expected_nof_properties();
// Check that the estimate is sane.
if (expected_nof_properties <= JSObject::kMaxInObjectProperties - count) {
expected_nof_properties += count;
} else {
expected_nof_properties = JSObject::kMaxInObjectProperties;
}
} else {
// In case there was a compilation error for the constructor we will
// throw an error during instantiation. Hence we directly return 0;
return false;
}
if (!IsDerivedConstructor(shared->kind())) break;
}
CalculateInstanceSizeHelper(instance_type, true, requested_embedder_fields,
expected_nof_properties, instance_size,
in_object_properties);
return true;
}
// Output the source code without any allocation in the heap.
std::ostream& operator<<(std::ostream& os, const SourceCodeOf& v) {
const SharedFunctionInfo s = v.value;
......
......@@ -1091,10 +1091,8 @@ class JSFunction : public JSObject {
DECL_CAST(JSFunction)
// Calculate the instance size and in-object properties count.
static bool CalculateInstanceSizeForDerivedClass(
Handle<JSFunction> function, InstanceType instance_type,
int requested_embedder_fields, int* instance_size,
int* in_object_properties);
static V8_WARN_UNUSED_RESULT int CalculateExpectedNofProperties(
Isolate* isolate, Handle<JSFunction> function);
static void CalculateInstanceSizeHelper(InstanceType instance_type,
bool has_prototype_slot,
int requested_embedder_fields,
......
......@@ -1210,6 +1210,83 @@ TEST(SubclassPromiseBuiltinNoInlineNew) {
TestSubclassPromiseBuiltin();
}
TEST(SubclassTranspiledClassHierarchy) {
CcTest::InitializeVM();
v8::HandleScope scope(CcTest::isolate());
CompileRun(
"Object.setPrototypeOf(B, A);\n"
"function A() {\n"
" this.a0 = 0;\n"
" this.a1 = 1;\n"
" this.a2 = 1;\n"
" this.a3 = 1;\n"
" this.a4 = 1;\n"
" this.a5 = 1;\n"
" this.a6 = 1;\n"
" this.a7 = 1;\n"
" this.a8 = 1;\n"
" this.a9 = 1;\n"
" this.a10 = 1;\n"
" this.a11 = 1;\n"
" this.a12 = 1;\n"
" this.a13 = 1;\n"
" this.a14 = 1;\n"
" this.a15 = 1;\n"
" this.a16 = 1;\n"
" this.a17 = 1;\n"
" this.a18 = 1;\n"
" this.a19 = 1;\n"
"};\n"
"function B() {\n"
" A.call(this);\n"
" this.b = 1;\n"
"};\n");
Handle<JSFunction> func = GetGlobal<JSFunction>("B");
// Zero instances have been created so far.
CHECK(!func->has_initial_map());
v8::Local<v8::Script> new_script = v8_compile("new B()");
RunI<JSObject>(new_script);
CHECK(func->has_initial_map());
Handle<Map> initial_map(func->initial_map(), func->GetIsolate());
CHECK_EQ(JS_OBJECT_TYPE, initial_map->instance_type());
// One instance of a subclass created.
CHECK_EQ(Map::kSlackTrackingCounterStart - 1,
initial_map->construction_counter());
CHECK(initial_map->IsInobjectSlackTrackingInProgress());
// Create two instances in order to ensure that |obj|.o is a data field
// in case of Function subclassing.
Handle<JSObject> obj = RunI<JSObject>(new_script);
// Two instances of a subclass created.
CHECK_EQ(Map::kSlackTrackingCounterStart - 2,
initial_map->construction_counter());
CHECK(initial_map->IsInobjectSlackTrackingInProgress());
CHECK(IsObjectShrinkable(*obj));
// Create several subclass instances to complete the tracking.
for (int i = 2; i < Map::kGenerousAllocationCount; i++) {
CHECK(initial_map->IsInobjectSlackTrackingInProgress());
Handle<JSObject> tmp = RunI<JSObject>(new_script);
CHECK_EQ(initial_map->IsInobjectSlackTrackingInProgress(),
IsObjectShrinkable(*tmp));
}
CHECK(!initial_map->IsInobjectSlackTrackingInProgress());
CHECK(!IsObjectShrinkable(*obj));
// No slack left.
CHECK_EQ(21, obj->map()->GetInObjectProperties());
CHECK_EQ(JS_OBJECT_TYPE, obj->map()->instance_type());
}
} // namespace test_inobject_slack_tracking
} // namespace internal
} // namespace v8
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment