diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 8ad72285fcaeb3..7932f61e8de8aa 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -415,6 +415,43 @@ function lift_leaves(compact::IncrementalCompact, return nothing else typ = argextype(leaf, compact) + + # Here we implement a bit hacky special case to optimize a function call with + # type-unstable but `@nospecialize`-d keyword arguments, whose IR at this point + # would look like: + # ``` + # %1 = tuple(a, b, c)::Tuple{Any, Any, Any} + # %2 = NamedTuple{(:a, :b, :c)(%1)::NamedTuple{(:a, :b, :c), _A} where _A<:Tuple{Any, Any, Any} + # %3 = Core.getfield(%2, :a)::Any + # %4 = Core.getfield(%2, :b)::Any + # %5 = Core.getfield(%2, :c)::Any + # [... other body of the keyword func ...] + # ``` + # First, we check if this definition (%2) is partly well-known + # `NamedTuple` construction, where its names are fully known, and also + # if its call argument (%1) is fully-known `tuple` call. + # In a case when the length of the `NamedTuple` names and the length of + # the arguments for the `tuple` call, we can safely replace those `getfield` + # calls with the corresponding `tuple` call argument, while letting the + # later DCE pass to delete the constructions of tuple and named-tuple + typ′ = unwrap_unionall(widenconst(typ)) + if isa(typ′, DataType) && typ′.name === _NAMEDTUPLE_NAME && length(typ′.parameters) == 2 + ns = typ′.parameters[1] + if is_known_call(def, NamedTuple{ns}, compact) && length(def.args) == 2 + arg = def.args[2] + if isa(arg, AnySSAValue) + argexpr = compact[arg][:inst] + if is_known_call(argexpr, tuple, compact) && length(ns) == length(argexpr.args)-1 + # ok, we know this NamedTuple construction is nothrow, + # let's mark this NamedTuple as DCE-eligible + compact[leaf::AnySSAValue][:flag] |= IR_FLAG_EFFECT_FREE + lift_arg!(compact, argexpr.args[field+1], cache_key, argexpr, field+1, lifted_leaves) + continue + end + end + end + end + if !isa(typ, Const) # TODO: (disabled since #27126) # If the leaf is an old ssa value, insert a getfield here @@ -469,7 +506,7 @@ function lift_arg!( end end lifted_leaves[cache_key] = LiftedValue(lifted) - nothing + return nothing end function walk_to_def(compact::IncrementalCompact, @nospecialize(leaf)) diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 9c206270976b8d..e6750b8c9000c7 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1594,6 +1594,11 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end if istuple return Type{<:appl} + elseif isa(appl, DataType) && appl.name === _NAMEDTUPLE_NAME && appl.parameters[1] === () + # if the first parameter of `NamedTuple` is known to be empty tuple, + # the second argument should also be empty tuple type, + # so refine it here + return Const(NamedTuple{(),Tuple{}}) end ans = Type{appl} for i = length(outervars):-1:1 diff --git a/base/namedtuple.jl b/base/namedtuple.jl index 3e9f1272d588e4..b53304de7d8cc4 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -335,7 +335,7 @@ reverse(nt::NamedTuple) = NamedTuple{reverse(keys(nt))}(reverse(values(nt))) end """ - structdiff(a::NamedTuple{an}, b::Union{NamedTuple{bn},Type{NamedTuple{bn}}}) where {an,bn} + structdiff(a::NamedTuple, b::Union{NamedTuple,Type{NamedTuple}}) Construct a copy of named tuple `a`, except with fields that exist in `b` removed. `b` can be a named tuple, or a type of the form `NamedTuple{field_names}`. @@ -343,14 +343,19 @@ Construct a copy of named tuple `a`, except with fields that exist in `b` remove function structdiff(a::NamedTuple{an}, b::Union{NamedTuple{bn}, Type{NamedTuple{bn}}}) where {an, bn} if @generated names = diff_names(an, bn) + isempty(names) && return (;) # just a fast pass idx = Int[ fieldindex(a, names[n]) for n in 1:length(names) ] types = Tuple{Any[ fieldtype(a, idx[n]) for n in 1:length(idx) ]...} vals = Any[ :(getfield(a, $(idx[n]))) for n in 1:length(idx) ] - :( NamedTuple{$names,$types}(($(vals...),)) ) + return :( NamedTuple{$names,$types}(($(vals...),)) ) else names = diff_names(an, bn) + # N.B this early return is necessary to get a better type stability, + # and also allows us to cut off the cost from constructing + # potentially type unstable closure passed to the `map` below + isempty(names) && return (;) types = Tuple{Any[ fieldtype(typeof(a), names[n]) for n in 1:length(names) ]...} - NamedTuple{names,types}(map(Fix1(getfield, a), names)) + return NamedTuple{names,types}(map(n::Symbol->getfield(a, n), names)) end end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index dcd57916589cf4..d6f26ab2f899aa 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -2336,6 +2336,12 @@ end # Equivalence of Const(T.instance) and T for singleton types @test Const(nothing) ⊑ Nothing && Nothing ⊑ Const(nothing) +# `apply_type_tfunc` should always return accurate result for empty NamedTuple case +import Core: Const +import Core.Compiler: apply_type_tfunc +@test apply_type_tfunc(Const(NamedTuple), Const(()), Type{T} where T<:Tuple{}) === Const(typeof((;))) +@test apply_type_tfunc(Const(NamedTuple), Const(()), Type{T} where T<:Tuple) === Const(typeof((;))) + # Don't pessimize apply_type to anything worse than Type and yield Bottom for invalid Unions @test Core.Compiler.return_type(Core.apply_type, Tuple{Type{Union}}) == Type{Union{}} @test Core.Compiler.return_type(Core.apply_type, Tuple{Type{Union},Any}) == Type