Skip to content

Commit 9c2cce6

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Use AllocCheck.check_allocs for zero-allocation tests (#66)
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: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b3bcb59 commit 9c2cce6

1 file changed

Lines changed: 29 additions & 84 deletions

File tree

test/alloc_tests.jl

Lines changed: 29 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,36 @@
11
using AllocCheck
2-
using BenchmarkTools
32
using FastAlmostBandedMatrices
43
using FastAlmostBandedMatrices: DisjointRange
54
using ArrayLayouts: colsupport, rowsupport
65
using Test
76

7+
# Allocation freedom is verified with `AllocCheck.check_allocs`, which statically
8+
# proves the absence of any allocating code path for the given argument types.
9+
# This is immune to the first-call compilation and global-scope boxing noise that
10+
# `@allocated` picks up on newer Julia versions, so it directly tests the intended
11+
# invariant (the operation never allocates) rather than a single runtime sample.
12+
13+
getidx(A, i...) = A[i...]
14+
setidx!(A, v, i...) = (A[i...] = v; nothing)
15+
function sumiter(d)
16+
s = zero(eltype(d))
17+
for x in d
18+
s += x
19+
end
20+
return s
21+
end
22+
823
@testset "Allocation Tests" begin
924
@testset "DisjointRange - Zero Allocations" begin
10-
# Test that DisjointRange operations don't allocate
1125
r1 = Base.OneTo(5)
1226
r2 = 10:15
1327
dr = DisjointRange(r1, r2)
1428

15-
# Test length
16-
allocs = @allocated length(dr)
17-
@test allocs == 0
18-
19-
# Test getindex
20-
allocs = @allocated dr[3]
21-
@test allocs == 0
22-
23-
allocs = @allocated dr[8]
24-
@test allocs == 0
25-
26-
# Test first/last
27-
allocs = @allocated first(dr)
28-
@test allocs == 0
29-
30-
allocs = @allocated last(dr)
31-
@test allocs == 0
32-
33-
# Test iteration (after warmup)
34-
sum_test = 0
35-
for x in dr
36-
sum_test += x
37-
end
38-
allocs = @allocated begin
39-
s = 0
40-
for x in dr
41-
s += x
42-
end
43-
s
44-
end
45-
@test allocs == 0
29+
@test isempty(check_allocs(length, (typeof(dr),)))
30+
@test isempty(check_allocs(getidx, (typeof(dr), Int)))
31+
@test isempty(check_allocs(first, (typeof(dr),)))
32+
@test isempty(check_allocs(last, (typeof(dr),)))
33+
@test isempty(check_allocs(sumiter, (typeof(dr),)))
4634
end
4735

4836
@testset "colsupport - Zero Allocations" begin
@@ -52,17 +40,9 @@ using Test
5240
F = rand(Float64, m, n)
5341
A = AlmostBandedMatrix(B, F)
5442

55-
# Warmup
56-
colsupport(A, 5)
57-
colsupport(A, 50)
58-
59-
# Test colsupport for j <= l+u (should return OneTo, no allocation)
60-
allocs = @allocated colsupport(A, 5)
61-
@test allocs == 0
62-
63-
# Test colsupport for j > l+u (now returns DisjointRange instead of vcat)
64-
allocs = @allocated colsupport(A, 50)
65-
@test allocs == 0
43+
# colsupport returns OneTo for j <= l+u and a DisjointRange otherwise; both
44+
# branches must be allocation-free.
45+
@test isempty(check_allocs(colsupport, (typeof(A), Int)))
6646
end
6747

6848
@testset "rowsupport - Zero Allocations" begin
@@ -72,16 +52,7 @@ using Test
7252
F = rand(Float64, m, n)
7353
A = AlmostBandedMatrix(B, F)
7454

75-
# Warmup
76-
rowsupport(A, 1)
77-
rowsupport(A, 50)
78-
79-
# Test rowsupport (always returns UnitRange, no allocation)
80-
allocs = @allocated rowsupport(A, 1)
81-
@test allocs == 0
82-
83-
allocs = @allocated rowsupport(A, 50)
84-
@test allocs == 0
55+
@test isempty(check_allocs(rowsupport, (typeof(A), Int)))
8556
end
8657

8758
@testset "getindex/setindex! - Zero Allocations" begin
@@ -91,25 +62,8 @@ using Test
9162
F = rand(Float64, m, n)
9263
A = AlmostBandedMatrix(B, F)
9364

94-
# Warmup
95-
_ = A[50, 50]
96-
A[50, 50] = 1.0
97-
98-
# Test getindex
99-
allocs = @allocated A[50, 50]
100-
@test allocs == 0
101-
102-
# Test setindex! in band part
103-
allocs = @allocated A[50, 50] = 2.0
104-
@test allocs == 0
105-
106-
# Test setindex! in fill part
107-
allocs = @allocated A[1, 50] = 3.0
108-
@test allocs == 0
109-
110-
# Test setindex! in overlapping part
111-
allocs = @allocated A[1, 1] = 4.0
112-
@test allocs == 0
65+
@test isempty(check_allocs(getidx, (typeof(A), Int, Int)))
66+
@test isempty(check_allocs(setidx!, (typeof(A), Float64, Int, Int)))
11367
end
11468

11569
@testset "bandpart/fillpart - Zero Allocations" begin
@@ -119,16 +73,7 @@ using Test
11973
F = rand(Float64, m, n)
12074
A = AlmostBandedMatrix(B, F)
12175

122-
# Warmup
123-
bandpart(A)
124-
fillpart(A)
125-
126-
# Test bandpart
127-
allocs = @allocated bandpart(A)
128-
@test allocs == 0
129-
130-
# Test fillpart
131-
allocs = @allocated fillpart(A)
132-
@test allocs == 0
76+
@test isempty(check_allocs(bandpart, (typeof(A),)))
77+
@test isempty(check_allocs(fillpart, (typeof(A),)))
13378
end
13479
end

0 commit comments

Comments
 (0)