From 1e30321b011f0c74c78b5c50492851312be0f155 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Mon, 15 Jun 2026 10:23:10 -0400 Subject: [PATCH] Use AllocCheck.check_allocs for zero-allocation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The allocation tests in test/alloc_tests.jl used `@allocated`, which measures allocations of a single runtime call. On Julia 1.12+ this surfaced false positives that did not reflect any real allocation in the package: - `dr[8]` (DisjointRange second-range branch) was never warmed up before its `@allocated`, so it measured ~700k bytes of first-call compilation. - The iteration test accumulated into a global-scope variable inside the `@allocated begin ... end` block, so the accumulator was boxed (704 bytes) — an artifact of measuring in non-function scope, not of the iterate methods. - The first `@allocated A[50, 50]` after a single warmup still captured residual specialization allocations (16 bytes). `AllocCheck.check_allocs` (a test dependency already declared but unused) statically proves the absence of any allocating code path for the given argument types. It is immune to compilation and global-scope boxing noise and is a strictly stronger guarantee than `@allocated == 0`. `check_allocs` reports zero allocation sites for every operation on Julia 1.10, 1.12.6, and 1.13.0-rc1, confirming the package code is genuinely allocation-free; only the test methodology was flaky. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- test/alloc_tests.jl | 113 ++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 84 deletions(-) diff --git a/test/alloc_tests.jl b/test/alloc_tests.jl index d826b2c..c24ba51 100644 --- a/test/alloc_tests.jl +++ b/test/alloc_tests.jl @@ -1,48 +1,36 @@ using AllocCheck -using BenchmarkTools using FastAlmostBandedMatrices using FastAlmostBandedMatrices: DisjointRange using ArrayLayouts: colsupport, rowsupport using Test +# Allocation freedom is verified with `AllocCheck.check_allocs`, which statically +# proves the absence of any allocating code path for the given argument types. +# This is immune to the first-call compilation and global-scope boxing noise that +# `@allocated` picks up on newer Julia versions, so it directly tests the intended +# invariant (the operation never allocates) rather than a single runtime sample. + +getidx(A, i...) = A[i...] +setidx!(A, v, i...) = (A[i...] = v; nothing) +function sumiter(d) + s = zero(eltype(d)) + for x in d + s += x + end + return s +end + @testset "Allocation Tests" begin @testset "DisjointRange - Zero Allocations" begin - # Test that DisjointRange operations don't allocate r1 = Base.OneTo(5) r2 = 10:15 dr = DisjointRange(r1, r2) - # Test length - allocs = @allocated length(dr) - @test allocs == 0 - - # Test getindex - allocs = @allocated dr[3] - @test allocs == 0 - - allocs = @allocated dr[8] - @test allocs == 0 - - # Test first/last - allocs = @allocated first(dr) - @test allocs == 0 - - allocs = @allocated last(dr) - @test allocs == 0 - - # Test iteration (after warmup) - sum_test = 0 - for x in dr - sum_test += x - end - allocs = @allocated begin - s = 0 - for x in dr - s += x - end - s - end - @test allocs == 0 + @test isempty(check_allocs(length, (typeof(dr),))) + @test isempty(check_allocs(getidx, (typeof(dr), Int))) + @test isempty(check_allocs(first, (typeof(dr),))) + @test isempty(check_allocs(last, (typeof(dr),))) + @test isempty(check_allocs(sumiter, (typeof(dr),))) end @testset "colsupport - Zero Allocations" begin @@ -52,17 +40,9 @@ using Test F = rand(Float64, m, n) A = AlmostBandedMatrix(B, F) - # Warmup - colsupport(A, 5) - colsupport(A, 50) - - # Test colsupport for j <= l+u (should return OneTo, no allocation) - allocs = @allocated colsupport(A, 5) - @test allocs == 0 - - # Test colsupport for j > l+u (now returns DisjointRange instead of vcat) - allocs = @allocated colsupport(A, 50) - @test allocs == 0 + # colsupport returns OneTo for j <= l+u and a DisjointRange otherwise; both + # branches must be allocation-free. + @test isempty(check_allocs(colsupport, (typeof(A), Int))) end @testset "rowsupport - Zero Allocations" begin @@ -72,16 +52,7 @@ using Test F = rand(Float64, m, n) A = AlmostBandedMatrix(B, F) - # Warmup - rowsupport(A, 1) - rowsupport(A, 50) - - # Test rowsupport (always returns UnitRange, no allocation) - allocs = @allocated rowsupport(A, 1) - @test allocs == 0 - - allocs = @allocated rowsupport(A, 50) - @test allocs == 0 + @test isempty(check_allocs(rowsupport, (typeof(A), Int))) end @testset "getindex/setindex! - Zero Allocations" begin @@ -91,25 +62,8 @@ using Test F = rand(Float64, m, n) A = AlmostBandedMatrix(B, F) - # Warmup - _ = A[50, 50] - A[50, 50] = 1.0 - - # Test getindex - allocs = @allocated A[50, 50] - @test allocs == 0 - - # Test setindex! in band part - allocs = @allocated A[50, 50] = 2.0 - @test allocs == 0 - - # Test setindex! in fill part - allocs = @allocated A[1, 50] = 3.0 - @test allocs == 0 - - # Test setindex! in overlapping part - allocs = @allocated A[1, 1] = 4.0 - @test allocs == 0 + @test isempty(check_allocs(getidx, (typeof(A), Int, Int))) + @test isempty(check_allocs(setidx!, (typeof(A), Float64, Int, Int))) end @testset "bandpart/fillpart - Zero Allocations" begin @@ -119,16 +73,7 @@ using Test F = rand(Float64, m, n) A = AlmostBandedMatrix(B, F) - # Warmup - bandpart(A) - fillpart(A) - - # Test bandpart - allocs = @allocated bandpart(A) - @test allocs == 0 - - # Test fillpart - allocs = @allocated fillpart(A) - @test allocs == 0 + @test isempty(check_allocs(bandpart, (typeof(A),))) + @test isempty(check_allocs(fillpart, (typeof(A),))) end end