Skip to content

Memory Phase 0A: Aliased Array Resize

Implemented against baseline d4a84ca. This is the first approved memory-roadmap step: repair Array::Resize/TryResize aliased fill across growth, preserving the public API, layout, allocation family and capacity policy. It corresponds to Phase 0A in the approved workspace docs/architecture/memory-management.md and finding C4 in docs/architecture/memory-management-review.md. Those design files were uncommitted in the source workspace at this baseline; this implementation change restates its acceptance contract here and does not bundle that larger architecture-document change.

Implemented

Previously, values.Resize(newSize, values[index]) could free or move from the fill object before copying it into the new tail. The new regression reproduced an ASan heap-use-after-free before the fix.

TryResizeImpl now obtains replacement storage, constructs the added elements while the fill reference remains valid, relocates the existing elements, releases their old storage and commits the new representation. With spare capacity it constructs in place. Shrink and same-size requests do not consume the fill value.

Public API

No new API, allocator, tag, instrumentation or ABI change. All four existing Resize/TryResize overloads still share the private implementation. Array remains three words (24 bytes on this host); StaticArray is unchanged.

Internal architecture

The implementation uses the existing AllocateStorage/FreeStorageBytes seam and centralized element-lifetime helpers. It neither copies a temporary fill object nor scans/computes an index to detect aliasing. Allocation/size rejection precedes element construction and state mutation. The existing exception-free element-operation contract remains in force; no exception rollback is added. No extra allocation or new allocation family is introduced.

Tests

Seven new Catch2 cases exercise 37 generated combinations: both public fill APIs; first/middle/last-element aliases; growing and reserved storage; nontrivial values with lifetime/canary accounting; 128-byte alignment; external fill with empty/populated storage; same-size/shrink/zero requests; default-initialized tails; and overflow rejection without mutation. The existing move-only, relocation, ownership, layout and allocation-count tests remain active. Fill resize requires copying; this phase does not expand element-type constraints.

Configuration Build and test result
Debug Warning-clean; 19/19 CTest tests passed
Development Warning-clean; 18/18 passed
Release, tests explicitly enabled Warning-clean; 17/18 passed initially; the sole assertion watchdog timeout passed on isolated retry
Profile Warning-clean; 18/18 passed
ASan/UBSan Warning-clean; 15/16 passed initially; the sole assertion watchdog timeout passed on isolated retry

The failures were ludus_assert_blocked_output's five-second watchdog during parallel configuration runs, outside the changed container code. Isolated reruns passed in 3.77 s (Release) and 4.15 s (sanitized). No watchdog limit was increased and no assertion implementation was changed. The sanitizer container suite passed all 3,543 assertions in 57 cases, including the formerly failing case.

Pinned clang-format 18, the repository's full configured-source clang-tidy 18 check, standalone benchmark clang-tidy, foundational include and standalone header gates passed. The existing benchmark helper's two count parameters have a narrow lint annotation documenting their existing order; its unused barrier helper was removed to allow a warnings-as-errors standalone build. Installed Development SDK validation and its consumer passed. The time-traced build budget passed: Array averaged 104 ms per include against 2,000 ms; total frontend parsing was 75.4 s against 120 s.

The isolated worktree reused cached pinned tools/dependencies. The scripts' bootstrap fingerprint check rejected that shared cache as stale; the same repository format/include/tidy and SDK verification functions were run directly against fresh CMake configurations. No dependency bootstrap metadata or recipe was changed. Every engine configuration used C++23, -fno-exceptions, PCH off and warnings-as-errors; test targets retained their normal exception setting.

Benchmarks

Extended the existing benchmark with two fill-resize workloads and more output precision; no new harness. Each processes 4,096 arrays, growing 64 initialized uint32 elements to 1,024, with capacity either reserved up front or grown. Both verify prefix/tail values and retain the existing optimization barrier. Construction, allocation and cleanup are included. External fill makes both revisions valid; timing the old dangling alias would be meaningless.

Clang 18.1.3/libstdc++, C++23, -O2 -DNDEBUG -fno-exceptions, system allocator, Intel i5-8265U, CPU 0 affinity, no sanitizers/capture. Both binaries use the same benchmark source and support library; the baseline header is from d4a84ca. After one warmup each, 12 fresh-process pairs ran in randomized order (seed 10). Each process retains the existing harness's best-of-seven batch policy. Results below are medians of those process minima, not individual-operation p99s.

Workload Before ns/element After ns/element Change
Fill resize, growing 0.1227 0.1200 -2.2%
Fill resize, reserved 0.1116 0.1099 -1.5%
Reserved append control 0.8144 0.8120 -0.3%
POD32 append control 36.4350 36.4909 +0.2%

The affected workloads' interquartile ranges overlap: growing was 0.1189–0.1256 before and 0.1174–0.1310 after; reserved was 0.1069–0.1130 before and 0.1076–0.1149 after. The unchanged POD copy workload varied by -16.4% (2.4520 → 2.0487), exposing environmental/code-layout sensitivity. No speedup claim is justified. This evidence shows no clear resize regression at this scale; it does not establish frame-time or allocator improvements.

Whole-benchmark median peak RSS was 15,060 KiB in both variants. This aggregate includes all workloads and is not a per-resize fragmentation measurement. Raw samples and metadata, hashes and process measurements retain all ten workloads, including unfavorable/noisy observations. Final benchmark binaries were byte-identical to the measured binaries after the comment-only lint annotation.

To reproduce, build the existing benchmark with the flags above and the Release Base library/generated includes. Compile the same benchmark source twice, once with the baseline array.hpp in a preceding include overlay, once with the current header. Keep the other sources, includes, compiler and library fixed; run each on one CPU in randomized fresh-process pairs. The normal CMake presets and ctest --test-dir out/build/<preset> --output-on-failure reproduce the tests; configure Release with -DLUDUS_BUILD_TESTS=ON.

Repository audit

Repository searches covered resize call sites, raw byte allocation families, placement construction and ordinary new/delete in maintained source. Every resize overload reaches TryResizeImpl; it still uses the single existing Array byte seam. There is no FoundationMemory/AllocationDomain API yet and no bypass or owning-pointer-family migration in this phase.

Intentionally retained paths are Array's aligned nothrow new/aligned delete, Base UniquePtr's matching default delete, logger/sink and window factories, profiling's current calloc/free slab, standard-library internals, native-library destroy pairs and isolated allocation-test interposers. Their migrations and known defects belong to later approved steps. No legacy allocator was replaced, so none was removed or left competing with a new implementation.

Known limitations

The first phase is a container correctness prerequisite, not an allocator implementation. Arbitrary throwing or side-effecting element constructors are outside its unchanged contract. Forced backend-OOM/self-move tests and the broader element-type contract remain Phase 0B. Ownership-helper and profiler lifetime/alignment findings remain separate work. Windows/macOS are not build-supported by this baseline; no new threading guarantee is introduced.

Next phase

Phase 0B: test and repair the fallible element contract, especially self-move TryAdd with forced allocation failure and supported copy/move/relocation constraints. Do not implement FoundationMemory until its approved prerequisites and baseline gates are met. Phase 0B is not included here.