[turbofan] Optimize call on Math.min/Math.max with JSArray of double elements

This change inline call to Math.min/Math.max like

   Math.min.apply(this, arguments_list)

to avoid packing and unpacking doubles during the optimized code execution.

Change-Id: I674476f688213df8eb13ee8c876b280c8fa47263
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3799214
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Commit-Queue: Fanchen Kong <fanchen.kong@intel.com>
Cr-Commit-Position: refs/heads/main@{#83810}
This commit is contained in:
Fanchen Kong 2022-10-18 05:27:57 +08:00 committed by V8 LUCI CQ
parent fafd7c5d22
commit 680225d17e
12 changed files with 284 additions and 2 deletions

View File

@ -240,6 +240,7 @@ class EffectControlLinearizer {
Node* LowerFoldConstant(Node* node);
Node* LowerConvertReceiver(Node* node);
Node* LowerDateNow(Node* node);
Node* LowerDoubleArrayMinMax(Node* node);
// Lowering of optional operators.
Maybe<Node*> LowerFloat64RoundUp(Node* node);
@ -1417,6 +1418,10 @@ bool EffectControlLinearizer::TryWireInStateEffect(Node* node,
case IrOpcode::kFoldConstant:
result = LowerFoldConstant(node);
break;
case IrOpcode::kDoubleArrayMax:
case IrOpcode::kDoubleArrayMin:
result = LowerDoubleArrayMinMax(node);
break;
default:
return false;
}
@ -6266,6 +6271,48 @@ Node* EffectControlLinearizer::LowerFoldConstant(Node* node) {
return constant;
}
Node* EffectControlLinearizer::LowerDoubleArrayMinMax(Node* node) {
DCHECK(node->opcode() == IrOpcode::kDoubleArrayMin ||
node->opcode() == IrOpcode::kDoubleArrayMax);
bool is_max = node->opcode() == IrOpcode::kDoubleArrayMax;
Node* arguments_list = node->InputAt(0);
// Iterate the elements and find the result.
Node* empty_value = is_max ? __ Float64Constant(-V8_INFINITY)
: __ Float64Constant(V8_INFINITY);
Node* array_length = __ LoadField(
AccessBuilder::ForJSArrayLength(ElementsKind::PACKED_DOUBLE_ELEMENTS),
arguments_list);
array_length = ChangeSmiToIntPtr(array_length);
Node* elements =
__ LoadField(AccessBuilder::ForJSObjectElements(), arguments_list);
auto loop = __ MakeLoopLabel(MachineType::PointerRepresentation(),
MachineRepresentation::kFloat64);
auto done = __ MakeLabel(MachineRepresentation::kFloat64);
__ Goto(&loop, __ IntPtrConstant(0), empty_value);
__ Bind(&loop);
{
Node* index = loop.PhiAt(0);
Node* accumulator = loop.PhiAt(1);
Node* check = __ UintLessThan(index, array_length);
__ GotoIfNot(check, &done, accumulator);
Node* element = __ LoadElement(AccessBuilder::ForFixedDoubleArrayElement(),
elements, index);
__ Goto(&loop, __ IntAdd(index, __ IntPtrConstant(1)),
is_max ? __ Float64Max(accumulator, element)
: __ Float64Min(accumulator, element));
}
__ Bind(&done);
return ChangeFloat64ToTagged(done.PhiAt(0),
CheckForMinusZeroMode::kCheckForMinusZero);
}
Node* EffectControlLinearizer::LowerConvertReceiver(Node* node) {
ConvertReceiverMode const mode = ConvertReceiverModeOf(node->op());
Node* value = node->InputAt(0);

View File

@ -330,6 +330,18 @@ TNode<Boolean> JSGraphAssembler::NumberLessThanOrEqual(TNode<Number> lhs,
graph()->NewNode(simplified()->NumberLessThanOrEqual(), lhs, rhs));
}
TNode<Number> JSGraphAssembler::NumberShiftRightLogical(TNode<Number> lhs,
TNode<Number> rhs) {
return AddNode<Number>(
graph()->NewNode(simplified()->NumberShiftRightLogical(), lhs, rhs));
}
TNode<Number> JSGraphAssembler::NumberBitwiseAnd(TNode<Number> lhs,
TNode<Number> rhs) {
return AddNode<Number>(
graph()->NewNode(simplified()->NumberBitwiseAnd(), lhs, rhs));
}
TNode<String> JSGraphAssembler::StringSubstring(TNode<String> string,
TNode<Number> from,
TNode<Number> to) {
@ -342,6 +354,10 @@ TNode<Boolean> JSGraphAssembler::ObjectIsCallable(TNode<Object> value) {
graph()->NewNode(simplified()->ObjectIsCallable(), value));
}
TNode<Boolean> JSGraphAssembler::ObjectIsSmi(TNode<Object> value) {
return AddNode<Boolean>(graph()->NewNode(simplified()->ObjectIsSmi(), value));
}
TNode<Boolean> JSGraphAssembler::ObjectIsUndetectable(TNode<Object> value) {
return AddNode<Boolean>(
graph()->NewNode(simplified()->ObjectIsUndetectable(), value));
@ -379,6 +395,16 @@ TNode<FixedArrayBase> JSGraphAssembler::MaybeGrowFastElements(
index_needed, old_length, effect(), control()));
}
TNode<Object> JSGraphAssembler::DoubleArrayMax(TNode<JSArray> array) {
return AddNode<Object>(graph()->NewNode(simplified()->DoubleArrayMax(), array,
effect(), control()));
}
TNode<Object> JSGraphAssembler::DoubleArrayMin(TNode<JSArray> array) {
return AddNode<Object>(graph()->NewNode(simplified()->DoubleArrayMax(), array,
effect(), control()));
}
Node* JSGraphAssembler::StringCharCodeAt(TNode<String> string,
TNode<Number> position) {
return AddNode(graph()->NewNode(simplified()->StringCharCodeAt(), string,

View File

@ -70,6 +70,8 @@ class Reducer;
V(Float64InsertLowWord32) \
V(Float64LessThan) \
V(Float64LessThanOrEqual) \
V(Float64Max) \
V(Float64Min) \
V(Float64Mod) \
V(Float64Sub) \
V(Int32Add) \
@ -945,9 +947,12 @@ class V8_EXPORT_PRIVATE JSGraphAssembler : public GraphAssembler {
TNode<Boolean> NumberLessThanOrEqual(TNode<Number> lhs, TNode<Number> rhs);
TNode<Number> NumberAdd(TNode<Number> lhs, TNode<Number> rhs);
TNode<Number> NumberSubtract(TNode<Number> lhs, TNode<Number> rhs);
TNode<Number> NumberShiftRightLogical(TNode<Number> lhs, TNode<Number> rhs);
TNode<Number> NumberBitwiseAnd(TNode<Number> lhs, TNode<Number> rhs);
TNode<String> StringSubstring(TNode<String> string, TNode<Number> from,
TNode<Number> to);
TNode<Boolean> ObjectIsCallable(TNode<Object> value);
TNode<Boolean> ObjectIsSmi(TNode<Object> value);
TNode<Boolean> ObjectIsUndetectable(TNode<Object> value);
Node* CheckIf(Node* cond, DeoptimizeReason reason);
TNode<Boolean> NumberIsFloat64Hole(TNode<Number> value);
@ -960,6 +965,8 @@ class V8_EXPORT_PRIVATE JSGraphAssembler : public GraphAssembler {
TNode<Number> new_length,
TNode<Number> old_length);
Node* StringCharCodeAt(TNode<String> string, TNode<Number> position);
TNode<Object> DoubleArrayMax(TNode<JSArray> array);
TNode<Object> DoubleArrayMin(TNode<JSArray> array);
JSGraph* jsgraph() const { return jsgraph_; }
Isolate* isolate() const { return jsgraph()->isolate(); }

View File

@ -83,6 +83,7 @@ class JSCallReducerAssembler : public JSGraphAssembler {
TNode<Boolean> ReduceStringPrototypeStartsWith(
const StringRef& search_element_string);
TNode<String> ReduceStringPrototypeSlice();
TNode<Object> ReduceJSCallMathMinMaxWithArrayLike(Builtin builtin);
TNode<Object> TargetInput() const { return JSCallNode{node_ptr()}.target(); }
@ -294,6 +295,8 @@ class JSCallReducerAssembler : public JSGraphAssembler {
return NumberAdd(value, OneConstant());
}
TNode<Number> LoadMapElementsKind(TNode<Map> map);
void MaybeInsertMapChecks(MapInference* inference,
bool has_stability_dependency) {
// TODO(jgruber): Implement MapInference::InsertMapChecks in graph
@ -1157,6 +1160,15 @@ TNode<JSArray> JSCallReducerAssembler::AllocateEmptyJSArray(
return TNode<JSArray>::UncheckedCast(result);
}
TNode<Number> JSCallReducerAssembler::LoadMapElementsKind(TNode<Map> map) {
TNode<Number> bit_field2 =
LoadField<Number>(AccessBuilder::ForMapBitField2(), map);
return NumberShiftRightLogical(
NumberBitwiseAnd(bit_field2,
NumberConstant(Map::Bits2::ElementsKindBits::kMask)),
NumberConstant(Map::Bits2::ElementsKindBits::kShift));
}
TNode<Object> JSCallReducerAssembler::ReduceMathUnary(const Operator* op) {
TNode<Object> input = Argument(0);
TNode<Number> input_as_number = SpeculativeToNumber(input);
@ -1327,6 +1339,57 @@ TNode<String> JSCallReducerAssembler::ReduceStringPrototypeSlice() {
.Value();
}
TNode<Object> JSCallReducerAssembler::ReduceJSCallMathMinMaxWithArrayLike(
Builtin builtin) {
JSCallWithArrayLikeNode n(node_ptr());
TNode<Object> arguments_list = n.Argument(0);
auto call_builtin = MakeLabel();
auto done = MakeLabel(MachineRepresentation::kTagged);
// Check if {arguments_list} is a JSArray.
GotoIf(ObjectIsSmi(arguments_list), &call_builtin);
TNode<Map> arguments_list_map =
LoadField<Map>(AccessBuilder::ForMap(),
TNode<HeapObject>::UncheckedCast(arguments_list));
TNode<Number> arguments_list_instance_type = LoadField<Number>(
AccessBuilder::ForMapInstanceType(), arguments_list_map);
auto check_instance_type =
NumberEqual(arguments_list_instance_type, NumberConstant(JS_ARRAY_TYPE));
GotoIfNot(check_instance_type, &call_builtin);
// Check if {arguments_list} has PACKED_DOUBLE_ELEMENTS.
TNode<Number> arguments_list_elements_kind =
LoadMapElementsKind(arguments_list_map);
auto check_element_kind = NumberEqual(arguments_list_elements_kind,
NumberConstant(PACKED_DOUBLE_ELEMENTS));
GotoIfNot(check_element_kind, &call_builtin);
// If {arguments_list} is a JSArray with PACKED_DOUBLE_ELEMENTS, calculate the
// result with inlined loop.
TNode<JSArray> array_arguments_list =
TNode<JSArray>::UncheckedCast(arguments_list);
Goto(&done, builtin == Builtin::kMathMax
? DoubleArrayMax(array_arguments_list)
: DoubleArrayMin(array_arguments_list));
// Otherwise, call BuiltinMathMin/Max as usual.
Bind(&call_builtin);
TNode<Object> call = CopyNode();
CallParameters const& p = n.Parameters();
// Set SpeculationMode to kDisallowSpeculation to avoid infinite
// recursion.
NodeProperties::ChangeOp(
call, javascript()->CallWithArrayLike(
p.frequency(), p.feedback(),
SpeculationMode::kDisallowSpeculation, p.feedback_relation()));
Goto(&done, call);
Bind(&done);
return done.PhiAt<Object>(0);
}
TNode<Object> IteratingArrayBuiltinReducerAssembler::ReduceArrayPrototypeAt(
ZoneVector<ElementsKind> kinds, bool needs_fallback_builtin_call,
Node* receiver_kind) {
@ -5192,6 +5255,13 @@ Reduction JSCallReducer::ReduceJSCallWithArrayLike(Node* node) {
if (TargetIsClassConstructor(node, broker())) {
return NoChange();
}
base::Optional<Reduction> maybe_result =
TryReduceJSCallMathMinMaxWithArrayLike(node);
if (maybe_result.has_value()) {
return maybe_result.value();
}
return ReduceCallOrConstructWithArrayLikeOrSpread(
node, n.ArgumentCount(), n.LastArgumentIndex(), p.frequency(),
p.feedback(), p.speculation_mode(), p.feedback_relation(), n.target(),
@ -8410,6 +8480,108 @@ Reduction JSCallReducer::ReduceBigIntAsN(Node* node, Builtin builtin) {
return NoChange();
}
base::Optional<Reduction> JSCallReducer::TryReduceJSCallMathMinMaxWithArrayLike(
Node* node) {
if (!v8_flags.turbo_optimize_math_minmax) return base::nullopt;
JSCallWithArrayLikeNode n(node);
CallParameters const& p = n.Parameters();
Node* target = n.target();
Effect effect = n.effect();
Control control = n.control();
if (p.speculation_mode() == SpeculationMode::kDisallowSpeculation) {
return base::nullopt;
}
if (n.ArgumentCount() != 1) {
return base::nullopt;
}
// These ops are handled by ReduceCallOrConstructWithArrayLikeOrSpread.
Node* arguments_list = n.Argument(0);
if (arguments_list->opcode() == IrOpcode::kJSCreateLiteralArray ||
arguments_list->opcode() == IrOpcode::kJSCreateEmptyLiteralArray ||
arguments_list->opcode() == IrOpcode::kJSCreateArguments) {
return base::nullopt;
}
HeapObjectMatcher m(target);
if (m.HasResolvedValue()) {
ObjectRef target_ref = m.Ref(broker());
if (target_ref.IsJSFunction()) {
JSFunctionRef function = target_ref.AsJSFunction();
// Don't inline cross native context.
if (!function.native_context().equals(native_context())) {
return base::nullopt;
}
SharedFunctionInfoRef shared = function.shared();
Builtin builtin =
shared.HasBuiltinId() ? shared.builtin_id() : Builtin::kNoBuiltinId;
if (builtin == Builtin::kMathMax || builtin == Builtin::kMathMin) {
return ReduceJSCallMathMinMaxWithArrayLike(node, builtin);
} else {
return base::nullopt;
}
}
}
// Try specialize the JSCallWithArrayLike node with feedback target.
if (ShouldUseCallICFeedback(target) &&
p.feedback_relation() == CallFeedbackRelation::kTarget &&
p.feedback().IsValid()) {
ProcessedFeedback const& feedback =
broker()->GetFeedbackForCall(p.feedback());
if (feedback.IsInsufficient()) {
return base::nullopt;
}
base::Optional<HeapObjectRef> feedback_target = feedback.AsCall().target();
if (feedback_target.has_value() && feedback_target->map().is_callable()) {
Node* target_function = jsgraph()->Constant(*feedback_target);
ObjectRef target_ref = feedback_target.value();
if (!target_ref.IsJSFunction()) {
return base::nullopt;
}
JSFunctionRef function = target_ref.AsJSFunction();
SharedFunctionInfoRef shared = function.shared();
Builtin builtin =
shared.HasBuiltinId() ? shared.builtin_id() : Builtin::kNoBuiltinId;
if (builtin == Builtin::kMathMax || builtin == Builtin::kMathMin) {
// Check that the {target} is still the {target_function}.
Node* check = graph()->NewNode(simplified()->ReferenceEqual(), target,
target_function);
effect = graph()->NewNode(
simplified()->CheckIf(DeoptimizeReason::kWrongCallTarget), check,
effect, control);
// Specialize the JSCallWithArrayLike node to the {target_function}.
NodeProperties::ReplaceValueInput(node, target_function,
n.TargetIndex());
NodeProperties::ReplaceEffectInput(node, effect);
// Try to further reduce the Call MathMin/Max with double array.
return Changed(node).FollowedBy(
ReduceJSCallMathMinMaxWithArrayLike(node, builtin));
}
}
}
return base::nullopt;
}
Reduction JSCallReducer::ReduceJSCallMathMinMaxWithArrayLike(Node* node,
Builtin builtin) {
JSCallWithArrayLikeNode n(node);
DCHECK_NE(n.Parameters().speculation_mode(),
SpeculationMode::kDisallowSpeculation);
DCHECK_EQ(n.ArgumentCount(), 1);
JSCallReducerAssembler a(this, node);
Node* subgraph = a.ReduceJSCallMathMinMaxWithArrayLike(builtin);
return ReplaceWithSubgraph(&a, subgraph);
}
CompilationDependencies* JSCallReducer::dependencies() const {
return broker()->dependencies();
}

View File

@ -229,6 +229,9 @@ class V8_EXPORT_PRIVATE JSCallReducer final : public AdvancedReducer {
Reduction ReduceNumberConstructor(Node* node);
Reduction ReduceBigIntAsN(Node* node, Builtin builtin);
base::Optional<Reduction> TryReduceJSCallMathMinMaxWithArrayLike(Node* node);
Reduction ReduceJSCallMathMinMaxWithArrayLike(Node* node, Builtin builtin);
// The pendant to ReplaceWithValue when using GraphAssembler-based reductions.
Reduction ReplaceWithSubgraph(JSCallReducerAssembler* gasm, Node* subgraph);

View File

@ -430,6 +430,8 @@
V(ConvertReceiver) \
V(ConvertTaggedHoleToUndefined) \
V(DateNow) \
V(DoubleArrayMax) \
V(DoubleArrayMin) \
V(EnsureWritableFastElements) \
V(FastApiCall) \
V(FindOrderedHashMapEntry) \

View File

@ -4117,6 +4117,14 @@ class RepresentationSelector {
case IrOpcode::kDateNow:
VisitInputs<T>(node);
return SetOutput<T>(node, MachineRepresentation::kTagged);
case IrOpcode::kDoubleArrayMax: {
return VisitUnop<T>(node, UseInfo::AnyTagged(),
MachineRepresentation::kTagged);
}
case IrOpcode::kDoubleArrayMin: {
return VisitUnop<T>(node, UseInfo::AnyTagged(),
MachineRepresentation::kTagged);
}
case IrOpcode::kFrameState:
return VisitFrameState<T>(FrameState{node});
case IrOpcode::kStateValues:

View File

@ -812,7 +812,9 @@ bool operator==(CheckMinusZeroParameters const& lhs,
V(StringCodePointAt, Operator::kNoProperties, 2, 1) \
V(StringFromCodePointAt, Operator::kNoProperties, 2, 1) \
V(StringSubstring, Operator::kNoProperties, 3, 1) \
V(DateNow, Operator::kNoProperties, 0, 1)
V(DateNow, Operator::kNoProperties, 0, 1) \
V(DoubleArrayMax, Operator::kNoProperties, 1, 1) \
V(DoubleArrayMin, Operator::kNoProperties, 1, 1)
#define SPECULATIVE_NUMBER_BINOP_LIST(V) \
SIMPLIFIED_SPECULATIVE_NUMBER_BINOP_LIST(V) \

View File

@ -1087,6 +1087,11 @@ class V8_EXPORT_PRIVATE SimplifiedOperatorBuilder final
#endif
const Operator* DateNow();
// Math.min/max for JSArray with PACKED_DOUBLE_ELEMENTS.
const Operator* DoubleArrayMin();
const Operator* DoubleArrayMax();
// Unsigned32Divide is a special operator to express the division of two
// Unsigned32 inputs and truncating the result to Unsigned32. It's semantics
// is equivalent to NumberFloor(NumberDivide(x:Unsigned32, y:Unsigned32)) but

View File

@ -1528,6 +1528,10 @@ Type Typer::Visitor::TypeJSObjectIsArray(Node* node) { return Type::Boolean(); }
Type Typer::Visitor::TypeDateNow(Node* node) { return Type::Number(); }
Type Typer::Visitor::TypeDoubleArrayMin(Node* node) { return Type::Number(); }
Type Typer::Visitor::TypeDoubleArrayMax(Node* node) { return Type::Number(); }
Type Typer::Visitor::TypeUnsigned32Divide(Node* node) {
Type lhs = Operand(node, 0);
return Type::Range(0, lhs.Max(), zone());

View File

@ -1535,7 +1535,11 @@ void Verifier::Visitor::Check(Node* node, const AllNodes& all) {
case IrOpcode::kAssertType:
case IrOpcode::kVerifyType:
break;
case IrOpcode::kDoubleArrayMin:
case IrOpcode::kDoubleArrayMax:
CheckValueInputIs(node, 0, Type::Any());
CheckTypeIs(node, Type::Number());
break;
case IrOpcode::kCheckFloat64Hole:
CheckValueInputIs(node, 0, Type::NumberOrHole());
CheckTypeIs(node, Type::NumberOrUndefined());

View File

@ -943,6 +943,8 @@ DEFINE_BOOL(turbo_force_mid_tier_regalloc, false,
"always use the mid-tier register allocator (for testing)")
DEFINE_BOOL(turbo_optimize_apply, true, "optimize Function.prototype.apply")
DEFINE_BOOL(turbo_optimize_math_minmax, true,
"optimize call math.min/max with double array")
DEFINE_BOOL(turbo_collect_feedback_in_generic_lowering, true,
"enable experimental feedback collection in generic lowering.")