julia icon indicating copy to clipboard operation
julia copied to clipboard

make `copymutable(::Array)` use `copy` to avoid overhead

Open thchr opened this issue 1 year ago • 2 comments

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

thchr avatar Oct 18 '24 10:10 thchr

On v1.11 and master, the gain seems to be smaller (in fact, might be measurement noise), at least on my machine.

martinholters avatar Oct 18 '24 10:10 martinholters

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

thchr avatar Oct 18 '24 11:10 thchr

on my machine I see essentially zero overhead in both cases

adienes avatar Apr 28 '25 23:04 adienes

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 to copy(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.

thchr avatar Apr 29 '25 07:04 thchr

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

adienes avatar Apr 29 '25 14:04 adienes