Repository navigation
Add FixedSizeArrays.wrap to construct a FixedSizeArray around an existing parent without copying - #196
Conversation
…isting parent without copying `wrap(FixedSizeArray, parent::DenseVector, size)` returns a `FixedSizeArray` using `parent` as its memory backend. It checks that `length(parent)` equals `prod(size)`, and that the result matches the (optionally more specific) `FixedSizeArray` type given as the first argument. This allows reusing a resizable `Vector` as backing storage: wrap it again after `resize!`. Assisted-by: Claude Code (Opus 5.5)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #196 +/- ##
==========================================
+ Coverage 92.95% 93.39% +0.43%
==========================================
Files 5 5
Lines 213 227 +14
==========================================
+ Hits 198 212 +14
Misses 15 15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The bot points out that using FixedSizeArrays: FixedSizeArray, wrap
v = collect(1:6)
a = wrap(FixedSizeArray, v, (2, 3))
resize!(v, 2)
a[6] # returns 6 from stale memory, no BoundsErroraccesses undefined memory. I don't think we can do much about it (for sure we can't resize the FixedSizeArray!) because this is |
Yes please! |
| The result shares its elements with `parent`, and `parent(FixedSizeArrays.wrap(FixedSizeArray, p, size)) === p`. | ||
| Changing the length of `parent` (e.g. with `resize!` when `parent` is a `Vector`) while the | ||
| result is in use is not allowed: wrap `parent` again after resizing it instead. | ||
|
|
There was a problem hiding this comment.
The bot points out that
using FixedSizeArrays: FixedSizeArray, wrap v = collect(1:6) a = wrap(FixedSizeArray, v, (2, 3)) resize!(v, 2) a[6] # returns 6 from stale memory, no BoundsErroraccesses undefined memory. I don't think we can do much about it (for sure we can't resize the FixedSizeArray!) because this is
parent's behaviour, but perhaps it's worth calling out in the docstring the undefined behaviour of when the parent memory is resized?
Uhm, this is actually already here, so perhaps not much else to do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace the `unsafe_wrap(Array, pointer(v), dims)` views of the
`resize!`able backing vectors with `FixedSizeArray{T, N, Vector{T}}`s
created with `FixedSizeArrays.wrap`, re-created after every `resize!`.
Since Julia v1.11, `Array` is a mutable struct and its size and data
pointer are reloaded inside inner loops whenever LLVM cannot prove that
the stores in the loop do not alias them. The dimensions of a
`FixedSizeArray` are immutable. The view also keeps the backing vector
alive, so it is GC-safe without `GC.@preserve`.
Requires JuliaArrays/FixedSizeArrays.jl#196 (temporarily via [sources]).
Assisted-by: Claude Code (Opus 5.5)
Replace the `unsafe_wrap(Array, pointer(v), dims)` views of the
`resize!`able backing vectors with `FixedSizeArray{T, N, Vector{T}}`s
created with `FixedSizeArrays.wrap`, re-created after every `resize!`.
Since Julia v1.11, `Array` is a mutable struct and its size and data
pointer are reloaded inside inner loops whenever LLVM cannot prove that
the stores in the loop do not alias them. The dimensions of a
`FixedSizeArray` are immutable. The view also keeps the backing vector
alive, so it is GC-safe without `GC.@preserve`.
Requires JuliaArrays/FixedSizeArrays.jl#196 (temporarily via [sources]).
Assisted-by: Claude Code (Opus 5.5)
This adds a public, checked way to construct a
FixedSizeArrayaround an existing parent without copying:DimensionMismatchunlesslength(parent) == prod(size), so the existing invariant betweenparentandsize(cf. MemoryViews.jl interoperability #111) holds.FixedSizeArray{T},FixedSizeArray{T,N},FixedSizeArray{T,N,Mem}, ...). The element type, number of dimensions and memory backend are checked against it.Memorybackend.wrapis declaredpublicon Julia 1.11+, but not exported. It mirrors the call shape ofBase.wrap(Array, mem, dims). I did not extendBase.wrapbecause it is not public in Base, and it allows a parent longer than the array.Currently the only no-copy path is the internal
new_fixed_size_array, which performs no checks. All public constructors andcollect_ascopy.Motivation
Trixi.jl keeps its solver containers in resizable
Vectors, because adaptive mesh refinement changes the number of elements and interfaces. After everyresize!it re-creates N-d views withunsafe_wrap(Array, pointer(v), dims). On Julia 1.11–1.13 theseArrayviews make some kernels much slower (JuliaLang/julia#62798, trixi-framework/Trixi.jl#3197). Theunsafe_wrapaliases also skip write barriers for non-isbits element types.With this PR the views can be
FixedSizeArray{T,N,Vector{T}}s that are wrapped again after eachresize!. Benefits:Vector, and stores go through theVector, so write barriers are emitted.A local prototype of the Trixi containers using this pattern changes
calc_interface_flux!from 555 → 372 μs on Julia 1.12 and 595 → 340 μs on 1.13. Results with AMR are bit-identical.A
Vectorkeeps its capacity when it shrinks, so wrapping its underlyingMemorywould breaklength(parent) == prod(size). That is why the check is strict and wrapping theVectoritself is the intended pattern. #189 (MemoryRef backend) would be a natural follow-up for reusing a larger buffer, but it is not needed here.Testing
resize!, 0-d, and the error paths.CHANGELOG.mdentry because its last entry is for v1.2.0. Happy to add one if wanted.🤖 Generated with Claude Code