make `copymutable(::Array)` use `copy` to avoid overhead
This removes the overhead of using Base.copymutable over copy for small Arrays:
using BenchmarkTools
v = rand(2)
A = rand(2, 2)
B = rand(3, 3)
C = rand(4, 4)
@btime Base.copymutable($v); # v1.10.4: 24 ns, PR: 18 ns
@btime Base.copymutable($A); # v1.10.4: 29 ns, PR: 23 ns
@btime Base.copymutable($B); # v1.10.4: 35 ns, PR: 31 ns
@btime Base.copymutable($C); # v1.10.4: 35 ns, PR: 33 ns
On v1.11 and master, the gain seems to be smaller (in fact, might be measurement noise), at least on my machine.
On v1.11 and master, the gain seems to be smaller (in fact, might be measurement noise), at least on my machine.
That's very cool! - I see the same, now that I check.
There's still a difference for more complicated element types though (similar to https://github.com/JuliaLang/julia/pull/55748):
struct GarbageVector{N} <: AbstractVector{Int}
v :: Vector{Int}
garbage :: NTuple{N, Int}
end
GarbageVector{N}(v::Vector{Int}) where N = GarbageVector{N}(v, ntuple(identity, Val(N)))
Base.getindex(gv::GarbageVector, i::Int) = gv.v[i]
Base.size(gv::GarbageVector) = size(gv.v)
using BenchmarkTools
v = [GarbageVector{20}([1,2,3]), GarbageVector{20}([4,5,6]), GarbageVector{20}([7,8,9])]
@btime copy($v) # 54 ns
@btime Base.copymutable($v) # 66 ns
on my machine I see essentially zero overhead in both cases
I continue to see a difference with the GarbageVector example above (on v1.11.3):
julia> using Chairmarks
julia> @be copy($v)
julia> @be Base.copy($v) seconds=5
Benchmark: 127544 samples with 132 evaluations
min 52.068 ns (2 allocs: 576 bytes)
median 155.508 ns (2 allocs: 576 bytes)
mean 246.656 ns (2 allocs: 576 bytes, 0.12% gc time)
max 133.112 μs (2 allocs: 576 bytes, 99.58% gc time)
julia> @be Base.copymutable($v) seconds=5
Benchmark: 237948 samples with 67 evaluations
min 57.015 ns (2 allocs: 576 bytes)
median 159.746 ns (2 allocs: 576 bytes)
mean 289.386 ns (2 allocs: 576 bytes, 0.06% gc time)
max 2.995 ms (2 allocs: 576 bytes, 99.96% gc time)
I think it makes sense that there would be a (small) difference: the copymutable(a::AbstractArray) implementation is just copyto!(similar(a), a), while the copy(::Array) implementation goes through a more complicated hoop, referencing the underlying Memory directly.
At a more basic level, the docstring of copymutable states (my emphasis):
Make a mutable copy of an array or iterable a. For
a::Array, this is equivalent tocopy(a), (...)
With that in mind, it seems natural to have the Array implementation directly use the copy method, rather than relying on a generic AbstractArray fallback.
Conversely, if copyto!(similar(a), a) is as fast as what happens in the copy(::Array) implementation, it seems that should be the implementation there - but I'm imagining it's not generally.
I suppose there is a similar definition here anyway https://github.com/JuliaLang/julia/blob/1aeea1979fac8f8cc9ffcc89d4c62830be55bebf/base/bitset.jl#L49
and it does make sense
looking through Base a bit I see a few times as well that copy specialization is defined in terms of copymutable. I guess it's a bit unfortunate / confusing that there's not a clear hierarchy of methods there, but that's unrelated to this PR