Skip to content

Commit

Permalink
[builtins] Properly optimize Object.prototype.isPrototypeOf.
Browse files Browse the repository at this point in the history
Port the baseline implementation of Object.prototype.isPrototypeOf to
the CodeStubAssembler, sharing the existing prototype chain lookup logic
with the instanceof / OrdinaryHasInstance implementation. Based on that,
do the same in TurboFan, introducing a new JSHasInPrototypeChain
operator, which encapsulates the central prototype chain walk logic.

This speeds up Object.prototype.isPrototypeOf by more than a factor of
four, so that the code

  A.prototype.isPrototypeOf(a)

is now performance-wise on par with

  a instanceof A

for the case where A is a regular constructor function and a is an
instance of A.

Since instanceof does more than just the fundamental prototype chain
lookup, it was discovered in Node core that O.p.isPrototypeOf would
be a more appropriate alternative for certain sanity checks, since
it's less vulnerable to monkey-patching. In addition, the Object
builtin would also avoid the performance-cliff associated with
instanceof (due to the Symbol.hasInstance hook), as for example hit
by nodejs/node#13403 (comment).
The main blocker was the missing performance of isPrototypeOf, since
it was still a JS builtin backed by a runtime call.

This CL also adds more test coverage for the
Object.prototype.isPrototypeOf builtin, especially when called from
optimized code.

CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:linux_chromium_rel_ng
BUG=v8:5269,v8:5989,v8:6483
[email protected]

