Skip to content

Commit

Permalink
[es2015] Optimize Object.is baseline and interesting cases.
Browse files Browse the repository at this point in the history
The Object.is builtin provides an entry point to the abstract operation
SameValue, which properly distinguishes -0 and 0, and also identifies
NaNs. Most of the time you don't need these, but rather just regular
strict equality, but when you do, Object.is(o, -0) is the most readable
way to check for minus zero.

This is for example used in Node.js by formatNumber to properly print -0
for negative zero. However since the builtin thus far implemented as C++
builtin and TurboFan didn't know anything about it, Node.js considering
to go with a more performant, less readable version (which also makes
assumptions about the input value) in

  nodejs/node#15726

until the performance of Object.is will be on par (so hopefully we can
go back to Object.is in Node 9).

This CL ports the baseline implementation of Object.is to CSA, which
is pretty straight-forward since SameValue is already available in
CodeStubAssembler, and inlines a few interesting cases into TurboFan,
i.e. comparing same SSA node, and checking for -0 and NaN explicitly.

On the micro-benchmarks we go from

  testNumberIsMinusZero: 1000 ms.
  testObjectIsMinusZero: 929 ms.
  testObjectIsNaN: 954 ms.
  testObjectIsSame: 793 ms.
  testStrictEqualSame: 104 ms.

to

  testNumberIsMinusZero: 89 ms.
  testObjectIsMinusZero: 88 ms.
  testObjectIsNaN: 88 ms.
  testObjectIsSame: 86 ms.
  testStrictEqualSame: 105 ms.

which is a nice 10x to 11x improvement and brings Object.is on par with
strict equality for most cases.

Drive-by-fix: Also refactor and optimize the SameValue check in the
CodeStubAssembler to avoid code bloat (by not inlining StrictEqual
into every user of SameValue, and also avoiding useless checks).

