Skip to content
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

Merged
merged 3 commits into from
Dec 4, 2023

Conversation

matthias314
Copy link
Contributor

This PR is the result of this Discourse thread. It speeds up first for Array and String and only for Dict, IdDict and Set. For only the speed up comes from using length and first instead of calling iterate twice (once to get the first element, once to check that there are no more). I have also deleted dedicated only methods for Tuple, Int and the like because they are not faster than the standard method.

Here are some benchmarks. They were generated by the function

function runtest(v)
    y = only(v[1])
    @show eltype(v)
    @btime count(x -> first(x) == $y, $v)
    @btime count(x -> @inbounds(first(x)) == $y, $v)
    @btime count(x -> only(x) == $y, $v)
    @btime count(x -> @inbounds(only(x)) == $y, $v)
end

applied to a Vector v with n = 1000 randomly generated containers of the given type having one element each. Below I only show the relevant output.

first

type master new @inbounds master @inbounds new
String 3.145 μs 1.241 μs 3.147 μs 1.368 μs
Vector{Char} 985.571 ns 982.688 ns 961.227 ns 748.953 ns
Matrix{Char} 1.275 μs 1.276 μs 1.277 μs 866.526 ns
Array{Char, 3} 1.273 μs 1.268 μs 1.302 μs 872.368 ns

only

type master new @inbounds master @inbounds new
Dict{Char, Int64} 16.931 μs 5.940 μs 6.167 μs 5.830 μs
IdDict{Char, Int64} 28.419 μs 18.514 μs 22.029 μs 15.923 μs
Set{Char} 14.810 μs 3.742 μs 5.113 μs 3.561 μs
Tuple{Int64} 71.966 ns 70.243 ns 71.962 ns 70.251 ns
Int64 71.974 ns 71.031 ns 71.961 ns 71.061 ns
Char 102.729 ns 102.744 ns 100.216 ns 100.071 ns

There are a couple of types where the new method only(x, Val(:first)) would be faster the current one (now accessible as only(x, Val(:iterate)), but only when using @inbounds. However, if one adds @inbounds, then one can also switch to first instead of only to get a larger speed-up.

Note that only for IdDict 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.

Copy link
Contributor

@mkitti mkitti left a 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))
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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?

@propagate_inbounds function only(x)
@propagate_inbounds only(x) = only(x, Val(:iterate))

@propagate_inbounds function only(x, ::Val{:iterate})
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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)

Copy link
Contributor

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

Copy link
Contributor

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).

only(x::NamedTuple) = throw(
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
)
@inline function only(x, ::Val{:first})
Copy link
Contributor

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to iterators.jl

Copy link
Contributor Author

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.

@@ -1559,18 +1561,13 @@ Stacktrace:
return ret
end

# Collections of known size
only(x::Ref) = x[]
Copy link
Contributor

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?

Copy link
Contributor Author

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"
}

Copy link
Contributor

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.

@oscardssmith oscardssmith added performance Must go faster domain:collections Data structures holding multiple items, e.g. sets labels Nov 25, 2023
@mkitti
Copy link
Contributor

mkitti commented Nov 25, 2023

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.

  1. Acceralate the current only API for particular types.
  2. Introduce a new public only API using Val.
  3. Simplify and refactor old methods of only.

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 Val usage needs a refactor for public API, but might work well enough for private methods for the moment. This is part probably needs to go to a triage review.

The simplification is fine but I think is fully dividable from this pull request since that should be effectively a no-op.

@LilithHafner LilithHafner added the status:triage This should be discussed on a triage call label Nov 28, 2023
@LilithHafner
Copy link
Member

Adding triage label exclusively for the additions to the public API.

aviatesk added a commit that referenced this pull request Nov 28, 2023
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]>
@matthias314
Copy link
Contributor Author

I have made the following changes:

  • rebased the PR on top of master
  • renamed the new two-argument version of only to _only
  • the second argument to _only now is first or iterate
  • added WeakKeyDict and PersistentDict to the types to which the new method applies. The other types are Dict, IdDict and Set.
  • change to first for AbstractArray partly undone. It now only adds @propagate_inbounds. (The previous version led to hickups in testing.)

The new methods for only are still defined where the types are defined except for IdDict (which is defined before iterators.jl). Doing it otherwise seems like a massive change to Base.jl for me.

@LilithHafner LilithHafner removed the status:triage This should be discussed on a triage call label Nov 29, 2023
Copy link
Member

@LilithHafner LilithHafner left a 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

Copy link
Contributor

@mkitti mkitti left a 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?

@LilithHafner
Copy link
Member

The string only( does not occur in the BaseBenchmarks repo so this won't be super informative, but first( does occur at least a couple of times

@nanosoldier runbenchmarks("array" || "collection")

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - successfully executed benchmarks. A full report can be found here.

@LilithHafner
Copy link
Member

@maleadt, I thought the vs keyword was optional?

@maleadt
Copy link
Member

maleadt commented Nov 30, 2023

@maleadt, I thought the vs keyword was optional?

Correct, but only for runtests invocations, as the benchmark bot hasn't been updated in ages (i.e. it needs to be updated to Nanosoldier.jl#master).

@LilithHafner
Copy link
Member

@nanosoldier runbenchmarks("array" || "collection", vs="master")

@LilithHafner
Copy link
Member

@nanosoldier runbenchmarks("array" || "collection", vs=:master)

@LilithHafner
Copy link
Member

@nanosoldier runbenchmarks("array" || "collection", vs=":master")

@LilithHafner
Copy link
Member

@nanosoldier runbenchmarks("array" || "collection", vs=":master")

lol

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here.

@mkitti
Copy link
Contributor

mkitti commented Dec 1, 2023

["array", "index", ("sumcartesian", "1:100000")] requires some investigation.

@LilithHafner
Copy link
Member

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

@LilithHafner LilithHafner merged commit bb7091c into JuliaLang:master Dec 4, 2023
9 of 10 checks passed
@LilithHafner
Copy link
Member

Thank you for your contribution, @matthias314!

mkitti pushed a commit to mkitti/julia that referenced this pull request Dec 9, 2023
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:collections Data structures holding multiple items, e.g. sets performance Must go faster
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants