Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Common initialization patterns have convenience functions:
| `zeros!(pool, 10)` | `acquire!` + `fill!(0)` |
| `ones!(pool, Float32, 3, 3)` | `acquire!` + `fill!(1)` |
| `similar!(pool, A)` | `acquire!` matching `eltype(A)`, `size(A)` |
| `reshape!(pool, A, 3, 4)` | Reshape sharing memory, zero-alloc (1.11+) |

These return views like `acquire!`. For raw `Array` types, use `unsafe_acquire!` or its convenience variants (`unsafe_zeros!`, `unsafe_ones!`, `unsafe_similar!`). See [API Reference](https://projecttorreypines.github.io/AdaptiveArrayPools.jl/stable/usage/api#convenience-functions).

Expand Down
21 changes: 21 additions & 0 deletions docs/src/basics/api-essentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ Match existing array properties:
end
```

### Reshaping with `reshape!`

Reshape an existing array using the pool's wrapper cache. The result shares memory with the original — mutations are visible in both:

```julia
@with_pool pool function process_grid(data, nx, ny)
M = reshape!(pool, data, nx, ny) # 1D → 2D, shares memory with data
col_sums = zeros!(pool, Float64, ny)
for j in 1:ny, i in 1:nx
col_sums[j] += M[i, j]
end
return sum(col_sums)
end
```

On Julia 1.11+, cross-dimensional reshapes are **zero-allocation** after warmup via `setfield!`-based wrapper reuse. On Julia 1.10, falls back to `Base.reshape`.

!!! warning "DimensionMismatch"
`prod(dims)` must equal `length(A)`, otherwise a `DimensionMismatch` is thrown.

### Custom Initialization with `fill!`

For values other than 0 or 1, use Julia's built-in `fill!`:
Expand Down Expand Up @@ -117,6 +137,7 @@ end
| `zeros!(pool, [T,] dims...)` | View type | 0 bytes | Zero-initialized |
| `ones!(pool, [T,] dims...)` | View type | 0 bytes | One-initialized |
| `similar!(pool, A)` | View type | 0 bytes | Match existing array |
| `reshape!(pool, A, dims...)` | Reshaped array | 0 bytes (1.11+) | Reshape sharing memory |
| `reset!(pool)` | `nothing` | - | Release all memory |
| `pooling_enabled(pool)` | `Bool` | - | Check pool status |

Expand Down
1 change: 1 addition & 0 deletions docs/src/basics/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Common initialization patterns have shortcuts:
| `zeros!(pool, 10)` | `acquire!` + `fill!(0)` |
| `ones!(pool, Float32, 3, 3)` | `acquire!` + `fill!(1)` |
| `similar!(pool, A)` | `acquire!` matching `eltype(A)`, `size(A)` |
| `reshape!(pool, A, 3, 4)` | Reshape sharing memory, zero-alloc (1.11+) |

```julia
@with_pool pool function example(n)
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Default element type is `Float64` (CPU) or `Float32` (CUDA).
| `trues!(pool, dims...)` | Bit-packed `BitVector` / `BitArray{N}` filled with `true`. |
| `falses!(pool, dims...)` | Bit-packed `BitVector` / `BitArray{N}` filled with `false`. |
| `similar!(pool, A)` | View matching `eltype(A)` and `size(A)`. |
| `reshape!(pool, A, dims...)` | Reshape `A` to `dims`, sharing memory. Zero-alloc on Julia 1.11+. |

### Types

Expand Down
2 changes: 1 addition & 1 deletion src/AdaptiveArrayPools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using Printf
# Public API
export AdaptiveArrayPool, acquire!, unsafe_acquire!, pool_stats, get_task_local_pool
export acquire_view!, acquire_array! # Explicit naming aliases
export zeros!, ones!, trues!, falses!, similar!, default_eltype # Convenience functions
export zeros!, ones!, trues!, falses!, similar!, reshape!, default_eltype # Convenience functions
export unsafe_zeros!, unsafe_ones!, unsafe_similar! # Unsafe convenience functions
export Bit # Sentinel type for BitArray (use with acquire!, trues!, falses!)
export @with_pool, @maybe_with_pool
Expand Down
106 changes: 100 additions & 6 deletions src/acquire.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ negligible relative to the 100-200 ns cost of the full allocation path.
return total
end

# ==============================================================================
# Helper: Pool Growth Warning (cold path, kept out of hot loops)
# ==============================================================================

@noinline function _warn_pool_growing(tp::AbstractTypedPool{T}, idx::Int) where {T}
total_bytes = sum(length, tp.vectors) * sizeof(T)
@warn "$(nameof(typeof(tp))){$T} growing large ($idx arrays, ~$(Base.format_bytes(total_bytes))). Missing rewind!()?"
return nothing
end

@inline function _check_pool_growth(tp::AbstractTypedPool, idx::Int)
# Warn at every power of 2 from 512 onward (512, 1024, 2048, …)
if idx >= 512 && (idx & (idx - 1)) == 0
_warn_pool_growing(tp, idx)
end
end

# ==============================================================================
# Get 1D View (Internal - Zero-Allocation Cache)
# ==============================================================================
Expand All @@ -61,12 +78,7 @@ function get_view!(tp::AbstractTypedPool{T}, n::Int) where {T}
new_view = view(tp.vectors[idx], 1:n)
push!(tp.views, new_view)
push!(tp.view_lengths, n)

# Warn at powers of 2 (512, 1024, 2048, ...) - possible missing rewind!()
if idx >= 512 && (idx & (idx - 1)) == 0
total_bytes = sum(length, tp.vectors) * sizeof(T)
@warn "$(nameof(typeof(tp))){$T} growing large ($idx arrays, ~$(Base.format_bytes(total_bytes))). Missing rewind!()?"
end
_check_pool_growth(tp, idx)

return new_view
end
Expand All @@ -90,6 +102,88 @@ function get_view!(tp::AbstractTypedPool{T}, n::Int) where {T}
return new_view
end

# ==============================================================================
# Slot Claim (for reshape! — wrapper-only, no backing memory)
# ==============================================================================

"""
_claim_slot!(tp::TypedPool{T}) -> Int

Claim the next slot index by incrementing `n_active`.
Ensures the slot exists in vectors/views/view_lengths arrays.
The backing vector at this slot is unused — this is for wrapper-only caching
(e.g., `reshape!` uses the slot index for `nd_wrapper` storage only).
"""
@inline function _claim_slot!(tp::TypedPool{T}) where {T}
tp.n_active += 1
idx = tp.n_active
if idx > length(tp.vectors)
push!(tp.vectors, Vector{T}(undef, 0))
push!(tp.views, view(tp.vectors[idx], 1:0))
push!(tp.view_lengths, 0)
_check_pool_growth(tp, idx)
end
return idx
end

# ==============================================================================
# reshape! — Zero-Allocation Reshape (setfield!-based, Julia 1.11+)
# ==============================================================================

"""
_reshape_impl!(pool::AdaptiveArrayPool, A::Array{T,M}, dims::NTuple{N,Int}) -> Array{T,N}

Zero-allocation reshape using `setfield!`-based wrapper reuse (Julia 1.11+).

- **Same dimensionality (M == N)**: `setfield!(A, :size, dims)` — no pool interaction
- **Different dimensionality (M ≠ N)**: Claims a pool slot via `_claim_slot!`,
reuses cached `Array{T,N}` wrapper with `setfield!(:ref, :size)` pointing to `A`'s memory.
Automatically reclaimed on `rewind!` via `n_active` restoration.
"""
@inline function _reshape_impl!(pool::AdaptiveArrayPool, A::Array{T,M}, dims::NTuple{N,Int}) where {T,M,N}
# Reject negative dimensions (match Base.reshape behavior)
for d in dims
d < 0 && throw(ArgumentError("invalid Array dimensions"))
end

# Validate before claiming slot
total_len = safe_prod(dims)
length(A) == total_len || throw(DimensionMismatch(
"new dimensions $(dims) must be consistent with array length $(length(A))"))

# 0-D reshape: rare edge case, delegate to Base (nd_wrappers is 1-indexed by N)
N == 0 && return reshape(A, dims)

# Same dimensionality: just update size in-place, no pool interaction
if M == N
setfield!(A, :size, dims)
return A
end

# Different dimensionality: claim slot + reuse cached N-D wrapper
tp = get_typed_pool!(pool, T)
slot = _claim_slot!(tp)

# Look up cached wrapper (direct index, no hash)
wrappers = N <= length(tp.nd_wrappers) ? (@inbounds tp.nd_wrappers[N]) : nothing
if wrappers !== nothing && slot <= length(wrappers)
wrapper = @inbounds wrappers[slot]
if wrapper !== nothing
arr = wrapper::Array{T,N}
setfield!(arr, :ref, getfield(A, :ref))
setfield!(arr, :size, dims)
return arr
end
end

# Cache miss (first call per slot+N): create wrapper, cache forever
arr = Array{T,N}(undef, ntuple(_ -> 0, Val(N)))
setfield!(arr, :ref, getfield(A, :ref))
setfield!(arr, :size, dims)
_store_nd_wrapper!(tp, N, slot, arr)
return arr
end

# ==============================================================================
# Get N-D Array (setfield!-based Wrapper Reuse, Julia 1.11+)
# ==============================================================================
Expand Down
69 changes: 69 additions & 0 deletions src/convenience.jl
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,67 @@ end
_acquire_impl!(pool, T, dims...)
end

# ==============================================================================
# reshape! - Reshape arrays using pool's wrapper cache
# ==============================================================================

"""
reshape!(pool, A, dims...) -> reshaped array
reshape!(pool, A, dims::Tuple) -> reshaped array

Reshape array `A` to dimensions `dims` using the pool's wrapper cache.

The returned array shares memory with `A` — mutations are visible in both.
The pool provides cached wrapper objects to reduce allocation on repeated calls.

On Julia 1.11+:
- If `ndims(A) == length(dims)` (same dimensionality), `reshape!` mutates `A`
in-place by changing its size. This differs from `Base.reshape`, which always
returns a new wrapper.
- For cross-dimensional reshapes (`ndims(A) != length(dims)`), the returned
`Array` wrapper is taken from the pool's internal cache and may be reused
after `rewind!` or pool scope exit.

As with all pool-backed objects, the reshaped result must not escape the
surrounding `@with_pool` scope.

On Julia 1.10 and CUDA, falls back to `Base.reshape`.

Throws `DimensionMismatch` if `prod(dims) != length(A)`.

## Example
```julia
A = collect(1.0:12.0)
@with_pool pool begin
B = reshape!(pool, A, 3, 4) # 12-element vector → 3×4 matrix
B[1,1] = 999.0 # A[1] is now 999.0
end
```

See also: [`acquire!`](@ref), [`similar!`](@ref)
"""
@inline function reshape!(pool::AbstractArrayPool, A::AbstractArray{T}, dims::Vararg{Int,N}) where {T,N}
_record_type_touch!(pool, T)
_reshape_impl!(pool, A, dims)
end

@inline function reshape!(pool::AbstractArrayPool, A::AbstractArray{T}, dims::NTuple{N,Int}) where {T,N}
_record_type_touch!(pool, T)
_reshape_impl!(pool, A, dims)
end

# Internal implementation (fallback: delegates to Base.reshape)
@inline function _reshape_impl!(::AbstractArrayPool, A::AbstractArray, dims::NTuple{N,Int}) where {N}
for d in dims
d < 0 && throw(ArgumentError("invalid Array dimensions"))
end
reshape(A, dims)
end

# Vararg forwarding (macro transforms reshape!(pool, A, 3, 4) → _reshape_impl!(pool, A, 3, 4))
@inline _reshape_impl!(pool::AbstractArrayPool, A::AbstractArray, dims::Vararg{Int,N}) where {N} =
_reshape_impl!(pool, A, dims)

# ==============================================================================
# unsafe_zeros! - Acquire zero-initialized raw arrays from pool
# ==============================================================================
Expand Down Expand Up @@ -587,6 +648,10 @@ end
@inline similar!(::DisabledPool{:cpu}, x::AbstractArray, dims::Vararg{Int,N}) where {N} = similar(x, dims...)
@inline similar!(::DisabledPool{:cpu}, x::AbstractArray, ::Type{T}, dims::Vararg{Int,N}) where {T,N} = similar(x, T, dims...)

# --- reshape! for DisabledPool{:cpu} ---
@inline reshape!(::DisabledPool{:cpu}, A::AbstractArray, dims::Vararg{Int,N}) where {N} = reshape(A, dims...)
@inline reshape!(::DisabledPool{:cpu}, A::AbstractArray, dims::NTuple{N,Int}) where {N} = reshape(A, dims)

# --- unsafe_zeros! for DisabledPool{:cpu} ---
@inline unsafe_zeros!(::DisabledPool{:cpu}, ::Type{T}, dims::Vararg{Int,N}) where {T,N} = zeros(T, dims...)
@inline unsafe_zeros!(p::DisabledPool{:cpu}, dims::Vararg{Int,N}) where {N} = zeros(default_eltype(p), dims...)
Expand Down Expand Up @@ -614,6 +679,7 @@ end
@inline unsafe_zeros!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B)
@inline unsafe_ones!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B)
@inline unsafe_similar!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B)
@inline reshape!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B)

