Why is `copy(adjoint)` not an adjoint object?

This surprised me:

julia> typeof(copy(rand(4,4)'))
Matrix{Float64} (alias for Array{Float64, 2})

julia> typeof(deepcopy(rand(4,4)'))
LinearAlgebra.Adjoint{Float64, Matrix{Float64}}

I have an algorithm which looks prettier when written assuming row-major order. To ensure the code looks pretty and it also runs fast on Julia’s native column-major arrays, I simply used the Adjoint object. It works great and it is both fast and the code looks nice. However, the speedup was lost when I ran it on copies of my arrays. Personally, I got to fix that simply by using deepcopy, and it is truly not a problem, but the behavior of copy surprised me.

This does not seem to be a question of shallow copy vs deep copy, right? It is more of a question related to the fact that copy implicitly runs collect on the object it is copying? This sound counterintuitive to me. Is there any rationale behind it, or is it more of a random historic fact?

1 Like

More generally, why is copy not preserving the type of the object it is copying?

Same remark seems to have been made here by @dlfivefifty. It does not say if a PR was created.

1 Like

copy was chosen as the function that materializes lazy adjoints and transposes (xref https://github.com/JuliaLang/julia/pull/25364).

5 Likes

But that was apparently when collect was supposed to be deprecated (replaced with Array), which didn’t happen… See https://github.com/JuliaLang/julia/issues/16029

In any case, I wonder what was the rationale: it seems more useful and intuitive to use collect or Array for materialization and have copy make a straight copy?

3 Likes

This may be related to another thread of mine where I reported that adjointness ‘disappears’ after abs.: How to write generic low-rank updates?

1 Like

I think that the fundamental question is why you would want to copy a LinearAlgebra.Adjoint.

1 Like

That question seems weird to me, as it can be asked of any functionality. Here is of the top of my head:

  • First and foremost, the whole point of LinearAlgebra.Adjoint is that it is lazy-ish, which permits certain optimizations. Otherwise we would just always use an explicit transpose. It is also the way to create a row-major matrix in Julia, which is important for efficient use of CPU cache by some algorithms.
  • the copy is of use for benchmarks of in-place operations
  • or for Monte Carlo simulations involving states stored in such an object
  • purely for consistency purposes: copy changing the type of an object seems like a weird contract
1 Like

The interface of copy is mostly a convention, but generally if the argument is a wrapper that provides a “view” (eg Adjoint, PermutedDimsArray, SubArray), would you expect that it copies the parent and makes a similar view?

This may be a reasonable use case in some contexts, but that’s not how it works currently — maybe finding a different name for it would be the best way forward, instead of breaking copy.

In the meantime, you can copy the parent if you want to modify an object like this.

2 Likes

Yeah, I did expect it would copy the parent and make a similar view. I see how this can go wrong if the parent is much bigger.

The status quo now makes sense to me (the current convention about copies of views). It is certainly not an intuitive convention to me, but I can hardly claim some universal opinion here.

1 Like

As mentioned in the thread linked above

julia> copy([1,2]')
1×2 LinearAlgebra.Adjoint{Int64,Array{Int64,1}}:
 1  2

so copy does preserve Adjoint wrappers at times.

1 Like

That final case is important — and hopefully proves the rule by exception — because adjoint vectors behave differently than one-row matrices.

2 Likes

How should wrappers of Adjoint vectors behave? I guess it will make sense to preserve their nature. Currently

julia> OffsetArray([1,2]', 0, 0)
1×2 OffsetArray(adjoint(::Vector{Int64}), 1:1, 1:2) with eltype Int64 with indices 1:1×1:2:
 1  2

julia> OffsetArray([1,2]', 0, 0) |> copy
1×2 OffsetArray(::Matrix{Int64}, 1:1, 1:2) with eltype Int64 with indices 1:1×1:2:
 1  2

In this case the copy is a 1-row matrix and not an Adjoint. Would it make sense for OffsetArrays to pass on the copy operation to the parent?