Skip to content

Commit 89ebaa7

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: * #59377
1 parent 8d953e1 commit 89ebaa7

File tree

4 files changed

+102
-59
lines changed

4 files changed

+102
-59
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/operators.jl

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,6 @@ isless(x::AbstractFloat, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x
223223
isless(x::Real, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y)
224224
isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y)
225225

226-
# Performance optimization to reduce branching
227-
# This is useful for sorting tuples of integers
228-
# TODO: remove this when the compiler can optimize the generic version better
229-
# See #48724 and #48753
230-
isless(a::Tuple{BitInteger, BitInteger}, b::Tuple{BitInteger, BitInteger}) =
231-
isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2]))
232-
233226
"""
234227
isgreater(x, y)
235228

base/tuple.jl

Lines changed: 82 additions & 50 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,95 @@ 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
628+
# copy of `BitInteger` defined later during bootstrap in int.jl
629+
const _BitInteger = Union{
630+
Int8, Int16, Int32, Int64, Int128,
631+
UInt8, UInt16, UInt32, UInt64, UInt128,
632+
}
620633

621634
"""
622635
isless(t1::Tuple, t2::Tuple)
623636
624637
Return `true` when `t1` is less than `t2` in lexicographic order.
625638
"""
626639
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)
640+
f(::Tuple{}, ::Tuple{}) = false
641+
f(::Tuple{}, ::Tuple) = true
642+
f(::Tuple, ::Tuple{}) = false
643+
function f(t1::Tuple, t2::Tuple)
644+
a, b = t1[1], t2[1]
645+
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
646+
end
647+
function f(t1::Any32, t2::Any32)
648+
n1, n2 = length(t1), length(t2)
649+
for i = 1:min(n1, n2)
650+
a, b = t1[i], t2[i]
651+
if !isequal(a, b)
652+
return isless(a, b)
653+
end
636654
end
655+
return n1 < n2
656+
end
657+
# Performance optimization to reduce branching
658+
# This is useful for sorting tuples of integers
659+
# TODO: remove this when the compiler can optimize the generic version better
660+
# See #48724 and #48753
661+
function isless(a::Tuple{_BitInteger, _BitInteger}, b::Tuple{_BitInteger, _BitInteger})
662+
isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2]))
637663
end
638-
return n1 < n2
664+
f(t1, t2)
639665
end
640666

641667
## functions ##
642668

643-
isempty(x::Tuple{}) = true
644-
isempty(@nospecialize x::Tuple) = false
669+
function isempty(x::Tuple)
670+
f(x::Tuple{}) = true
671+
f(@nospecialize x::Tuple) = false
672+
f(x)
673+
end
645674

646675
revargs() = ()
647676
revargs(x, r...) = (revargs(r...)..., x)
@@ -679,11 +708,14 @@ empty(@nospecialize x::Tuple) = ()
679708
foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing)
680709
foreach(f, itr::Tuple, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itr, itrs...), init=nothing)
681710

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
711+
function circshift(t::Tuple, shift::Integer)
712+
f((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
713+
f(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
714+
function f(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
715+
@inline
716+
len = N + 3
717+
j = mod1(shift, len)
718+
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
719+
end
720+
f(t, shift)
689721
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)