Skip to content

Commit d494973

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 3396562 commit d494973

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
@@ -531,8 +531,14 @@ julia> Base.tail(())
531531
ERROR: ArgumentError: Cannot call tail on an empty tuple.
532532
```
533533
"""
534-
tail(x::Tuple) = argtail(x...)
535-
tail(::Tuple{}) = throw(ArgumentError("Cannot call tail on an empty tuple."))
534+
function tail(x::Tuple)
535+
f(x::Tuple) = argtail(x...)
536+
function f(::Tuple{})
537+
@noinline
538+
throw(ArgumentError("Cannot call tail on an empty tuple."))
539+
end
540+
f(x)
541+
end
536542

537543
function unwrap_unionall(@nospecialize(a))
538544
@_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

@@ -570,71 +575,95 @@ function _eq(t1::Any32, t2::Any32)
570575
end
571576

572577
const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90
573-
hash(::Tuple{}, h::UInt) = h tuplehash_seed
574-
hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
575-
function hash(t::Any32, h::UInt)
576-
out = h tuplehash_seed
577-
for i = length(t):-1:1
578-
out = hash(t[i], out)
578+
function hash(t::Tuple, h::UInt)
579+
f(::Tuple{}, h::UInt) = h tuplehash_seed
580+
f(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
581+
function f(t::Any32, h::UInt)
582+
out = h tuplehash_seed
583+
for i = length(t):-1:1
584+
out = hash(t[i], out)
585+
end
586+
return out
579587
end
580-
return out
588+
f(t, h)
581589
end
582590

583-
<(::Tuple{}, ::Tuple{}) = false
584-
<(::Tuple{}, ::Tuple) = true
585-
<(::Tuple, ::Tuple{}) = false
586591
function <(t1::Tuple, t2::Tuple)
587-
a, b = t1[1], t2[1]
588-
eq = (a == b)
589-
if ismissing(eq)
590-
return missing
591-
elseif !eq
592-
return a < b
593-
end
594-
return tail(t1) < tail(t2)
595-
end
596-
function <(t1::Any32, t2::Any32)
597-
n1, n2 = length(t1), length(t2)
598-
for i = 1:min(n1, n2)
599-
a, b = t1[i], t2[i]
592+
f(::Tuple{}, ::Tuple{}) = false
593+
f(::Tuple{}, ::Tuple) = true
594+
f(::Tuple, ::Tuple{}) = false
595+
function f(t1::Tuple, t2::Tuple)
596+
a, b = t1[1], t2[1]
600597
eq = (a == b)
601598
if ismissing(eq)
602599
return missing
603600
elseif !eq
604-
return a < b
601+
return a < b
602+
end
603+
return tail(t1) < tail(t2)
604+
end
605+
function f(t1::Any32, t2::Any32)
606+
n1, n2 = length(t1), length(t2)
607+
for i = 1:min(n1, n2)
608+
a, b = t1[i], t2[i]
609+
eq = (a == b)
610+
if ismissing(eq)
611+
return missing
612+
elseif !eq
613+
return a < b
614+
end
605615
end
616+
return n1 < n2
606617
end
607-
return n1 < n2
618+
f(t1, t2)
608619
end
609620

610-
isless(::Tuple{}, ::Tuple{}) = false
611-
isless(::Tuple{}, ::Tuple) = true
612-
isless(::Tuple, ::Tuple{}) = false
621+
# copy of `BitInteger` defined later during bootstrap in int.jl
622+
const _BitInteger = Union{
623+
Int8, Int16, Int32, Int64, Int128,
624+
UInt8, UInt16, UInt32, UInt64, UInt128,
625+
}
613626

614627
"""
615628
isless(t1::Tuple, t2::Tuple)
616629
617630
Return `true` when `t1` is less than `t2` in lexicographic order.
618631
"""
619632
function isless(t1::Tuple, t2::Tuple)
620-
a, b = t1[1], t2[1]
621-
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
622-
end
623-
function isless(t1::Any32, t2::Any32)
624-
n1, n2 = length(t1), length(t2)
625-
for i = 1:min(n1, n2)
626-
a, b = t1[i], t2[i]
627-
if !isequal(a, b)
628-
return isless(a, b)
633+
f(::Tuple{}, ::Tuple{}) = false
634+
f(::Tuple{}, ::Tuple) = true
635+
f(::Tuple, ::Tuple{}) = false
636+
function f(t1::Tuple, t2::Tuple)
637+
a, b = t1[1], t2[1]
638+
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
639+
end
640+
function f(t1::Any32, t2::Any32)
641+
n1, n2 = length(t1), length(t2)
642+
for i = 1:min(n1, n2)
643+
a, b = t1[i], t2[i]
644+
if !isequal(a, b)
645+
return isless(a, b)
646+
end
629647
end
648+
return n1 < n2
649+
end
650+
# Performance optimization to reduce branching
651+
# This is useful for sorting tuples of integers
652+
# TODO: remove this when the compiler can optimize the generic version better
653+
# See #48724 and #48753
654+
function f(a::Tuple{_BitInteger, _BitInteger}, b::Tuple{_BitInteger, _BitInteger})
655+
isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2]))
630656
end
631-
return n1 < n2
657+
f(t1, t2)
632658
end
633659

634660
## functions ##
635661

636-
isempty(x::Tuple{}) = true
637-
isempty(@nospecialize x::Tuple) = false
662+
function isempty(x::Tuple)
663+
f(x::Tuple{}) = true
664+
f(@nospecialize x::Tuple) = false
665+
f(x)
666+
end
638667

639668
revargs() = ()
640669
revargs(x, r...) = (revargs(r...)..., x)
@@ -672,11 +701,14 @@ empty(@nospecialize x::Tuple) = ()
672701
foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing)
673702
foreach(f, itr::Tuple, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itr, itrs...), init=nothing)
674703

675-
circshift((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
676-
circshift(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
677-
function circshift(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
678-
@inline
679-
len = N + 3
680-
j = mod1(shift, len)
681-
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
704+
function circshift(t::Tuple, shift::Integer)
705+
f((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
706+
f(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
707+
function f(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
708+
@inline
709+
len = N + 3
710+
j = mod1(shift, len)
711+
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
712+
end
713+
f(t, shift)
682714
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)