-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
speed up first
and only
for various types
#52296
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename the versions of only
that take a Val
to _only
. They should not be part of the public only API. This is an implementation detail.
It is unclear to me if removing the methods for Number
, Char
, Tuple
, etc is actually necessary at this stage. Could you justify that further?
I suggest moving the Iterator
module methods into iterators.jl. I do not think there is precedent for those existing outside of that file.
base/dict.jl
Outdated
@@ -735,6 +735,8 @@ end | |||
end | |||
@propagate_inbounds iterate(t::Dict, i) = _iterate(t, skip_deleted(t, i)) | |||
|
|||
@propagate_inbounds Iterators.only(t::Dict) = only(t, Val(:first)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this to iterators.jl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dict
is defined in dict.jl, which is read after iterators.jl. So it can't be moved there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to change the order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, at least not without major changes to Base.jl. There are 24 include
statements between iterators.jl on the one hand and dict.jl & set.jl on the other. In between is for example hashing.jl, where hash
is defined. Shall we leave the definitions for Dict
and Set
(whichever form they take for your objective 1) where they are?
base/iterators.jl
Outdated
@propagate_inbounds function only(x) | ||
@propagate_inbounds only(x) = only(x, Val(:iterate)) | ||
|
||
@propagate_inbounds function only(x, ::Val{:iterate}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename these versions of only to _only. They should not be part of the public only
API. This is an implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm asking myself whether these versions should be (semi)public (under the current or a different name). Currently the method is chosen on a case-by-case basis. Package developers and other users may want to do the same.
This also reflects that currently there is no general principle to choose the method. It's tempting to use haslength
, but that doesn't always do the right thing:
typeof(y) = String
Base.haslength(typeof(y)) = true
2.652 μs (0 allocations: 0 bytes) # runtest with only(y, Val(:iterate))
3.185 μs (0 allocations: 0 bytes) # runtest with only(y, Val(:first))
typeof(y) = Vector{Char}
Base.haslength(typeof(y)) = true
913.744 ns (0 allocations: 0 bytes)
961.190 ns (0 allocations: 0 bytes)
typeof(y) = BitSet
Base.haslength(typeof(y)) = true
4.046 μs (0 allocations: 0 bytes)
5.834 μs (0 allocations: 0 bytes)
Maybe a function hasfastlength
in analogy with hasfastin
would help. On the other hand, only
is probably not important enough to justify major additions.
Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new method should also be used for WeakKeyDict
and maybe also for PersistentDict
and Pairs
. For ImmutableDict
there is a mysterious allocation that slows it down:
typeof(y) = WeakKeyDict{Vector{Any}, Int64}
Base.haslength(typeof(y)) = true
425.247 μs (9000 allocations: 203.12 KiB) # runtest with only(y, Val(:iterate))
253.297 μs (6000 allocations: 156.25 KiB) # runtest with only(y, Val(:first))
typeof(y) = Base.PersistentDict{Char, Int64}
Base.haslength(typeof(y)) = true
1.455 ms (18000 allocations: 562.50 KiB)
1.203 ms (16000 allocations: 500.00 KiB)
typeof(y) = Base.ImmutableDict{Char, Int64}
Base.haslength(typeof(y)) = true
1.594 μs (0 allocations: 0 bytes)
2.715 μs (1 allocation: 32 bytes)
typeof(y) = Base.Pairs{Symbol, Int64, Tuple{Symbol}, @NamedTuple{x::Int64}}
Base.haslength(typeof(y)) = true
9.510 μs (0 allocations: 0 bytes)
9.261 μs (0 allocations: 0 bytes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As new public API, there are a few issues to work out here and this would require significantly more technical review since we would be committing to this specific interface for quite some time.
Additionally, rather than using Val
the typical pattern here for the API would be to create trait types such as Base.HasLength
.
Maybe something like as follows:
abstract type OnlyApproach end
struct OnlyIterate <: OnlyApproach end
struct OnlyFirst <: OnlyApproach end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than using Val
what if we used the functions themselves as the second argument.
function only(x, ::typeof(iterate))
# ...
end
function only(x, ::typeof(first)
# ...
end
One can then invoke the functions as only(x, iterate)
or only(x, first)
.
base/iterators.jl
Outdated
only(x::NamedTuple) = throw( | ||
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") | ||
) | ||
@inline function only(x, ::Val{:first}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename these versions of only to _only. They should not be part of the public only
API. This is an implementation detail.
base/set.jl
Outdated
@@ -141,6 +141,8 @@ rehash!(s::Set) = (rehash!(s.dict); s) | |||
|
|||
iterate(s::Set, i...) = iterate(KeySet(s.dict), i...) | |||
|
|||
@propagate_inbounds Iterators.only(s::Set) = only(s, Val(:first)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move to iterators.jl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, Set
is defined in set.jl, which comes after iterators.jl.
base/iterators.jl
Outdated
@@ -1559,18 +1561,13 @@ Stacktrace: | |||
return ret | |||
end | |||
|
|||
# Collections of known size | |||
only(x::Ref) = x[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is unclear to me if we need to remove all of these. Is there an issue with leaving these here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed them to keep the code simple. As far as I can tell, these methods don't add anything because the default method (which is not new) already produces optimal code in these cases. Example:
julia> @code_llvm only(1) # this PR
; Function Signature: only(Int64)
; @ iterators.jl:1550 within `only`
define i64 @julia_only_2628(i64 signext %"x::Int64") #0 {
top:
ret i64 %"x::Int64"
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I recognize that you are trying to simplify the code, performing this simplification while you are trying to advance the other technical points makes the diff complicated. I think it might be good to focus on the additions first, and then make a second pull request for the simplification. This also would aide future debugging if there is an issue.
While I think you are technically correct, there is too much going on here at once. At the moment, you are suggesting three distinguishable changes.
I suggest focusing this initial pull reqest on objective 1, which might be straightforward enough to advance quickly. If I understand correctly, this part of the pull request should just make existing API faster without a change in behavior. Therefore, it could advance as a pure performance pull request. The The simplification is fine but I think is fully dividable from this pull request since that should be effectively a no-op. |
Adding triage label exclusively for the additions to the public API. |
An easily merged subset of @matthias314's #52296, separated from that PR at @mkitti's suggestion. I kept the low-but-nonzero value specializations for Tuple and Named tuple which offer better error messages. They may be removed with a future PR that improves the generic error message. To verify this is indeed a no-op ``` @code_llvm only(Ref(4)) @code_llvm only(Ref(nothing)) @code_llvm only('z') @code_llvm only((4,)) @code_llvm only((nothing,)) only((4,2)) @code_llvm only(Array{Int, 0}(undef)) @code_llvm only((;x=3)) only((;)) only((;x=2, y=4)) ``` Before ``` julia> @code_llvm only(Ref(4)) ; Function Signature: only(Base.RefValue{Int64}) ; @ iterators.jl:1563 within `only` define i64 @julia_only_2451({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { top: ; ┌ @ refvalue.jl:59 within `getindex` ; │┌ @ Base.jl:49 within `getproperty` %0 = bitcast {}* %"x::RefValue" to i64* %.x = load i64, i64* %0, align 8 ; └└ ret i64 %.x } julia> @code_llvm only(Ref(nothing)) ; Function Signature: only(Base.RefValue{Nothing}) ; @ iterators.jl:1563 within `only` define void @julia_only_2519({}* noundef nonnull %"x::RefValue") #0 { top: ret void } julia> @code_llvm only('z') ; Function Signature: only(Char) ; @ iterators.jl:1565 within `only` define i32 @julia_only_2527(i32 zeroext %"x::Char") #0 { top: ret i32 %"x::Char" } julia> @code_llvm only((4,)) ; Function Signature: only(Tuple{Int64}) ; @ iterators.jl:1566 within `only` define i64 @julia_only_2529([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { top: ; ┌ @ tuple.jl:31 within `getindex` %"x::Tuple[1]_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::Tuple", i64 0, i64 0 ; └ %"x::Tuple[1]_ptr.unbox" = load i64, i64* %"x::Tuple[1]_ptr", align 8 ret i64 %"x::Tuple[1]_ptr.unbox" } julia> @code_llvm only((nothing,)) ; Function Signature: only(Tuple{Nothing}) ; @ iterators.jl:1566 within `only` define void @julia_only_2532() #0 { top: ret void } julia> only((4,2)) ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::Tuple{Int64, Int64}) @ Base.Iterators ./iterators.jl:1567 [2] top-level scope @ REPL[6]:1 julia> @code_llvm only(Array{Int, 0}(undef)) ; Function Signature: only(Array{Int64, 0}) ; @ iterators.jl:1570 within `only` define i64 @julia_only_2779({}* noundef nonnull align 8 dereferenceable(16) %"a::Array") #0 { top: ; ┌ @ abstractarray.jl:1314 within `getindex` ; │┌ @ abstractarray.jl:1343 within `_getindex` ; ││┌ @ essentials.jl:817 within `getindex` %0 = bitcast {}* %"a::Array" to i64** %1 = load i64*, i64** %0, align 8 %2 = load i64, i64* %1, align 8 ; └└└ ret i64 %2 } julia> @code_llvm only((;x=3)) ; Function Signature: only(NamedTuple{(:x,), Tuple{Int64}}) ; @ iterators.jl:1571 within `only` define i64 @julia_only_2794([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { top: ; ┌ @ abstractarray.jl:469 within `first` ; │┌ @ namedtuple.jl:165 within `iterate` @ namedtuple.jl:165 %"x::NamedTuple.x_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::NamedTuple", i64 0, i64 0 ; └└ %"x::NamedTuple.x_ptr.unbox" = load i64, i64* %"x::NamedTuple.x_ptr", align 8 ret i64 %"x::NamedTuple.x_ptr.unbox" } julia> only((;)) ERROR: ArgumentError: NamedTuple contains 0 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{}) @ Base.Iterators ./iterators.jl:1572 [2] top-level scope @ REPL[9]:1 julia> only((;x=2, y=4)) ERROR: ArgumentError: NamedTuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{x::Int64, y::Int64}) @ Base.Iterators ./iterators.jl:1572 [2] top-level scope @ REPL[10]:1 ``` After ``` julia> @code_llvm only(Ref(4)) ; Function Signature: only(Base.RefValue{Int64}) ; @ iterators.jl:1550 within `only` define i64 @julia_only_6821({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { top: ; @ iterators.jl:1551 within `only` ; ┌ @ refpointer.jl:103 within `iterate` ; │┌ @ refvalue.jl:59 within `getindex` ; ││┌ @ Base.jl:49 within `getproperty` %0 = bitcast {}* %"x::RefValue" to i64* %.x = load i64, i64* %0, align 8 ; └└└ ; @ iterators.jl:1559 within `only` ret i64 %.x } julia> @code_llvm only(Ref(nothing)) ; Function Signature: only(Base.RefValue{Nothing}) ; @ iterators.jl:1550 within `only` define void @julia_only_6824({}* noundef nonnull %"x::RefValue") #0 { top: ; @ iterators.jl:1559 within `only` ret void } julia> @code_llvm only('z') ; Function Signature: only(Char) ; @ iterators.jl:1550 within `only` define i32 @julia_only_6826(i32 zeroext %"x::Char") #0 { top: ; @ iterators.jl:1559 within `only` ret i32 %"x::Char" } julia> @code_llvm only((4,)) ; Function Signature: only(Tuple{Int64}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` define i64 @julia_only_6833([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { top: ; ┌ @ tuple.jl:31 within `getindex` %"x::Tuple[1]_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::Tuple", i64 0, i64 0 ; └ %"x::Tuple[1]_ptr.unbox" = load i64, i64* %"x::Tuple[1]_ptr", align 8 ret i64 %"x::Tuple[1]_ptr.unbox" } julia> @code_llvm only((nothing,)) ; Function Signature: only(Tuple{Nothing}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` define void @julia_only_6836() #0 { top: ret void } julia> only((4,2)) ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::Tuple{Int64, Int64}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1564 [2] top-level scope @ REPL[31]:1 julia> @code_llvm only(Array{Int, 0}(undef)) ; Function Signature: only(Array{Int64, 0}) ; @ iterators.jl:1550 within `only` define i64 @julia_only_6848({}* noundef nonnull align 8 dereferenceable(16) %"x::Array") #0 { L60: ; @ iterators.jl:1551 within `only` ; ┌ @ array.jl:884 within `iterate` @ array.jl:884 ; │┌ @ essentials.jl:817 within `getindex` %0 = bitcast {}* %"x::Array" to i64** %1 = load i64*, i64** %0, align 8 %2 = load i64, i64* %1, align 8 ; └└ ; @ iterators.jl:1559 within `only` ret i64 %2 } julia> @code_llvm only((;x=3)) ; Function Signature: only(NamedTuple{(:x,), Tuple{Int64}}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1571 within `only` define i64 @julia_only_6871([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { top: ; ┌ @ abstractarray.jl:469 within `first` ; │┌ @ namedtuple.jl:165 within `iterate` @ namedtuple.jl:165 %"x::NamedTuple.x_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::NamedTuple", i64 0, i64 0 ; └└ %"x::NamedTuple.x_ptr.unbox" = load i64, i64* %"x::NamedTuple.x_ptr", align 8 ret i64 %"x::NamedTuple.x_ptr.unbox" } julia> only((;)) ERROR: ArgumentError: NamedTuple contains 0 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 [2] top-level scope @ REPL[34]:1 julia> only((;x=2, y=4)) ERROR: ArgumentError: NamedTuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{x::Int64, y::Int64}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 [2] top-level scope @ REPL[35]:1 ``` Diff ```diff < # Before --- > # After 4,5c4,5 < ; @ iterators.jl:1563 within `only` < define i64 @julia_only_2451({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { --- > ; @ iterators.jl:1550 within `only` > define i64 @julia_only_6821({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { 7,11c7,14 < ; ┌ @ refvalue.jl:59 within `getindex` < ; │┌ @ Base.jl:49 within `getproperty` < %0 = bitcast {}* %"x::RefValue" to i64* < %.x = load i64, i64* %0, align 8 < ; └└ --- > ; @ iterators.jl:1551 within `only` > ; ┌ @ refpointer.jl:103 within `iterate` > ; │┌ @ refvalue.jl:59 within `getindex` > ; ││┌ @ Base.jl:49 within `getproperty` > %0 = bitcast {}* %"x::RefValue" to i64* > %.x = load i64, i64* %0, align 8 > ; └└└ > ; @ iterators.jl:1559 within `only` 17,18c20,21 < ; @ iterators.jl:1563 within `only` < define void @julia_only_2519({}* noundef nonnull %"x::RefValue") #0 { --- > ; @ iterators.jl:1550 within `only` > define void @julia_only_6824({}* noundef nonnull %"x::RefValue") #0 { 19a23 > ; @ iterators.jl:1559 within `only` 25,26c29,30 < ; @ iterators.jl:1565 within `only` < define i32 @julia_only_2527(i32 zeroext %"x::Char") #0 { --- > ; @ iterators.jl:1550 within `only` > define i32 @julia_only_6826(i32 zeroext %"x::Char") #0 { 27a32 > ; @ iterators.jl:1559 within `only` 33,34c38,39 < ; @ iterators.jl:1566 within `only` < define i64 @julia_only_2529([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` > define i64 @julia_only_6833([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { 45,46c50,51 < ; @ iterators.jl:1566 within `only` < define void @julia_only_2532() #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` > define void @julia_only_6836() #0 { 55c60 < @ Base.Iterators ./iterators.jl:1567 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1564 57c62 < @ REPL[6]:1 --- > @ REPL[31]:1 61,70c66,76 < ; @ iterators.jl:1570 within `only` < define i64 @julia_only_2779({}* noundef nonnull align 8 dereferenceable(16) %"a::Array") #0 { < top: < ; ┌ @ abstractarray.jl:1314 within `getindex` < ; │┌ @ abstractarray.jl:1343 within `_getindex` < ; ││┌ @ essentials.jl:817 within `getindex` < %0 = bitcast {}* %"a::Array" to i64** < %1 = load i64*, i64** %0, align 8 < %2 = load i64, i64* %1, align 8 < ; └└└ --- > ; @ iterators.jl:1550 within `only` > define i64 @julia_only_6848({}* noundef nonnull align 8 dereferenceable(16) %"x::Array") #0 { > L60: > ; @ iterators.jl:1551 within `only` > ; ┌ @ array.jl:884 within `iterate` @ array.jl:884 > ; │┌ @ essentials.jl:817 within `getindex` > %0 = bitcast {}* %"x::Array" to i64** > %1 = load i64*, i64** %0, align 8 > %2 = load i64, i64* %1, align 8 > ; └└ > ; @ iterators.jl:1559 within `only` 76,77c82,83 < ; @ iterators.jl:1571 within `only` < define i64 @julia_only_2794([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1571 within `only` > define i64 @julia_only_6871([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { 91c97 < @ Base.Iterators ./iterators.jl:1572 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 93c99 < @ REPL[9]:1 --- > @ REPL[34]:1 99c105 < @ Base.Iterators ./iterators.jl:1572 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 101c107 < @ REPL[10]:1 --- > @ REPL[35]:1 ``` --------- Co-authored-by: matthias314 <[email protected]> Co-authored-by: Shuhei Kadowaki <[email protected]>
421a477
to
33f340d
Compare
I have made the following changes:
The new methods for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing triage label as this PR is now an uncontroversial improvement.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I concur. Can we get a @nanosoldier run?
The string @nanosoldier |
Your benchmark job has completed - successfully executed benchmarks. A full report can be found here. |
@maleadt, I thought the |
Correct, but only for |
@nanosoldier |
@nanosoldier |
@nanosoldier runbenchmarks("array" || "collection", vs=":master") |
@nanosoldier lol |
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. |
|
That benchmark runs in O(1) time with a low constant factor so I'm guessing it's benchmarking noise. julia> function perf_sumcartesian(A)
s = zero(eltype(A))
for I in CartesianIndices(size(A))
val = Base.unsafe_getindex(A, I)
s += val
end
return s
end
perf_sumcartesian (generic function with 1 method)
julia> using Chairmarks
julia> @b 1:100000 perf_sumcartesian
0.466 ns |
Thank you for your contribution, @matthias314! |
An easily merged subset of @matthias314's JuliaLang#52296, separated from that PR at @mkitti's suggestion. I kept the low-but-nonzero value specializations for Tuple and Named tuple which offer better error messages. They may be removed with a future PR that improves the generic error message. To verify this is indeed a no-op ``` @code_llvm only(Ref(4)) @code_llvm only(Ref(nothing)) @code_llvm only('z') @code_llvm only((4,)) @code_llvm only((nothing,)) only((4,2)) @code_llvm only(Array{Int, 0}(undef)) @code_llvm only((;x=3)) only((;)) only((;x=2, y=4)) ``` Before ``` julia> @code_llvm only(Ref(4)) ; Function Signature: only(Base.RefValue{Int64}) ; @ iterators.jl:1563 within `only` define i64 @julia_only_2451({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { top: ; ┌ @ refvalue.jl:59 within `getindex` ; │┌ @ Base.jl:49 within `getproperty` %0 = bitcast {}* %"x::RefValue" to i64* %.x = load i64, i64* %0, align 8 ; └└ ret i64 %.x } julia> @code_llvm only(Ref(nothing)) ; Function Signature: only(Base.RefValue{Nothing}) ; @ iterators.jl:1563 within `only` define void @julia_only_2519({}* noundef nonnull %"x::RefValue") #0 { top: ret void } julia> @code_llvm only('z') ; Function Signature: only(Char) ; @ iterators.jl:1565 within `only` define i32 @julia_only_2527(i32 zeroext %"x::Char") #0 { top: ret i32 %"x::Char" } julia> @code_llvm only((4,)) ; Function Signature: only(Tuple{Int64}) ; @ iterators.jl:1566 within `only` define i64 @julia_only_2529([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { top: ; ┌ @ tuple.jl:31 within `getindex` %"x::Tuple[1]_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::Tuple", i64 0, i64 0 ; └ %"x::Tuple[1]_ptr.unbox" = load i64, i64* %"x::Tuple[1]_ptr", align 8 ret i64 %"x::Tuple[1]_ptr.unbox" } julia> @code_llvm only((nothing,)) ; Function Signature: only(Tuple{Nothing}) ; @ iterators.jl:1566 within `only` define void @julia_only_2532() #0 { top: ret void } julia> only((4,2)) ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::Tuple{Int64, Int64}) @ Base.Iterators ./iterators.jl:1567 [2] top-level scope @ REPL[6]:1 julia> @code_llvm only(Array{Int, 0}(undef)) ; Function Signature: only(Array{Int64, 0}) ; @ iterators.jl:1570 within `only` define i64 @julia_only_2779({}* noundef nonnull align 8 dereferenceable(16) %"a::Array") #0 { top: ; ┌ @ abstractarray.jl:1314 within `getindex` ; │┌ @ abstractarray.jl:1343 within `_getindex` ; ││┌ @ essentials.jl:817 within `getindex` %0 = bitcast {}* %"a::Array" to i64** %1 = load i64*, i64** %0, align 8 %2 = load i64, i64* %1, align 8 ; └└└ ret i64 %2 } julia> @code_llvm only((;x=3)) ; Function Signature: only(NamedTuple{(:x,), Tuple{Int64}}) ; @ iterators.jl:1571 within `only` define i64 @julia_only_2794([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { top: ; ┌ @ abstractarray.jl:469 within `first` ; │┌ @ namedtuple.jl:165 within `iterate` @ namedtuple.jl:165 %"x::NamedTuple.x_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::NamedTuple", i64 0, i64 0 ; └└ %"x::NamedTuple.x_ptr.unbox" = load i64, i64* %"x::NamedTuple.x_ptr", align 8 ret i64 %"x::NamedTuple.x_ptr.unbox" } julia> only((;)) ERROR: ArgumentError: NamedTuple contains 0 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{}) @ Base.Iterators ./iterators.jl:1572 [2] top-level scope @ REPL[9]:1 julia> only((;x=2, y=4)) ERROR: ArgumentError: NamedTuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{x::Int64, y::Int64}) @ Base.Iterators ./iterators.jl:1572 [2] top-level scope @ REPL[10]:1 ``` After ``` julia> @code_llvm only(Ref(4)) ; Function Signature: only(Base.RefValue{Int64}) ; @ iterators.jl:1550 within `only` define i64 @julia_only_6821({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { top: ; @ iterators.jl:1551 within `only` ; ┌ @ refpointer.jl:103 within `iterate` ; │┌ @ refvalue.jl:59 within `getindex` ; ││┌ @ Base.jl:49 within `getproperty` %0 = bitcast {}* %"x::RefValue" to i64* %.x = load i64, i64* %0, align 8 ; └└└ ; @ iterators.jl:1559 within `only` ret i64 %.x } julia> @code_llvm only(Ref(nothing)) ; Function Signature: only(Base.RefValue{Nothing}) ; @ iterators.jl:1550 within `only` define void @julia_only_6824({}* noundef nonnull %"x::RefValue") #0 { top: ; @ iterators.jl:1559 within `only` ret void } julia> @code_llvm only('z') ; Function Signature: only(Char) ; @ iterators.jl:1550 within `only` define i32 @julia_only_6826(i32 zeroext %"x::Char") #0 { top: ; @ iterators.jl:1559 within `only` ret i32 %"x::Char" } julia> @code_llvm only((4,)) ; Function Signature: only(Tuple{Int64}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` define i64 @julia_only_6833([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { top: ; ┌ @ tuple.jl:31 within `getindex` %"x::Tuple[1]_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::Tuple", i64 0, i64 0 ; └ %"x::Tuple[1]_ptr.unbox" = load i64, i64* %"x::Tuple[1]_ptr", align 8 ret i64 %"x::Tuple[1]_ptr.unbox" } julia> @code_llvm only((nothing,)) ; Function Signature: only(Tuple{Nothing}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` define void @julia_only_6836() #0 { top: ret void } julia> only((4,2)) ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::Tuple{Int64, Int64}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1564 [2] top-level scope @ REPL[31]:1 julia> @code_llvm only(Array{Int, 0}(undef)) ; Function Signature: only(Array{Int64, 0}) ; @ iterators.jl:1550 within `only` define i64 @julia_only_6848({}* noundef nonnull align 8 dereferenceable(16) %"x::Array") #0 { L60: ; @ iterators.jl:1551 within `only` ; ┌ @ array.jl:884 within `iterate` @ array.jl:884 ; │┌ @ essentials.jl:817 within `getindex` %0 = bitcast {}* %"x::Array" to i64** %1 = load i64*, i64** %0, align 8 %2 = load i64, i64* %1, align 8 ; └└ ; @ iterators.jl:1559 within `only` ret i64 %2 } julia> @code_llvm only((;x=3)) ; Function Signature: only(NamedTuple{(:x,), Tuple{Int64}}) ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1571 within `only` define i64 @julia_only_6871([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { top: ; ┌ @ abstractarray.jl:469 within `first` ; │┌ @ namedtuple.jl:165 within `iterate` @ namedtuple.jl:165 %"x::NamedTuple.x_ptr" = getelementptr inbounds [1 x i64], [1 x i64]* %"x::NamedTuple", i64 0, i64 0 ; └└ %"x::NamedTuple.x_ptr.unbox" = load i64, i64* %"x::NamedTuple.x_ptr", align 8 ret i64 %"x::NamedTuple.x_ptr.unbox" } julia> only((;)) ERROR: ArgumentError: NamedTuple contains 0 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 [2] top-level scope @ REPL[34]:1 julia> only((;x=2, y=4)) ERROR: ArgumentError: NamedTuple contains 2 elements, must contain exactly 1 element Stacktrace: [1] only(x::@NamedTuple{x::Int64, y::Int64}) @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 [2] top-level scope @ REPL[35]:1 ``` Diff ```diff < # Before --- > # After 4,5c4,5 < ; @ iterators.jl:1563 within `only` < define i64 @julia_only_2451({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { --- > ; @ iterators.jl:1550 within `only` > define i64 @julia_only_6821({}* noundef nonnull align 8 dereferenceable(8) %"x::RefValue") #0 { 7,11c7,14 < ; ┌ @ refvalue.jl:59 within `getindex` < ; │┌ @ Base.jl:49 within `getproperty` < %0 = bitcast {}* %"x::RefValue" to i64* < %.x = load i64, i64* %0, align 8 < ; └└ --- > ; @ iterators.jl:1551 within `only` > ; ┌ @ refpointer.jl:103 within `iterate` > ; │┌ @ refvalue.jl:59 within `getindex` > ; ││┌ @ Base.jl:49 within `getproperty` > %0 = bitcast {}* %"x::RefValue" to i64* > %.x = load i64, i64* %0, align 8 > ; └└└ > ; @ iterators.jl:1559 within `only` 17,18c20,21 < ; @ iterators.jl:1563 within `only` < define void @julia_only_2519({}* noundef nonnull %"x::RefValue") #0 { --- > ; @ iterators.jl:1550 within `only` > define void @julia_only_6824({}* noundef nonnull %"x::RefValue") #0 { 19a23 > ; @ iterators.jl:1559 within `only` 25,26c29,30 < ; @ iterators.jl:1565 within `only` < define i32 @julia_only_2527(i32 zeroext %"x::Char") #0 { --- > ; @ iterators.jl:1550 within `only` > define i32 @julia_only_6826(i32 zeroext %"x::Char") #0 { 27a32 > ; @ iterators.jl:1559 within `only` 33,34c38,39 < ; @ iterators.jl:1566 within `only` < define i64 @julia_only_2529([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` > define i64 @julia_only_6833([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::Tuple") #0 { 45,46c50,51 < ; @ iterators.jl:1566 within `only` < define void @julia_only_2532() #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1566 within `only` > define void @julia_only_6836() #0 { 55c60 < @ Base.Iterators ./iterators.jl:1567 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1564 57c62 < @ REPL[6]:1 --- > @ REPL[31]:1 61,70c66,76 < ; @ iterators.jl:1570 within `only` < define i64 @julia_only_2779({}* noundef nonnull align 8 dereferenceable(16) %"a::Array") #0 { < top: < ; ┌ @ abstractarray.jl:1314 within `getindex` < ; │┌ @ abstractarray.jl:1343 within `_getindex` < ; ││┌ @ essentials.jl:817 within `getindex` < %0 = bitcast {}* %"a::Array" to i64** < %1 = load i64*, i64** %0, align 8 < %2 = load i64, i64* %1, align 8 < ; └└└ --- > ; @ iterators.jl:1550 within `only` > define i64 @julia_only_6848({}* noundef nonnull align 8 dereferenceable(16) %"x::Array") #0 { > L60: > ; @ iterators.jl:1551 within `only` > ; ┌ @ array.jl:884 within `iterate` @ array.jl:884 > ; │┌ @ essentials.jl:817 within `getindex` > %0 = bitcast {}* %"x::Array" to i64** > %1 = load i64*, i64** %0, align 8 > %2 = load i64, i64* %1, align 8 > ; └└ > ; @ iterators.jl:1559 within `only` 76,77c82,83 < ; @ iterators.jl:1571 within `only` < define i64 @julia_only_2794([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { --- > ; @ /Users/x/.julia/dev/julia/base/iterators.jl:1571 within `only` > define i64 @julia_only_6871([1 x i64]* nocapture noundef nonnull readonly align 8 dereferenceable(8) %"x::NamedTuple") #0 { 91c97 < @ Base.Iterators ./iterators.jl:1572 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 93c99 < @ REPL[9]:1 --- > @ REPL[34]:1 99c105 < @ Base.Iterators ./iterators.jl:1572 --- > @ Base.Iterators ~/.julia/dev/julia/base/iterators.jl:1568 101c107 < @ REPL[10]:1 --- > @ REPL[35]:1 ``` --------- Co-authored-by: matthias314 <[email protected]> Co-authored-by: Shuhei Kadowaki <[email protected]>
This PR is the result of this Discourse thread. It speeds up
first
forArray
andString
andonly
forDict
,IdDict
andSet
. Foronly
the speed up comes from usinglength
andfirst
instead of callingiterate
twice (once to get the first element, once to check that there are no more). I have also deleted dedicatedonly
methods forTuple
,Int
and the like because they are not faster than the standard method.Here are some benchmarks. They were generated by the function
applied to a
Vector
v
withn = 1000
randomly generated containers of the given type having one element each. Below I only show the relevant output.first
@inbounds
master@inbounds
newString
Vector{Char}
Matrix{Char}
Array{Char, 3}
only
@inbounds
master@inbounds
newDict{Char, Int64}
IdDict{Char, Int64}
Set{Char}
Tuple{Int64}
Int64
Char
There are a couple of types where the new method
only(x, Val(:first))
would be faster the current one (now accessible asonly(x, Val(:iterate))
, but only when using@inbounds
. However, if one adds@inbounds
, then one can also switch tofirst
instead ofonly
to get a larger speed-up.Note that
only
forIdDict
is defined in iterators.jl since it is included later than iddict.jl into Base.jl. Maybe there is a more elegant way of doing it.