Safety Rules
AdaptiveArrayPools achieves zero allocation by reusing memory across calls. This requires understanding one critical rule.
The One Rule
+-------------------------------------------------------------+
| |
| Pool arrays are ONLY valid within their @with_pool scope |
| |
| When the scope ends, arrays are marked for reuse. |
| Using arrays after scope ends = UNDEFINED BEHAVIOR |
| |
+-------------------------------------------------------------+What's Safe
| Pattern | Example | Why It Works |
|---|---|---|
| Return computed values | return sum(v) | Scalar escapes, not the array |
| Return copies | return copy(v) | New allocation, independent data |
| Use within scope | result = A * B | Arrays valid during computation |
What's Dangerous
| Pattern | Example | Why It Fails |
|---|---|---|
| Return array | return v | Array marked for reuse after return |
| Store in global | global_ref = v | Points to reusable memory |
| Capture in closure | () -> sum(v) | v may be overwritten when closure runs |
The Scope Rule in Detail
When @with_pool ends, all arrays acquired within that scope are marked available for reuse—not immediately freed. This is what makes zero-allocation possible on subsequent calls.
@with_pool pool begin
v = acquire!(pool, Float64, 100)
result = sum(v) # ✅ compute and return values
copied = copy(v) # ✅ copy if you need data outside
end
# v is no longer valid here - it's marked for reuseAfter scope ends, using v is undefined because:
- Subsequent
acquire!calls may overwrite the data — the memory is available for reuse - Task termination may trigger GC — the pool itself could be garbage collected
- It might "work" by luck — data unchanged until next acquire, but don't rely on this
The worst case is silent data corruption: your code appears to work but produces wrong results intermittently.
What NOT to Do
Don't return pool-backed arrays
# ❌ Wrong: returning the array itself
@with_pool pool function bad_example()
v = acquire!(pool, Float64, 100)
return v # v marked for reuse after return!
end
# ✅ Correct: return computed values or copies
@with_pool pool function good_example()
v = acquire!(pool, Float64, 100)
return sum(v) # scalar result
endDon't store in globals or closures
# ❌ Wrong: storing in global
global_ref = nothing
@with_pool pool begin
global_ref = acquire!(pool, Float64, 100)
end
# global_ref now points to reusable memory - data may be overwritten
# ❌ Wrong: capturing in closure
@with_pool pool begin
v = acquire!(pool, Float64, 100)
callback = () -> sum(v) # v captured but may be overwritten later
endDon't resize or push! to unsafe_acquire! arrays
@with_pool pool begin
v = unsafe_acquire!(pool, Float64, 100)
# ❌ These break pool memory management:
# resize!(v, 200)
# push!(v, 1.0)
# append!(v, [1.0, 2.0])
endDebugging with POOL_DEBUG
Enable runtime safety checks during development:
using AdaptiveArrayPools
AdaptiveArrayPools.POOL_DEBUG[] = true
@with_pool pool function test()
v = acquire!(pool, Float64, 100)
return v # Will warn about returning pool-backed array
endacquire! vs unsafe_acquire!
| Function | Returns | Best For |
|---|---|---|
acquire! | View types (SubArray, ReshapedArray) | General use, BLAS/LAPACK |
unsafe_acquire! | Native Array/CuArray | FFI, type constraints |
Both follow the same scope rules. Use acquire! by default—views work with all standard Julia linear algebra operations.
Thread Safety
Pools are task-local, so each thread automatically gets its own pool:
# ✅ Safe: each task has independent pool
Threads.@threads for i in 1:N
@with_pool pool begin
a = acquire!(pool, Float64, 100)
# work with a...
end
end
# ❌ Unsafe: pool created outside threaded region
@with_pool pool begin
Threads.@threads for i in 1:N
a = acquire!(pool, Float64, 100) # race condition!
end
endSee Multi-Threading for more patterns.