Skip to content

Commit 53f0434

Browse files
committed
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: * JuliaLang#59377
1 parent 100ef9d commit 53f0434

File tree

3 files changed

+90
-53
lines changed

3 files changed

+90
-53
lines changed

base/essentials.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,14 @@ julia> Base.tail(())
533533
ERROR: ArgumentError: Cannot call tail on an empty tuple.
534534
```
535535
"""
536-
tail(x::Tuple) = argtail(x...)
537-
tail(::Tuple{}) = throw(ArgumentError("Cannot call tail on an empty tuple."))
536+
function tail(x::Tuple)
537+
f(x::Tuple) = argtail(x...)
538+
function f(::Tuple{})
539+
@noinline
540+
throw(ArgumentError("Cannot call tail on an empty tuple."))
541+
end
542+
f(x)
543+
end
538544

539545
function unwrap_unionall(@nospecialize(a))
540546
@_foldable_meta

base/tuple.jl

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,14 @@ end
263263

264264
@eval split_rest(t::Tuple, n::Int, i=1) = ($(Expr(:meta, :aggressive_constprop)); (t[i:end-n], t[end-n+1:end]))
265265

266-
# Use dispatch to avoid a branch in first
267-
first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty"))
268-
first(t::Tuple) = t[1]
266+
function first(t::Tuple)
267+
f(t::Tuple) = t[1]
268+
function f(::Tuple{})
269+
@noinline
270+
throw(ArgumentError("tuple must be non-empty"))
271+
end
272+
f(t)
273+
end
269274

270275
# eltype
271276

@@ -577,71 +582,82 @@ function _eq(t1::Any32, t2::Any32)
577582
end
578583

579584
const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90
580-
hash(::Tuple{}, h::UInt) = h tuplehash_seed
581-
hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
582-
function hash(t::Any32, h::UInt)
583-
out = h tuplehash_seed
584-
for i = length(t):-1:1
585-
out = hash(t[i], out)
585+
function hash(t::Tuple, h::UInt)
586+
f(::Tuple{}, h::UInt) = h tuplehash_seed
587+
f(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
588+
function f(t::Any32, h::UInt)
589+
out = h tuplehash_seed
590+
for i = length(t):-1:1
591+
out = hash(t[i], out)
592+
end
593+
return out
586594
end
587-
return out
595+
f(t, h)
588596
end
589597

590-
<(::Tuple{}, ::Tuple{}) = false
591-
<(::Tuple{}, ::Tuple) = true
592-
<(::Tuple, ::Tuple{}) = false
593598
function <(t1::Tuple, t2::Tuple)
594-
a, b = t1[1], t2[1]
595-
eq = (a == b)
596-
if ismissing(eq)
597-
return missing
598-
elseif !eq
599-
return a < b
600-
end
601-
return tail(t1) < tail(t2)
602-
end
603-
function <(t1::Any32, t2::Any32)
604-
n1, n2 = length(t1), length(t2)
605-
for i = 1:min(n1, n2)
606-
a, b = t1[i], t2[i]
599+
f(::Tuple{}, ::Tuple{}) = false
600+
f(::Tuple{}, ::Tuple) = true
601+
f(::Tuple, ::Tuple{}) = false
602+
function f(t1::Tuple, t2::Tuple)
603+
a, b = t1[1], t2[1]
607604
eq = (a == b)
608605
if ismissing(eq)
609606
return missing
610607
elseif !eq
611-
return a < b
608+
return a < b
609+
end
610+
return tail(t1) < tail(t2)
611+
end
612+
function f(t1::Any32, t2::Any32)
613+
n1, n2 = length(t1), length(t2)
614+
for i = 1:min(n1, n2)
615+
a, b = t1[i], t2[i]
616+
eq = (a == b)
617+
if ismissing(eq)
618+
return missing
619+
elseif !eq
620+
return a < b
621+
end
612622
end
623+
return n1 < n2
613624
end
614-
return n1 < n2
625+
f(t1, t2)
615626
end
616627

617-
isless(::Tuple{}, ::Tuple{}) = false
618-
isless(::Tuple{}, ::Tuple) = true
619-
isless(::Tuple, ::Tuple{}) = false
620-
621628
"""
622629
isless(t1::Tuple, t2::Tuple)
623630
624631
Return `true` when `t1` is less than `t2` in lexicographic order.
625632
"""
626633
function isless(t1::Tuple, t2::Tuple)
627-
a, b = t1[1], t2[1]
628-
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
629-
end
630-
function isless(t1::Any32, t2::Any32)
631-
n1, n2 = length(t1), length(t2)
632-
for i = 1:min(n1, n2)
633-
a, b = t1[i], t2[i]
634-
if !isequal(a, b)
635-
return isless(a, b)
634+
f(::Tuple{}, ::Tuple{}) = false
635+
f(::Tuple{}, ::Tuple) = true
636+
f(::Tuple, ::Tuple{}) = false
637+
function f(t1::Tuple, t2::Tuple)
638+
a, b = t1[1], t2[1]
639+
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
640+
end
641+
function f(t1::Any32, t2::Any32)
642+
n1, n2 = length(t1), length(t2)
643+
for i = 1:min(n1, n2)
644+
a, b = t1[i], t2[i]
645+
if !isequal(a, b)
646+
return isless(a, b)
647+
end
636648
end
649+
return n1 < n2
637650
end
638-
return n1 < n2
651+
f(t1, t2)
639652
end
640653

641654
## functions ##
642655

643-
isempty(x::Tuple{}) = true
644-
isempty(@nospecialize x::Tuple) = false
656+
function isempty(x::Tuple)
657+
f(x::Tuple{}) = true
658+
f(@nospecialize x::Tuple) = false
659+
f(x)
660+
end
645661

646662
revargs() = ()
647663
revargs(x, r...) = (revargs(r...)..., x)
@@ -679,11 +695,14 @@ empty(@nospecialize x::Tuple) = ()
679695
foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing)
680696
foreach(f, itr::Tuple, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itr, itrs...), init=nothing)
681697

682-
circshift((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
683-
circshift(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
684-
function circshift(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
685-
@inline
686-
len = N + 3
687-
j = mod1(shift, len)
688-
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
698+
function circshift(t::Tuple, shift::Integer)
699+
f((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
700+
f(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
701+
function f(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
702+
@inline
703+
len = N + 3
704+
j = mod1(shift, len)
705+
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
706+
end
707+
f(t, shift)
689708
end

test/tuple.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,18 @@ namedtup = (;a=1, b=2, c=3)
824824
@test Val{Tuple{Int64, Vararg{Int32,N}} where N} === Val{Tuple{Int64, Vararg{Int32}}}
825825
@test Val{Tuple{Int32, Vararg{Int64}}} === Val{Tuple{Int32, Vararg{Int64,N}} where N}
826826

827+
@testset "avoid method proliferation" begin
828+
t = isone length methods
829+
@test t(circshift, Tuple{Tuple, Integer})
830+
@test t(hash, Tuple{Tuple, UInt})
831+
for f in (Base.tail, first, isempty)
832+
@test t(f, Tuple{Tuple})
833+
end
834+
for f in (<, isless, ==, isequal)
835+
@test t(f, Tuple{Tuple, Tuple})
836+
end
837+
end
838+
827839
@testset "from Pair, issue #52636" begin
828840
pair = (1 => "2")
829841
@test (1, "2") == @inferred Tuple(pair)

0 commit comments

Comments
 (0)