From 32f682476990ad726e0f4d64d157ee60163a4afe Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 28 Aug 2025 14:45:03 +0200 Subject: [PATCH] avoid method proliferation for `Tuple` functions * Introducing new types and methods for a callable can invalidate already compiled method instances of a function for which world-splitting is enabled (`max_methods`). * Invalidation of sysimage or package precompiled code worsens latency due to requiring recompilation. * Lowering the `max_methods` setting for a function often causes inference issues for existing code that is not completely type-stable (which is a lot of code). In many cases this is easy to fix by avoiding method proliferation, such as by merging some methods and introducing branching into the merged method. This PR aims to fix the latter issue for some `Tuple`-related methods of some functions where decreasing `max_methods` might be interesting. Seeing as branching was deliberately avoided in the bodies of many of these methods, I opted for the approach of introducing local functions which preserve the dispatch logic as before, without branching. Thus there should be no regressions, except perhaps because of changed inlining costs. This PR is a prerequisite for PRs which try to decrease `max_methods` for select functions, such as PR: * #59377 --- base/essentials.jl | 10 +++- base/operators.jl | 7 --- base/tuple.jl | 132 ++++++++++++++++++++++++++++----------------- test/tuple.jl | 12 +++++ 4 files changed, 102 insertions(+), 59 deletions(-) diff --git a/base/essentials.jl b/base/essentials.jl index e37ce55dac4e6..078e4e1809bd3 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -533,8 +533,14 @@ julia> Base.tail(()) ERROR: ArgumentError: Cannot call tail on an empty tuple. ``` """ -tail(x::Tuple) = argtail(x...) -tail(::Tuple{}) = throw(ArgumentError("Cannot call tail on an empty tuple.")) +function tail(x::Tuple) + f(x::Tuple) = argtail(x...) + function f(::Tuple{}) + @noinline + throw(ArgumentError("Cannot call tail on an empty tuple.")) + end + f(x) +end function unwrap_unionall(@nospecialize(a)) @_foldable_meta diff --git a/base/operators.jl b/base/operators.jl index 51729b852070d..627bfb3e9def6 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -223,13 +223,6 @@ isless(x::AbstractFloat, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x isless(x::Real, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) -# Performance optimization to reduce branching -# This is useful for sorting tuples of integers -# TODO: remove this when the compiler can optimize the generic version better -# See #48724 and #48753 -isless(a::Tuple{BitInteger, BitInteger}, b::Tuple{BitInteger, BitInteger}) = - isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2])) - """ isgreater(x, y) diff --git a/base/tuple.jl b/base/tuple.jl index ac42c667269e6..de811f2dd00fa 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -263,9 +263,14 @@ end @eval split_rest(t::Tuple, n::Int, i=1) = ($(Expr(:meta, :aggressive_constprop)); (t[i:end-n], t[end-n+1:end])) -# Use dispatch to avoid a branch in first -first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty")) -first(t::Tuple) = t[1] +function first(t::Tuple) + f(t::Tuple) = t[1] + function f(::Tuple{}) + @noinline + throw(ArgumentError("tuple must be non-empty")) + end + f(t) +end # eltype @@ -577,46 +582,54 @@ function _eq(t1::Any32, t2::Any32) end const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90 -hash(::Tuple{}, h::UInt) = h ⊻ tuplehash_seed -hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h)) -function hash(t::Any32, h::UInt) - out = h ⊻ tuplehash_seed - for i = length(t):-1:1 - out = hash(t[i], out) +function hash(t::Tuple, h::UInt) + f(::Tuple{}, h::UInt) = h ⊻ tuplehash_seed + f(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h)) + function f(t::Any32, h::UInt) + out = h ⊻ tuplehash_seed + for i = length(t):-1:1 + out = hash(t[i], out) + end + return out end - return out + f(t, h) end -<(::Tuple{}, ::Tuple{}) = false -<(::Tuple{}, ::Tuple) = true -<(::Tuple, ::Tuple{}) = false function <(t1::Tuple, t2::Tuple) - a, b = t1[1], t2[1] - eq = (a == b) - if ismissing(eq) - return missing - elseif !eq - return a < b - end - return tail(t1) < tail(t2) -end -function <(t1::Any32, t2::Any32) - n1, n2 = length(t1), length(t2) - for i = 1:min(n1, n2) - a, b = t1[i], t2[i] + f(::Tuple{}, ::Tuple{}) = false + f(::Tuple{}, ::Tuple) = true + f(::Tuple, ::Tuple{}) = false + function f(t1::Tuple, t2::Tuple) + a, b = t1[1], t2[1] eq = (a == b) if ismissing(eq) return missing elseif !eq - return a < b + return a < b + end + return tail(t1) < tail(t2) + end + function f(t1::Any32, t2::Any32) + n1, n2 = length(t1), length(t2) + for i = 1:min(n1, n2) + a, b = t1[i], t2[i] + eq = (a == b) + if ismissing(eq) + return missing + elseif !eq + return a < b + end end + return n1 < n2 end - return n1 < n2 + f(t1, t2) end -isless(::Tuple{}, ::Tuple{}) = false -isless(::Tuple{}, ::Tuple) = true -isless(::Tuple, ::Tuple{}) = false +# copy of `BitInteger` defined later during bootstrap in int.jl +const _BitInteger = Union{ + Int8, Int16, Int32, Int64, Int128, + UInt8, UInt16, UInt32, UInt64, UInt128, +} """ isless(t1::Tuple, t2::Tuple) @@ -624,24 +637,40 @@ isless(::Tuple, ::Tuple{}) = false Return `true` when `t1` is less than `t2` in lexicographic order. """ function isless(t1::Tuple, t2::Tuple) - a, b = t1[1], t2[1] - isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2))) -end -function isless(t1::Any32, t2::Any32) - n1, n2 = length(t1), length(t2) - for i = 1:min(n1, n2) - a, b = t1[i], t2[i] - if !isequal(a, b) - return isless(a, b) + f(::Tuple{}, ::Tuple{}) = false + f(::Tuple{}, ::Tuple) = true + f(::Tuple, ::Tuple{}) = false + function f(t1::Tuple, t2::Tuple) + a, b = t1[1], t2[1] + isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2))) + end + function f(t1::Any32, t2::Any32) + n1, n2 = length(t1), length(t2) + for i = 1:min(n1, n2) + a, b = t1[i], t2[i] + if !isequal(a, b) + return isless(a, b) + end end + return n1 < n2 + end + # Performance optimization to reduce branching + # This is useful for sorting tuples of integers + # TODO: remove this when the compiler can optimize the generic version better + # See #48724 and #48753 + function f(a::Tuple{_BitInteger, _BitInteger}, b::Tuple{_BitInteger, _BitInteger}) + isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2])) end - return n1 < n2 + f(t1, t2) end ## functions ## -isempty(x::Tuple{}) = true -isempty(@nospecialize x::Tuple) = false +function isempty(x::Tuple) + f(x::Tuple{}) = true + f(@nospecialize x::Tuple) = false + f(x) +end revargs() = () revargs(x, r...) = (revargs(r...)..., x) @@ -679,11 +708,14 @@ empty(@nospecialize x::Tuple) = () foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing) foreach(f, itr::Tuple, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itr, itrs...), init=nothing) -circshift((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t -circshift(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t) -function circshift(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N} - @inline - len = N + 3 - j = mod1(shift, len) - ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple +function circshift(t::Tuple, shift::Integer) + f((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t + f(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t) + function f(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N} + @inline + len = N + 3 + j = mod1(shift, len) + ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple + end + f(t, shift) end diff --git a/test/tuple.jl b/test/tuple.jl index 30782367803c5..c15f33be2d5f7 100644 --- a/test/tuple.jl +++ b/test/tuple.jl @@ -824,6 +824,18 @@ namedtup = (;a=1, b=2, c=3) @test Val{Tuple{Int64, Vararg{Int32,N}} where N} === Val{Tuple{Int64, Vararg{Int32}}} @test Val{Tuple{Int32, Vararg{Int64}}} === Val{Tuple{Int32, Vararg{Int64,N}} where N} +@testset "avoid method proliferation" begin + t = isone ∘ length ∘ methods + @test t(circshift, Tuple{Tuple, Integer}) + @test t(hash, Tuple{Tuple, UInt}) + for f in (Base.tail, first, isempty) + @test t(f, Tuple{Tuple}) + end + for f in (<, isless, ==, isequal) + @test t(f, Tuple{Tuple, Tuple}) + end +end + @testset "from Pair, issue #52636" begin pair = (1 => "2") @test (1, "2") == @inferred Tuple(pair)