Review-Url: https://codereview.chromium.org/2934893002
Cr-Commit-Position: refs/heads/master@{#45925}
  • Loading branch information
bmeurer authored and Commit Bot committed Jun 13, 2017
1 parent 8196e10 commit b11c557
Show file tree
Hide file tree
Showing 23 changed files with 553 additions and 256 deletions.
2 changes: 2 additions & 0 deletions src/bootstrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
SimpleInstallFunction(isolate->initial_object_prototype(),
"__lookupSetter__", Builtins::kObjectLookupSetter, 1,
true);
SimpleInstallFunction(isolate->initial_object_prototype(), "isPrototypeOf",
Builtins::kObjectPrototypeIsPrototypeOf, 1, true);
SimpleInstallFunction(
isolate->initial_object_prototype(), "propertyIsEnumerable",
Builtins::kObjectPrototypePropertyIsEnumerable, 1, false);
Expand Down
1 change: 1 addition & 0 deletions src/builtins/builtins-definitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ namespace internal {
TFJ(ObjectProtoToString, 0) \
/* ES6 #sec-object.prototype.valueof */ \
TFJ(ObjectPrototypeValueOf, 0) \
TFJ(ObjectPrototypeIsPrototypeOf, 1, kValue) \
CPP(ObjectPrototypePropertyIsEnumerable) \
CPP(ObjectPrototypeGetProto) \
CPP(ObjectPrototypeSetProto) \
Expand Down
46 changes: 46 additions & 0 deletions src/builtins/builtins-object-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,52 @@ TF_BUILTIN(ObjectKeys, ObjectBuiltinsAssembler) {
}
}

// ES #sec-object.prototype.isprototypeof
TF_BUILTIN(ObjectPrototypeIsPrototypeOf, ObjectBuiltinsAssembler) {
Node* receiver = Parameter(Descriptor::kReceiver);
Node* value = Parameter(Descriptor::kValue);
Node* context = Parameter(Descriptor::kContext);
Label if_receiverisnullorundefined(this, Label::kDeferred),
if_valueisnotreceiver(this, Label::kDeferred);

// We only check whether {value} is a Smi here, so that the
// prototype chain walk below can safely access the {value}s
// map. We don't rule out Primitive {value}s, since all of
// them have null as their prototype, so the chain walk below
// immediately aborts and returns false anyways.
GotoIf(TaggedIsSmi(value), &if_valueisnotreceiver);

// Check if {receiver} is either null or undefined and in that case,
// invoke the ToObject builtin, which raises the appropriate error.
// Otherwise we don't need to invoke ToObject, since {receiver} is
// either already a JSReceiver, in which case ToObject is a no-op,
// or it's a Primitive and ToObject would allocate a fresh JSValue
// wrapper, which wouldn't be identical to any existing JSReceiver
// found in the prototype chain of {value}, hence it will return
// false no matter if we search for the Primitive {receiver} or
// a newly allocated JSValue wrapper for {receiver}.
GotoIf(IsNull(receiver), &if_receiverisnullorundefined);
GotoIf(IsUndefined(receiver), &if_receiverisnullorundefined);

// Loop through the prototype chain looking for the {receiver}.
Return(HasInPrototypeChain(context, value, receiver));

BIND(&if_receiverisnullorundefined);
{
// If {value} is a primitive HeapObject, we need to return
// false instead of throwing an exception per order of the
// steps in the specification, so check that first here.
GotoIfNot(IsJSReceiver(value), &if_valueisnotreceiver);

// Simulate the ToObject invocation on {receiver}.
CallBuiltin(Builtins::kToObject, context, receiver);
Unreachable();
}

BIND(&if_valueisnotreceiver);
Return(FalseConstant());
}

// ES6 #sec-object.prototype.tostring
TF_BUILTIN(ObjectProtoToString, ObjectBuiltinsAssembler) {
Label return_undefined(this, Label::kDeferred),
Expand Down
107 changes: 66 additions & 41 deletions src/code-stub-assembler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5997,18 +5997,78 @@ void CodeStubAssembler::TryPrototypeChainLookup(
}
}

Node* CodeStubAssembler::OrdinaryHasInstance(Node* context, Node* callable,
Node* object) {
Node* CodeStubAssembler::HasInPrototypeChain(Node* context, Node* object,
Node* prototype) {
CSA_ASSERT(this, TaggedIsNotSmi(object));
VARIABLE(var_result, MachineRepresentation::kTagged);
Label return_false(this), return_true(this),
return_runtime(this, Label::kDeferred), return_result(this);

// Loop through the prototype chain looking for the {prototype}.
VARIABLE(var_object_map, MachineRepresentation::kTagged, LoadMap(object));
Label loop(this, &var_object_map);
Goto(&loop);
BIND(&loop);
{
// Check if we can determine the prototype directly from the {object_map}.
Label if_objectisdirect(this), if_objectisspecial(this, Label::kDeferred);
Node* object_map = var_object_map.value();
Node* object_instance_type = LoadMapInstanceType(object_map);
Branch(IsSpecialReceiverInstanceType(object_instance_type),
&if_objectisspecial, &if_objectisdirect);
BIND(&if_objectisspecial);
{
// The {object_map} is a special receiver map or a primitive map, check
// if we need to use the if_objectisspecial path in the runtime.
GotoIf(InstanceTypeEqual(object_instance_type, JS_PROXY_TYPE),
&return_runtime);
Node* object_bitfield = LoadMapBitField(object_map);
Node* mask = Int32Constant(1 << Map::kHasNamedInterceptor |
1 << Map::kIsAccessCheckNeeded);
Branch(Word32NotEqual(Word32And(object_bitfield, mask), Int32Constant(0)),
&return_runtime, &if_objectisdirect);
}
BIND(&if_objectisdirect);

// Check the current {object} prototype.
Node* object_prototype = LoadMapPrototype(object_map);
GotoIf(IsNull(object_prototype), &return_false);
GotoIf(WordEqual(object_prototype, prototype), &return_true);

// Continue with the prototype.
CSA_ASSERT(this, TaggedIsNotSmi(object_prototype));
var_object_map.Bind(LoadMap(object_prototype));
Goto(&loop);
}

BIND(&return_true);
var_result.Bind(TrueConstant());
Goto(&return_result);

BIND(&return_false);
var_result.Bind(FalseConstant());
Goto(&return_result);

BIND(&return_runtime);
{
// Fallback to the runtime implementation.
var_result.Bind(
CallRuntime(Runtime::kHasInPrototypeChain, context, object, prototype));
}
Goto(&return_result);

BIND(&return_result);
return var_result.value();
}

Node* CodeStubAssembler::OrdinaryHasInstance(Node* context, Node* callable,
Node* object) {
VARIABLE(var_result, MachineRepresentation::kTagged);
Label return_runtime(this, Label::kDeferred), return_result(this);

// Goto runtime if {object} is a Smi.
GotoIf(TaggedIsSmi(object), &return_runtime);

// Load map of {object}.
Node* object_map = LoadMap(object);

// Goto runtime if {callable} is a Smi.
GotoIf(TaggedIsSmi(callable), &return_runtime);

Expand Down Expand Up @@ -6056,42 +6116,7 @@ Node* CodeStubAssembler::OrdinaryHasInstance(Node* context, Node* callable,
}

// Loop through the prototype chain looking for the {callable} prototype.
VARIABLE(var_object_map, MachineRepresentation::kTagged, object_map);
Label loop(this, &var_object_map);
Goto(&loop);
BIND(&loop);
{
Node* object_map = var_object_map.value();

// Check if the current {object} needs to be access checked.
Node* object_bitfield = LoadMapBitField(object_map);
GotoIfNot(
Word32Equal(Word32And(object_bitfield,
Int32Constant(1 << Map::kIsAccessCheckNeeded)),
Int32Constant(0)),
&return_runtime);

// Check if the current {object} is a proxy.
Node* object_instance_type = LoadMapInstanceType(object_map);
GotoIf(Word32Equal(object_instance_type, Int32Constant(JS_PROXY_TYPE)),
&return_runtime);

// Check the current {object} prototype.
Node* object_prototype = LoadMapPrototype(object_map);
GotoIf(WordEqual(object_prototype, NullConstant()), &return_false);
GotoIf(WordEqual(object_prototype, callable_prototype), &return_true);

// Continue with the prototype.
var_object_map.Bind(LoadMap(object_prototype));
Goto(&loop);
}

BIND(&return_true);
var_result.Bind(BooleanConstant(true));
Goto(&return_result);

BIND(&return_false);
var_result.Bind(BooleanConstant(false));
var_result.Bind(HasInPrototypeChain(context, object, callable_prototype));
Goto(&return_result);

BIND(&return_runtime);
Expand Down
4 changes: 4 additions & 0 deletions src/code-stub-assembler.h
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,10 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
Label* if_end, Label* if_bailout);

// Instanceof helpers.
// Returns true if {object} has {prototype} somewhere in it's prototype
// chain, otherwise false is returned. Might cause arbitrary side effects
// due to [[GetPrototypeOf]] invocations.
Node* HasInPrototypeChain(Node* context, Node* object, Node* prototype);
// ES6 section 7.3.19 OrdinaryHasInstance (C, O)
Node* OrdinaryHasInstance(Node* context, Node* callable, Node* object);

Expand Down
34 changes: 34 additions & 0 deletions src/compiler/js-call-reducer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,38 @@ Reduction JSCallReducer::ReduceObjectPrototypeGetProto(Node* node) {
return ReduceObjectGetPrototype(node, receiver);
}

// ES #sec-object.prototype.isprototypeof
Reduction JSCallReducer::ReduceObjectPrototypeIsPrototypeOf(Node* node) {
DCHECK_EQ(IrOpcode::kJSCall, node->opcode());
Node* receiver = NodeProperties::GetValueInput(node, 1);
Node* value = node->op()->ValueInputCount() > 2
? NodeProperties::GetValueInput(node, 2)
: jsgraph()->UndefinedConstant();
Node* effect = NodeProperties::GetEffectInput(node);

// Ensure that the {receiver} is known to be a JSReceiver (so that
// the ToObject step of Object.prototype.isPrototypeOf is a no-op).
ZoneHandleSet<Map> receiver_maps;
NodeProperties::InferReceiverMapsResult result =
NodeProperties::InferReceiverMaps(receiver, effect, &receiver_maps);
if (result == NodeProperties::kNoReceiverMaps) return NoChange();
for (size_t i = 0; i < receiver_maps.size(); ++i) {
if (!receiver_maps[i]->IsJSReceiverMap()) return NoChange();
}

// We don't check whether {value} is a proper JSReceiver here explicitly,
// and don't explicitly rule out Primitive {value}s, since all of them
// have null as their prototype, so the prototype chain walk inside the
// JSHasInPrototypeChain operator immediately aborts and yields false.
NodeProperties::ReplaceValueInput(node, value, 0);
NodeProperties::ReplaceValueInput(node, receiver, 1);
for (int i = node->op()->ValueInputCount(); i-- > 2;) {
node->RemoveInput(i);
}
NodeProperties::ChangeOp(node, javascript()->HasInPrototypeChain());
return Changed(node);
}

// ES6 section 26.1.7 Reflect.getPrototypeOf ( target )
Reduction JSCallReducer::ReduceReflectGetPrototypeOf(Node* node) {
DCHECK_EQ(IrOpcode::kJSCall, node->opcode());
Expand Down Expand Up @@ -741,6 +773,8 @@ Reduction JSCallReducer::ReduceJSCall(Node* node) {
return ReduceObjectGetPrototypeOf(node);
case Builtins::kObjectPrototypeGetProto:
return ReduceObjectPrototypeGetProto(node);
case Builtins::kObjectPrototypeIsPrototypeOf:
return ReduceObjectPrototypeIsPrototypeOf(node);
case Builtins::kReflectGetPrototypeOf:
return ReduceReflectGetPrototypeOf(node);
case Builtins::kArrayForEach:
Expand Down
1 change: 1 addition & 0 deletions src/compiler/js-call-reducer.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class JSCallReducer final : public AdvancedReducer {
Reduction ReduceObjectGetPrototype(Node* node, Node* object);
Reduction ReduceObjectGetPrototypeOf(Node* node);
Reduction ReduceObjectPrototypeGetProto(Node* node);
Reduction ReduceObjectPrototypeIsPrototypeOf(Node* node);
Reduction ReduceReflectGetPrototypeOf(Node* node);
Reduction ReduceArrayForEach(Handle<JSFunction> function, Node* node);
Reduction ReduceSpreadCall(Node* node, int arity);
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/js-generic-lowering.cc
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ void JSGenericLowering::LowerJSGetSuperConstructor(Node* node) {
ReplaceWithStubCall(node, callable, flags);
}

void JSGenericLowering::LowerJSHasInPrototypeChain(Node* node) {
ReplaceWithRuntimeCall(node, Runtime::kHasInPrototypeChain);
}

void JSGenericLowering::LowerJSInstanceOf(Node* node) {
CallDescriptor::Flags flags = FrameStateFlagForCall(node);
Callable callable = Builtins::CallableFor(isolate(), Builtins::kInstanceOf);
Expand Down
Loading

0 comments on commit b11c557

Please sign in to comment.