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

PatternExampleWhy It Works
Return computed valuesreturn sum(v)Scalar escapes, not the array
Return copiesreturn copy(v)New allocation, independent data
Use within scoperesult = A * BArrays valid during computation

What's Dangerous

PatternExampleWhy It Fails
Return arrayreturn vArray marked for reuse after return
Store in globalglobal_ref = vPoints 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 reuse
Why Undefined Behavior?

After 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
end

Don'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
end

Don'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])
end

Debugging 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
end

acquire! vs unsafe_acquire!

FunctionReturnsBest For
acquire!View types (SubArray, ReshapedArray)General use, BLAS/LAPACK
unsafe_acquire!Native Array/CuArrayFFI, 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
end

See Multi-Threading for more patterns.