Skip to content

Add FixedSizeArrays.wrap to construct a FixedSizeArray around an existing parent without copying - #196

Merged
giordano merged 5 commits into
JuliaArrays:mainfrom
vchuravy:vc/wrap
Oct 13, 2026
Merged

giordano merged 5 commits into
JuliaArrays:mainfrom
vchuravy:vc/wrap

Conversation

@vchuravy

Copy link
Copy Markdown
Contributor

This adds a public, checked way to construct a FixedSizeArray around an existing parent without copying:

FixedSizeArrays.wrap(FixedSizeArray, parent::DenseVector, size::NTuple{N,Integer})
FixedSizeArrays.wrap(FixedSizeArray, parent::DenseVector, size1, size2, ...)
FixedSizeArrays.wrap(FixedSizeArray, parent::DenseVector)   # vector of length(parent)
  • Throws a DimensionMismatch unless length(parent) == prod(size), so the existing invariant between parent and size (cf. MemoryViews.jl interoperability #111) holds.
  • The first argument can be a more specific type (FixedSizeArray{T}, FixedSizeArray{T,N}, FixedSizeArray{T,N,Mem}, ...). The element type, number of dimensions and memory backend are checked against it.
  • The result is inferred, and it doesn't allocate with a Memory backend.
  • wrap is declared public on Julia 1.11+, but not exported. It mirrors the call shape of Base.wrap(Array, mem, dims). I did not extend Base.wrap because 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 and collect_as copy.

Motivation

Trixi.jl keeps its solver containers in resizable Vectors, because adaptive mesh refinement changes the number of elements and interfaces. After every resize! it re-creates N-d views with unsafe_wrap(Array, pointer(v), dims). On Julia 1.11–1.13 these Array views make some kernels much slower (JuliaLang/julia#62798, trixi-framework/Trixi.jl#3197). The unsafe_wrap aliases 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 each resize!. Benefits:

  • The memory stays rooted by the Vector, and stores go through the Vector, so write barriers are emitted.
  • The field types stay concrete.

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 Vector keeps its capacity when it shrinks, so wrapping its underlying Memory would break length(parent) == prod(size). That is why the check is strict and wrapping the Vector itself 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

  • Tests for both storage types: inference, no allocation (Memory backend), aliasing in both directions, re-wrapping after resize!, 0-d, and the error paths.
  • Full test suite passes locally on Julia 1.10, 1.12 and 1.13. The new docstring doctest passes.
  • I didn't add a CHANGELOG.md entry because its last entry is for v1.2.0. Happy to add one if wanted.

🤖 Generated with Claude Code

…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

codecov Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.39%. Comparing base (1f00752) to head (146b606).
⚠️ Report is 1 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@giordano giordano left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we had something like this already, but I'm probably thinking of new_fixed_size_array, which isn't public and this new function...wraps

Comment thread test/runtests.jl Outdated
vchuravy and others added 2 commits October 13, 2026 10:36
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@giordano

giordano commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 BoundsError

accesses 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?

@giordano

Copy link
Copy Markdown
Collaborator

I didn't add a CHANGELOG.md entry because its last entry is for v1.2.0. Happy to add one if wanted.

Yes please!

Comment thread src/FixedSizeArray.jl
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 BoundsError

accesses 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>
@giordano
giordano merged commit b038629 into JuliaArrays:main Oct 13, 2026
18 checks passed
@vchuravy
vchuravy deleted the vc/wrap branch October 13, 2026 12:28
vchuravy added a commit to trixi-framework/Trixi.jl that referenced this pull request Oct 13, 2026
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)
vchuravy added a commit to trixi-framework/Trixi.jl that referenced this pull request Oct 20, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants