Repository navigation
fill!(::AbstractVectorOfArray, x): fill inner arrays directly, fixing a silent ragged bug - #669
Conversation
fill! looped over VA[:, i], which for a ragged inner array builds and returns a zero-padded *copy* (src/vector_of_array.jl's _getindex for (::Colon, ::Int)). Filling that copy touches nothing: the write is silently dropped for every inner array shorter than the ragged maximum. It also costs an unnecessary allocating reconstruction on every iteration even in the common non-ragged case. Iterate VA.u[i] directly instead: it is always the real storage, so fill! correctly fills every inner array regardless of its size, and the non-ragged case no longer reconstructs anything. n=1000 inner arrays: fill! 2.43ms -> 4.8us (1.12), 2.45ms -> 2.1us (1.10, per the earlier audit). Ragged correctness (previously silently wrong) verified: filling a ragged VectorOfArray([[1.,2.],[3.],[4.,5.,6.]]) with 9.0 now gives [[9.,9.],[9.],[9.,9.,9.]] (every inner array filled) instead of the pre-existing [[1.,2.],[3.],[9.,9.,9.]] (only the longest inner array actually gets filled). Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.294 Agent-Model: claude-sonnet-5 Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Independent review (Devin CLI 3000.11.3, model fusion-claude-opus-5-5-high-sidekick-swe-2-medium): MERGE, risk low. Full reviewVERDICT: MERGE PR: #669 (head 61250c9). It has no linked issue and no tracking issue, so there was no sibling interface to check beyond the other open perf PRs (#666–#678). None of them touch Blocking findingsNone. The change at Non-blocking findings
What I ranAll runs used Julia 1.12, with TMPDIR and scratch envs under
What I did not verify
🤖 Posted by an AI agent — harness: Devin CLI 3000.11.3 (review), Claude Code 2.1.285 (posting) · model: fusion-claude-opus-5-5-high-sidekick-swe-2-medium |
What changed and why
fill!(VA::AbstractVectorOfArray, x)loopedfor i in 1:length(VA.u)and read/wrote throughVA[:, i]. For a ragged inner array,VA[:, i](_getindex(A, ::NotSymbolic, ::Colon, I::Int)) builds and returns a zero-padded copy when the inner array's size doesn't match the ragged maximum — it is not the real storage in that case.fill!(VA[:, i], x)then fills that throwaway copy and discards it; the realVA.u[i]is never touched. In the common non-ragged case,VA[:, i]does return the real storage directly, so this only costs an unnecessary allocating round-trip through_getindex/checkbounds/size(A)on every iteration (no correctness bug there).Fixed by iterating
VA.u[i]directly, which is always the actual storage regardless of raggedness. This is both faster (no reconstruction) and fixes the silent correctness bug for ragged arrays described below.Before/after
n = 1000inner (non-ragged)Vector{Float64}of length 2:(matches the earlier perf audit's 2.43ms -> 5.0µs on 1.12 / 2.45ms -> 2.1µs on 1.10)
Ragged correctness — this is the important part.
fill!on a raggedVectorOfArrayis silently broken on current master:Before (unmodified master,
02c477c):After (this branch):
Every inner array is filled, matching what
fill!obviously should do and what every non-ragged caller already (correctly) observed.Failing-before / passing-after
Added a test to
test/Core/interface_tests.jl(next to the existingfill!/testva2test):Before (reverted
src/vector_of_array.jlto unmodified master, kept the new test):After (this branch):
test/Core/interface_tests.jlruns to completion with no failures, on both Julia 1.10.12 and 1.12.7.Full
GROUP=Corerun (both Julia versions): all testsets green, including the existing non-raggedfill!coverage ininterface_tests.jlandutils_test.jl(immutable/mixedSVector/MVectorfill!).GROUP=QAon 1.12.7: 20/20 pass.GROUP=QAon 1.10.12 hits the same pre-existing, unrelatedAquaambiguity failure documented in #667 (confirmed present on unmodified master).What was not verified
fill!on aDiffEqArray/AbstractVectorOfArraywrapping GPU arrays was not specifically exercised (theismutable/AbstractVectorOfArraybranch is unchanged logic, just readinguionce instead ofVA[:, i]repeatedly, so this shouldn't interact with GPU scalar-indexing restrictions differently than before — but it wasn't measured).Anything a reviewer should push back on
fill!on a raggedVectorOfArraysilently filled only the longest inner array(s); after, it fills every inner array. I believe the old behavior was simply a bug (afill!that silently no-ops on part of its target with no error and no indication), and the fix is what any caller would expect, but flagging explicitly since it changes observable output for an existing (if undocumented) code path.Please ignore until reviewed by @ChrisRackauckas.
Risk assessment
src/vector_of_array.jl, onefill!method. No public API signature change. The behavior change is a correctness fix (ragged arrays now get fully filled instead of silently partially filled) rather than an API change — anything that depended on the old silent-no-op behavior for part of a ragged array was already relying on a bug.GROUP=Corefull pass on Julia 1.10 and 1.12, including all pre-existingfill!tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Independent review: Devin Fusion (fusion-claude-opus-5-5-high-sidekick-swe-2-medium) rated it low, verdict MERGE: #669 (comment)