Bug: v8:6882
Change-Id: Ibffd8c36511f219fcce0d89ed4e1073f5d6c6344
Reviewed-on: https://chromium-review.googlesource.com/700254
Reviewed-by: Jaroslav Sevcik <[email protected]>
Commit-Queue: Benedikt Meurer <[email protected]>
Cr-Commit-Position: refs/heads/master@{#48275}
  • Loading branch information
bmeurer authored and Commit Bot committed Oct 4, 2017
1 parent bfb43f8 commit d4da17c
Show file tree
Hide file tree
Showing 21 changed files with 385 additions and 85 deletions.
2 changes: 1 addition & 1 deletion src/builtins/builtins-definitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ namespace internal {
CPP(ObjectGetOwnPropertySymbols) \
CPP(ObjectGetPrototypeOf) \
CPP(ObjectSetPrototypeOf) \
CPP(ObjectIs) \
TFJ(ObjectIs, 2, kLeft, kRight) \
CPP(ObjectIsExtensible) \
CPP(ObjectIsFrozen) \
CPP(ObjectIsSealed) \
Expand Down
15 changes: 15 additions & 0 deletions src/builtins/builtins-object-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,21 @@ TF_BUILTIN(ObjectCreate, ObjectBuiltinsAssembler) {
}
}

// ES #sec-object.is
TF_BUILTIN(ObjectIs, ObjectBuiltinsAssembler) {
Node* const left = Parameter(Descriptor::kLeft);
Node* const right = Parameter(Descriptor::kRight);

Label return_true(this), return_false(this);
BranchIfSameValue(left, right, &return_true, &return_false);

BIND(&return_true);
Return(TrueConstant());

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

TF_BUILTIN(CreateIterResultObject, ObjectBuiltinsAssembler) {
Node* const value = Parameter(Descriptor::kValue);
Node* const done = Parameter(Descriptor::kDone);
Expand Down
9 changes: 0 additions & 9 deletions src/builtins/builtins-object.cc
Original file line number Diff line number Diff line change
Expand Up @@ -369,15 +369,6 @@ BUILTIN(ObjectGetOwnPropertySymbols) {
return GetOwnPropertyKeys(isolate, args, SKIP_STRINGS);
}

// ES#sec-object.is Object.is ( value1, value2 )
BUILTIN(ObjectIs) {
SealHandleScope shs(isolate);
DCHECK_EQ(3, args.length());
Handle<Object> value1 = args.at(1);
Handle<Object> value2 = args.at(2);
return isolate->heap()->ToBoolean(value1->SameValue(*value2));
}

// ES6 section 19.1.2.11 Object.isExtensible ( O )
BUILTIN(ObjectIsExtensible) {
HandleScope scope(isolate);
Expand Down
12 changes: 8 additions & 4 deletions src/builtins/builtins-promise-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,8 @@ void PromiseBuiltinsAssembler::InternalResolvePromise(Node* context,
VARIABLE(var_reason, MachineRepresentation::kTagged);
VARIABLE(var_then, MachineRepresentation::kTagged);

Label do_enqueue(this), fulfill(this), if_cycle(this, Label::kDeferred),
Label do_enqueue(this), fulfill(this), if_nocycle(this),
if_cycle(this, Label::kDeferred),
if_rejectpromise(this, Label::kDeferred), out(this);

Label cycle_check(this);
Expand All @@ -693,7 +694,8 @@ void PromiseBuiltinsAssembler::InternalResolvePromise(Node* context,

BIND(&cycle_check);
// 6. If SameValue(resolution, promise) is true, then
GotoIf(SameValue(promise, result), &if_cycle);
BranchIfSameValue(promise, result, &if_cycle, &if_nocycle);
BIND(&if_nocycle);

// 7. If Type(resolution) is not Object, then
GotoIf(TaggedIsSmi(result), &fulfill);
Expand Down Expand Up @@ -1435,11 +1437,13 @@ TF_BUILTIN(PromiseResolve, PromiseBuiltinsAssembler) {
// they could be of the same subclass.
BIND(&if_value_or_constructor_are_not_native_promise);
{
Label if_return(this);
Node* const xConstructor =
GetProperty(context, value, isolate->factory()->constructor_string());
BranchIfSameValue(xConstructor, constructor, &if_return,
&if_need_to_allocate);

GotoIfNot(SameValue(xConstructor, constructor), &if_need_to_allocate);

BIND(&if_return);
Return(value);
}

Expand Down
5 changes: 2 additions & 3 deletions src/builtins/builtins-proxy-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -517,9 +517,8 @@ void ProxiesCodeStubAssembler::CheckGetSetTrapResult(

// If SameValue(trapResult, targetDesc.[[Value]]) is false,
// throw a TypeError exception.
GotoIfNot(SameValue(trap_result, var_value.value()),
&throw_non_configurable_data);
Goto(check_passed);
BranchIfSameValue(trap_result, var_value.value(), check_passed,
&throw_non_configurable_data);
}

BIND(&check_accessor);
Expand Down
11 changes: 6 additions & 5 deletions src/builtins/builtins-regexp-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2208,9 +2208,10 @@ void RegExpBuiltinsAssembler::RegExpPrototypeSearchBodySlow(

// Ensure last index is 0.
{
Label next(this);
GotoIf(SameValue(previous_last_index, smi_zero), &next);
Label next(this), slow(this, Label::kDeferred);
BranchIfSameValue(previous_last_index, smi_zero, &next, &slow);

BIND(&slow);
SlowStoreLastIndex(context, regexp, smi_zero);
Goto(&next);
BIND(&next);
Expand All @@ -2221,14 +2222,14 @@ void RegExpBuiltinsAssembler::RegExpPrototypeSearchBodySlow(

// Reset last index if necessary.
{
Label next(this);
Label next(this), slow(this, Label::kDeferred);
Node* const current_last_index = SlowLoadLastIndex(context, regexp);

GotoIf(SameValue(current_last_index, previous_last_index), &next);
BranchIfSameValue(current_last_index, previous_last_index, &next, &slow);

BIND(&slow);
SlowStoreLastIndex(context, regexp, previous_last_index);
Goto(&next);

BIND(&next);
}

Expand Down
139 changes: 79 additions & 60 deletions src/code-stub-assembler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9010,88 +9010,107 @@ Node* CodeStubAssembler::StrictEqual(Node* lhs, Node* rhs,
// ECMA#sec-samevalue
// This algorithm differs from the Strict Equality Comparison Algorithm in its
// treatment of signed zeroes and NaNs.
Node* CodeStubAssembler::SameValue(Node* lhs, Node* rhs) {
VARIABLE(var_result, MachineRepresentation::kWord32);
Label strict_equal(this), out(this);
void CodeStubAssembler::BranchIfSameValue(Node* lhs, Node* rhs, Label* if_true,
Label* if_false) {
VARIABLE(var_lhs_value, MachineRepresentation::kFloat64);
VARIABLE(var_rhs_value, MachineRepresentation::kFloat64);
Label do_fcmp(this);

Node* const int_false = Int32Constant(0);
Node* const int_true = Int32Constant(1);
// Immediately jump to {if_true} if {lhs} == {rhs}, because - unlike
// StrictEqual - SameValue considers two NaNs to be equal.
GotoIf(WordEqual(lhs, rhs), if_true);

Label if_equal(this), if_notequal(this);
Branch(WordEqual(lhs, rhs), &if_equal, &if_notequal);
// Check if the {lhs} is a Smi.
Label if_lhsissmi(this), if_lhsisheapobject(this);
Branch(TaggedIsSmi(lhs), &if_lhsissmi, &if_lhsisheapobject);

BIND(&if_equal);
BIND(&if_lhsissmi);
{
// This covers the case when {lhs} == {rhs}. We can simply return true
// because SameValue considers two NaNs to be equal.

var_result.Bind(int_true);
Goto(&out);
// Since {lhs} is a Smi, the comparison can only yield true
// iff the {rhs} is a HeapNumber with the same float64 value.
GotoIf(TaggedIsSmi(rhs), if_false);
GotoIfNot(IsHeapNumber(rhs), if_false);
var_lhs_value.Bind(SmiToFloat64(lhs));
var_rhs_value.Bind(LoadHeapNumberValue(rhs));
Goto(&do_fcmp);
}

BIND(&if_notequal);
BIND(&if_lhsisheapobject);
{
// This covers the case when {lhs} != {rhs}. We only handle numbers here
// and defer to StrictEqual for the rest.

Node* const lhs_float = TryTaggedToFloat64(lhs, &strict_equal);
Node* const rhs_float = TryTaggedToFloat64(rhs, &strict_equal);
// Check if the {rhs} is a Smi.
Label if_rhsissmi(this), if_rhsisheapobject(this);
Branch(TaggedIsSmi(rhs), &if_rhsissmi, &if_rhsisheapobject);

Label if_lhsisnan(this), if_lhsnotnan(this);
BranchIfFloat64IsNaN(lhs_float, &if_lhsisnan, &if_lhsnotnan);

BIND(&if_lhsisnan);
BIND(&if_rhsissmi);
{
// Return true iff {rhs} is NaN.

Node* const result =
SelectConstant(Float64Equal(rhs_float, rhs_float), int_false,
int_true, MachineRepresentation::kWord32);
var_result.Bind(result);
Goto(&out);
// Since {rhs} is a Smi, the comparison can only yield true
// iff the {lhs} is a HeapNumber with the same float64 value.
GotoIfNot(IsHeapNumber(lhs), if_false);
var_lhs_value.Bind(LoadHeapNumberValue(lhs));
var_rhs_value.Bind(SmiToFloat64(rhs));
Goto(&do_fcmp);
}

BIND(&if_lhsnotnan);
BIND(&if_rhsisheapobject);
{
Label if_floatisequal(this), if_floatnotequal(this);
Branch(Float64Equal(lhs_float, rhs_float), &if_floatisequal,
&if_floatnotequal);

BIND(&if_floatisequal);
// Now this can only yield true if either both {lhs} and {rhs}
// are HeapNumbers with the same value or both {lhs} and {rhs}
// are Strings with the same character sequence.
Label if_lhsisheapnumber(this), if_lhsisstring(this);
Node* const lhs_map = LoadMap(lhs);
GotoIf(IsHeapNumberMap(lhs_map), &if_lhsisheapnumber);
Node* const lhs_instance_type = LoadMapInstanceType(lhs_map);
Branch(IsStringInstanceType(lhs_instance_type), &if_lhsisstring,
if_false);

BIND(&if_lhsisheapnumber);
{
// We still need to handle the case when {lhs} and {rhs} are -0.0 and
// 0.0 (or vice versa). Compare the high word to
// distinguish between the two.

Node* const lhs_hi_word = Float64ExtractHighWord32(lhs_float);
Node* const rhs_hi_word = Float64ExtractHighWord32(rhs_float);

// If x is +0 and y is -0, return false.
// If x is -0 and y is +0, return false.

Node* const result = Word32Equal(lhs_hi_word, rhs_hi_word);
var_result.Bind(result);
Goto(&out);
GotoIfNot(IsHeapNumber(rhs), if_false);
var_lhs_value.Bind(LoadHeapNumberValue(lhs));
var_rhs_value.Bind(LoadHeapNumberValue(rhs));
Goto(&do_fcmp);
}

BIND(&if_floatnotequal);
BIND(&if_lhsisstring);
{
var_result.Bind(int_false);
Goto(&out);
// Now we can only yield true if {rhs} is also a String
// with the same sequence of characters.
GotoIfNot(IsString(rhs), if_false);
Node* const result =
CallBuiltin(Builtins::kStringEqual, NoContextConstant(), lhs, rhs);
Branch(IsTrue(result), if_true, if_false);
}
}
}

BIND(&strict_equal);
BIND(&do_fcmp);
{
Node* const is_equal = StrictEqual(lhs, rhs);
Node* const result = WordEqual(is_equal, TrueConstant());
var_result.Bind(result);
Goto(&out);
}
Node* const lhs_value = var_lhs_value.value();
Node* const rhs_value = var_rhs_value.value();

BIND(&out);
return var_result.value();
Label if_equal(this), if_notequal(this);
Branch(Float64Equal(lhs_value, rhs_value), &if_equal, &if_notequal);

BIND(&if_equal);
{
// We still need to handle the case when {lhs} and {rhs} are -0.0 and
// 0.0 (or vice versa). Compare the high word to
// distinguish between the two.
Node* const lhs_hi_word = Float64ExtractHighWord32(lhs_value);
Node* const rhs_hi_word = Float64ExtractHighWord32(rhs_value);

// If x is +0 and y is -0, return false.
// If x is -0 and y is +0, return false.
Branch(Word32Equal(lhs_hi_word, rhs_hi_word), if_true, if_false);
}

BIND(&if_notequal);
{
// Return true iff both {rhs} and {lhs} are NaN.
GotoIf(Float64Equal(lhs_value, lhs_value), if_false);
Branch(Float64Equal(rhs_value, rhs_value), if_false, if_true);
}
}
}

Node* CodeStubAssembler::HasProperty(Node* object, Node* key, Node* context,
Expand Down
4 changes: 1 addition & 3 deletions src/code-stub-assembler.h
Original file line number Diff line number Diff line change
Expand Up @@ -1580,9 +1580,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
// ECMA#sec-samevalue
// Similar to StrictEqual except that NaNs are treated as equal and minus zero
// differs from positive zero.
// Unlike Equal and StrictEqual, returns a value suitable for use in Branch
// instructions, e.g. Branch(SameValue(...), &label).
Node* SameValue(Node* lhs, Node* rhs);
void BranchIfSameValue(Node* lhs, Node* rhs, Label* if_true, Label* if_false);

enum HasPropertyLookupMode { kHasProperty, kForInHasProperty };

Expand Down
28 changes: 28 additions & 0 deletions src/compiler/effect-control-linearizer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,9 @@ bool EffectControlLinearizer::TryWireInStateEffect(Node* node,
case IrOpcode::kObjectIsDetectableCallable:
result = LowerObjectIsDetectableCallable(node);
break;
case IrOpcode::kObjectIsMinusZero:
result = LowerObjectIsMinusZero(node);
break;
case IrOpcode::kObjectIsNaN:
result = LowerObjectIsNaN(node);
break;
Expand Down Expand Up @@ -1944,6 +1947,31 @@ Node* EffectControlLinearizer::LowerObjectIsDetectableCallable(Node* node) {
return done.PhiAt(0);
}

Node* EffectControlLinearizer::LowerObjectIsMinusZero(Node* node) {
Node* value = node->InputAt(0);
Node* zero = __ Int32Constant(0);

auto done = __ MakeLabel(MachineRepresentation::kBit);

// Check if {value} is a Smi.
__ GotoIf(ObjectIsSmi(value), &done, zero);

// Check if {value} is a HeapNumber.
Node* value_map = __ LoadField(AccessBuilder::ForMap(), value);
__ GotoIfNot(__ WordEqual(value_map, __ HeapNumberMapConstant()), &done,
zero);

// Check if {value} contains -0.
Node* value_value = __ LoadField(AccessBuilder::ForHeapNumberValue(), value);
__ Goto(&done,
__ Float64Equal(
__ Float64Div(__ Float64Constant(1.0), value_value),
__ Float64Constant(-std::numeric_limits<double>::infinity())));

__ Bind(&done);
return done.PhiAt(0);
}

Node* EffectControlLinearizer::LowerObjectIsNaN(Node* node) {
Node* value = node->InputAt(0);
Node* zero = __ Int32Constant(0);
Expand Down
1 change: 1 addition & 0 deletions src/compiler/effect-control-linearizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class V8_EXPORT_PRIVATE EffectControlLinearizer {
Node* LowerObjectIsArrayBufferView(Node* node);
Node* LowerObjectIsCallable(Node* node);
Node* LowerObjectIsDetectableCallable(Node* node);
Node* LowerObjectIsMinusZero(Node* node);
Node* LowerObjectIsNaN(Node* node);
Node* LowerObjectIsNonCallable(Node* node);
Node* LowerObjectIsNumber(Node* node);
Expand Down
1 change: 1 addition & 0 deletions src/compiler/graph-assembler.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ namespace compiler {
V(Int32LessThan) \
V(Float64Add) \
V(Float64Sub) \
V(Float64Div) \
V(Float64Mod) \
V(Float64Equal) \
V(Float64LessThan) \
Expand Down
Loading

0 comments on commit d4da17c

Please sign in to comment.