I’ve noticed that only is not as fast as it could be. I compare it to
function only2(x)
length(x) == 1 || error("Collection must contain exactly one element")
@inbounds first(x) # EDIT: added @inbounds
end
In the following benchmarks (done with master) for various subtypes of AbstractDict, only2 is never slower than only:
x
only(x)
only2(x)
Dict('x' => 3)
11.930 ns
5.898 ns
IdDict('x' => 3)
16.893 ns
8.153 ns
v = [1,2]; x = WeakKeyDict(v => 3)
410.720 ns
240.881 ns
Base.PersistentDict('x' => 3)
1.409 μs
1.146 μs
pairs((x = 3,))
12.674 ns
12.672 ns
Base.ImmutableDict('x' => 3)
3.451 ns
3.268 ns
Would you be interest in a PR that uses first for some or all abstract dictionaries? Or maybe also for other types?
Here are some more benchmarks:
x
only(x)
only2(x)
('x',)
2.673 ns
2.602 ns
['x']
2.963 ns
2.966 ns
Set([x])
9.225 ns
4.083 ns
"x"
3.261 ns
6.571 ns
The last example shows that only2 is not automatically faster for types T with Base.haslength(T) == true. If desired, one could make only(x::T) look at haslength(T) be default and add separate methods for types like String where this is not a good idea.
function only3(x)
r = iterate(x)
isnothing(r) && error("Collection is empty")
t,s = r
isnothing(iterate(x,s)) ? t : error("Collection contains more than one element")
end
is also pretty fast. Doesn’t length(x) which can sometimes take more time.
I should have said that the benchmarks were on master (now added above). For 1.9.4 and 1.10.0-rc1 I get similar patters. Many benchmarks are somewhat faster than on master, but the ratio between only and only2 is roughly the same.
The only exception is ImmutableDict on 1.10.0-rc1 where only suddenly allocates:
iterate has to prepare state for the next iteration, while first can just return first element (regardless of the rest of the container).
Perhaps the right way to go, is two versions of only dispatching on the IteratorSize trait. If a container has a HasLength then use length otherwise just use iterate.
But e.g. Set([1]) was shown as one of the biggest differences (9ns vs 4ns), but
julia> s = Set([1])
Set{Int64} with 1 element:
1
julia> @edit first(s)
shows the implementation is just the generic first that calls iterate:
function first(itr)
x = iterate(itr)
x === nothing && throw(ArgumentError("collection must be non-empty"))
x[1]
end
That is to say, I am skeptical of these benchmarks, because the numbers are so tiny (a few ns) that I think they can be overwhelmed by artifacts, and one needs to be very careful to accurately conclude one implementation is faster than another.
For data structures based on hash tables it makes sense to me that using first is faster: When iterating through a hash table, Julia doesn’t count the number of elements seen so far, but only scans for more elements until it reaches the end of the table. (Correct me if I’m wrong.) Hence for only (with error checking on) it needs to scan twice instead of only once.