-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
speed up first and only for various types
#52296
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
Conversation
mkitti
left a comment
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
| 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 endThere 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)
# ...
endOne 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
|
|
||
| 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
| 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 |
LilithHafner
left a comment
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
mkitti
left a comment
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
firstforArrayandStringandonlyforDict,IdDictandSet. Foronlythe speed up comes from usinglengthandfirstinstead of callingiteratetwice (once to get the first element, once to check that there are no more). I have also deleted dedicatedonlymethods forTuple,Intand the like because they are not faster than the standard method.Here are some benchmarks. They were generated by the function
applied to a
Vectorvwithn = 1000randomly generated containers of the given type having one element each. Below I only show the relevant output.first@inboundsmaster@inboundsnewStringVector{Char}Matrix{Char}Array{Char, 3}only@inboundsmaster@inboundsnewDict{Char, Int64}IdDict{Char, Int64}Set{Char}Tuple{Int64}Int64CharThere 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 tofirstinstead ofonlyto get a larger speed-up.Note that
onlyforIdDictis 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.