# ==============================================================================
# _impl! Delegators for DisabledPool
Expand Down Expand Up @@ -650,6 +716,9 @@ end
@inline _similar_impl!(p::DisabledPool, x::AbstractArray, dims::Vararg{Int,N}) where {N} = similar!(p, x, dims...)
@inline _similar_impl!(p::DisabledPool, x::AbstractArray, ::Type{T}, dims::Vararg{Int,N}) where {T,N} = similar!(p, x, T, dims...)

# --- _reshape_impl! ---
@inline _reshape_impl!(p::DisabledPool, A::AbstractArray, dims::NTuple{N,Int}) where {N} = reshape!(p, A, dims)

# --- _unsafe_zeros_impl! ---
@inline _unsafe_zeros_impl!(p::DisabledPool, ::Type{T}, dims::Vararg{Int,N}) where {T,N} = unsafe_zeros!(p, T, dims...)
@inline _unsafe_zeros_impl!(p::DisabledPool, dims::Vararg{Int,N}) where {N} = unsafe_zeros!(p, dims...)
Expand Down
11 changes: 11 additions & 0 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,12 @@ function _extract_acquire_types(expr, target_pool, types=Set{Any}())
push!(types, Expr(:call, :eltype, expr.args[3]))
end
end
# reshape!
elseif fn in (:reshape!,) || fn_name in (:reshape!,)
# reshape!(pool, A, dims...) — extract eltype(A) from second arg
if nargs >= 3
push!(types, Expr(:call, :eltype, expr.args[3]))
end
end
end
end
Expand Down Expand Up @@ -1015,6 +1021,7 @@ const _SIMILAR_IMPL_REF = GlobalRef(@__MODULE__, :_similar_impl!)
const _UNSAFE_ZEROS_IMPL_REF = GlobalRef(@__MODULE__, :_unsafe_zeros_impl!)
const _UNSAFE_ONES_IMPL_REF = GlobalRef(@__MODULE__, :_unsafe_ones_impl!)
const _UNSAFE_SIMILAR_IMPL_REF = GlobalRef(@__MODULE__, :_unsafe_similar_impl!)
const _RESHAPE_IMPL_REF = GlobalRef(@__MODULE__, :_reshape_impl!)

