diff --git a/src/compiler/effect-control-linearizer.cc b/src/compiler/effect-control-linearizer.cc index eec55d6869..0734afd020 100644 --- a/src/compiler/effect-control-linearizer.cc +++ b/src/compiler/effect-control-linearizer.cc @@ -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 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); diff --git a/src/compiler/graph-assembler.cc b/src/compiler/graph-assembler.cc index 8d032235b7..7fea1fbbb9 100644 --- a/src/compiler/graph-assembler.cc +++ b/src/compiler/graph-assembler.cc @@ -330,6 +330,18 @@ TNode JSGraphAssembler::NumberLessThanOrEqual(TNode lhs, graph()->NewNode(simplified()->NumberLessThanOrEqual(), lhs, rhs)); } +TNode JSGraphAssembler::NumberShiftRightLogical(TNode lhs, + TNode rhs) { + return AddNode( + graph()->NewNode(simplified()->NumberShiftRightLogical(), lhs, rhs)); +} + +TNode JSGraphAssembler::NumberBitwiseAnd(TNode lhs, + TNode rhs) { + return AddNode( + graph()->NewNode(simplified()->NumberBitwiseAnd(), lhs, rhs)); +} + TNode JSGraphAssembler::StringSubstring(TNode string, TNode from, TNode to) { @@ -342,6 +354,10 @@ TNode JSGraphAssembler::ObjectIsCallable(TNode value) { graph()->NewNode(simplified()->ObjectIsCallable(), value)); } +TNode JSGraphAssembler::ObjectIsSmi(TNode value) { + return AddNode(graph()->NewNode(simplified()->ObjectIsSmi(), value)); +} + TNode JSGraphAssembler::ObjectIsUndetectable(TNode value) { return AddNode( graph()->NewNode(simplified()->ObjectIsUndetectable(), value)); @@ -379,6 +395,16 @@ TNode JSGraphAssembler::MaybeGrowFastElements( index_needed, old_length, effect(), control())); } +TNode JSGraphAssembler::DoubleArrayMax(TNode array) { + return AddNode(graph()->NewNode(simplified()->DoubleArrayMax(), array, + effect(), control())); +} + +TNode JSGraphAssembler::DoubleArrayMin(TNode array) { + return AddNode(graph()->NewNode(simplified()->DoubleArrayMax(), array, + effect(), control())); +} + Node* JSGraphAssembler::StringCharCodeAt(TNode string, TNode position) { return AddNode(graph()->NewNode(simplified()->StringCharCodeAt(), string, diff --git a/src/compiler/graph-assembler.h b/src/compiler/graph-assembler.h index 1ccfe8cb4a..7ba7e1eec4 100644 --- a/src/compiler/graph-assembler.h +++ b/src/compiler/graph-assembler.h @@ -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 NumberLessThanOrEqual(TNode lhs, TNode rhs); TNode NumberAdd(TNode lhs, TNode rhs); TNode NumberSubtract(TNode lhs, TNode rhs); + TNode NumberShiftRightLogical(TNode lhs, TNode rhs); + TNode NumberBitwiseAnd(TNode lhs, TNode rhs); TNode StringSubstring(TNode string, TNode from, TNode to); TNode ObjectIsCallable(TNode value); + TNode ObjectIsSmi(TNode value); TNode ObjectIsUndetectable(TNode value); Node* CheckIf(Node* cond, DeoptimizeReason reason); TNode NumberIsFloat64Hole(TNode value); @@ -960,6 +965,8 @@ class V8_EXPORT_PRIVATE JSGraphAssembler : public GraphAssembler { TNode new_length, TNode old_length); Node* StringCharCodeAt(TNode string, TNode position); + TNode DoubleArrayMax(TNode array); + TNode DoubleArrayMin(TNode array); JSGraph* jsgraph() const { return jsgraph_; } Isolate* isolate() const { return jsgraph()->isolate(); } diff --git a/src/compiler/js-call-reducer.cc b/src/compiler/js-call-reducer.cc index 2a915516ce..f7cd80aa93 100644 --- a/src/compiler/js-call-reducer.cc +++ b/src/compiler/js-call-reducer.cc @@ -83,6 +83,7 @@ class JSCallReducerAssembler : public JSGraphAssembler { TNode ReduceStringPrototypeStartsWith( const StringRef& search_element_string); TNode ReduceStringPrototypeSlice(); + TNode ReduceJSCallMathMinMaxWithArrayLike(Builtin builtin); TNode TargetInput() const { return JSCallNode{node_ptr()}.target(); } @@ -294,6 +295,8 @@ class JSCallReducerAssembler : public JSGraphAssembler { return NumberAdd(value, OneConstant()); } + TNode LoadMapElementsKind(TNode map); + void MaybeInsertMapChecks(MapInference* inference, bool has_stability_dependency) { // TODO(jgruber): Implement MapInference::InsertMapChecks in graph @@ -1157,6 +1160,15 @@ TNode JSCallReducerAssembler::AllocateEmptyJSArray( return TNode::UncheckedCast(result); } +TNode JSCallReducerAssembler::LoadMapElementsKind(TNode map) { + TNode bit_field2 = + LoadField(AccessBuilder::ForMapBitField2(), map); + return NumberShiftRightLogical( + NumberBitwiseAnd(bit_field2, + NumberConstant(Map::Bits2::ElementsKindBits::kMask)), + NumberConstant(Map::Bits2::ElementsKindBits::kShift)); +} + TNode JSCallReducerAssembler::ReduceMathUnary(const Operator* op) { TNode input = Argument(0); TNode input_as_number = SpeculativeToNumber(input); @@ -1327,6 +1339,57 @@ TNode JSCallReducerAssembler::ReduceStringPrototypeSlice() { .Value(); } +TNode JSCallReducerAssembler::ReduceJSCallMathMinMaxWithArrayLike( + Builtin builtin) { + JSCallWithArrayLikeNode n(node_ptr()); + TNode 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 arguments_list_map = + LoadField(AccessBuilder::ForMap(), + TNode::UncheckedCast(arguments_list)); + TNode arguments_list_instance_type = LoadField( + 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 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 array_arguments_list = + TNode::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 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(0); +} + TNode IteratingArrayBuiltinReducerAssembler::ReduceArrayPrototypeAt( ZoneVector 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 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 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 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(); } diff --git a/src/compiler/js-call-reducer.h b/src/compiler/js-call-reducer.h index dbb4fb2d0b..7cd87071b9 100644 --- a/src/compiler/js-call-reducer.h +++ b/src/compiler/js-call-reducer.h @@ -229,6 +229,9 @@ class V8_EXPORT_PRIVATE JSCallReducer final : public AdvancedReducer { Reduction ReduceNumberConstructor(Node* node); Reduction ReduceBigIntAsN(Node* node, Builtin builtin); + base::Optional TryReduceJSCallMathMinMaxWithArrayLike(Node* node); + Reduction ReduceJSCallMathMinMaxWithArrayLike(Node* node, Builtin builtin); + // The pendant to ReplaceWithValue when using GraphAssembler-based reductions. Reduction ReplaceWithSubgraph(JSCallReducerAssembler* gasm, Node* subgraph); diff --git a/src/compiler/opcodes.h b/src/compiler/opcodes.h index 9d23d5e96a..f73bd43c52 100644 --- a/src/compiler/opcodes.h +++ b/src/compiler/opcodes.h @@ -430,6 +430,8 @@ V(ConvertReceiver) \ V(ConvertTaggedHoleToUndefined) \ V(DateNow) \ + V(DoubleArrayMax) \ + V(DoubleArrayMin) \ V(EnsureWritableFastElements) \ V(FastApiCall) \ V(FindOrderedHashMapEntry) \ diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.cc index 8ee13f494e..413ec94ca7 100644 --- a/src/compiler/simplified-lowering.cc +++ b/src/compiler/simplified-lowering.cc @@ -4117,6 +4117,14 @@ class RepresentationSelector { case IrOpcode::kDateNow: VisitInputs(node); return SetOutput(node, MachineRepresentation::kTagged); + case IrOpcode::kDoubleArrayMax: { + return VisitUnop(node, UseInfo::AnyTagged(), + MachineRepresentation::kTagged); + } + case IrOpcode::kDoubleArrayMin: { + return VisitUnop(node, UseInfo::AnyTagged(), + MachineRepresentation::kTagged); + } case IrOpcode::kFrameState: return VisitFrameState(FrameState{node}); case IrOpcode::kStateValues: diff --git a/src/compiler/simplified-operator.cc b/src/compiler/simplified-operator.cc index 5b74d328fc..1eeed36af9 100644 --- a/src/compiler/simplified-operator.cc +++ b/src/compiler/simplified-operator.cc @@ -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) \ diff --git a/src/compiler/simplified-operator.h b/src/compiler/simplified-operator.h index 107d736cc6..7c608c73c9 100644 --- a/src/compiler/simplified-operator.h +++ b/src/compiler/simplified-operator.h @@ -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 diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc index 3ff6a41cb0..4b2afec0cc 100644 --- a/src/compiler/typer.cc +++ b/src/compiler/typer.cc @@ -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()); diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc index cd8ee2ada2..14f3c1bcab 100644 --- a/src/compiler/verifier.cc +++ b/src/compiler/verifier.cc @@ -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()); diff --git a/src/flags/flag-definitions.h b/src/flags/flag-definitions.h index a75fae11ad..555cf2813a 100644 --- a/src/flags/flag-definitions.h +++ b/src/flags/flag-definitions.h @@ -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.")