function _transform_acquire_calls(expr, pool_name)
if expr isa Expr
Expand All @@ -1040,6 +1047,8 @@ function _transform_acquire_calls(expr, pool_name)
expr = Expr(:call, _FALSES_IMPL_REF, expr.args[2:end]...)
elseif fn == :similar!
expr = Expr(:call, _SIMILAR_IMPL_REF, expr.args[2:end]...)
elseif fn == :reshape!
expr = Expr(:call, _RESHAPE_IMPL_REF, expr.args[2:end]...)
elseif fn == :unsafe_zeros!
expr = Expr(:call, _UNSAFE_ZEROS_IMPL_REF, expr.args[2:end]...)
elseif fn == :unsafe_ones!
Expand All @@ -1063,6 +1072,8 @@ function _transform_acquire_calls(expr, pool_name)
expr = Expr(:call, _FALSES_IMPL_REF, expr.args[2:end]...)
elseif qn == QuoteNode(:similar!)
expr = Expr(:call, _SIMILAR_IMPL_REF, expr.args[2:end]...)
elseif qn == QuoteNode(:reshape!)
expr = Expr(:call, _RESHAPE_IMPL_REF, expr.args[2:end]...)
elseif qn == QuoteNode(:unsafe_zeros!)
expr = Expr(:call, _UNSAFE_ZEROS_IMPL_REF, expr.args[2:end]...)
elseif qn == QuoteNode(:unsafe_ones!)
Expand Down
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ else
include("test_fixed_slots.jl")
include("test_backend_macro_expansion.jl")
include("test_convenience.jl")
include("test_reshape.jl")
include("test_bitarray.jl")
include("test_coverage.jl")
include("test_allocation.jl")
Expand All @@ -56,6 +57,7 @@ else
include("test_fixed_slots.jl")
include("test_backend_macro_expansion.jl")
include("test_convenience.jl")
include("test_reshape.jl")
include("test_bitarray.jl")
include("test_coverage.jl")
include("test_allocation.jl")
Expand Down
Loading