diff --git a/docs/make.jl b/docs/make.jl index d441c21..69ba0af 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -93,6 +93,7 @@ makedocs( ], "Features" => [ "`@maybe_with_pool`" => "features/maybe-with-pool.md", + "Bit Arrays" => "features/bit-arrays.md", "CUDA Support" => "features/cuda-support.md", "Configuration" => "features/configuration.md", ], diff --git a/docs/src/features/bit-arrays.md b/docs/src/features/bit-arrays.md new file mode 100644 index 0000000..ec21d39 --- /dev/null +++ b/docs/src/features/bit-arrays.md @@ -0,0 +1,78 @@ +# BitVector Support + +AdaptiveArrayPools.jl includes specialized support for `BitArray` (specifically `BitVector`), enabling **~8x memory savings** for boolean arrays compared to standard `Vector{Bool}`. + +## The `Bit` Sentinel Type + +To distinguish between standard boolean arrays (`Vector{Bool}`, 1 byte/element) and bit-packed arrays (`BitVector`, 1 bit/element), use the `Bit` sentinel type. + +| Call | Result | Memory | +|------|--------|--------| +| `acquire!(pool, Bool, 1000)` | `Vector{Bool}` | 1000 bytes | +| `acquire!(pool, Bit, 1000)` | `BitVector` | ~125 bytes | + +## Usage + +### 1D Arrays (BitVector) +For 1D arrays, `acquire!` returns a view into a pooled `BitVector`. + +```julia +@with_pool pool begin + # Acquire a BitVector of length 1000 + bv = acquire!(pool, Bit, 1000) + + # Use like normal + bv .= true + bv[1] = false + + # Supports standard operations + count(bv) +end +``` + +### N-D Arrays (BitArray / Reshaped) +For multi-dimensional arrays, `acquire!` returns a `ReshapedArray` wrapper around the linear `BitVector`. This maintains zero-allocation efficiency while providing N-D indexing. + +```julia +@with_pool pool begin + # 100x100 bit matrix + mask = zeros!(pool, Bit, 100, 100) + + mask[5, 5] = true +end +``` + +### Convenience Functions + +For specific `BitVector` operations, prefer `trues!` and `falses!` which mirror Julia's standard functions: + +```julia +@with_pool pool begin + # Filled with false (equivalent to `falses(256)`) + mask = falses!(pool, 256) + + # Filled with true (equivalent to `trues(256)`) + flags = trues!(pool, 256) + + # Multidimensional + grid = trues!(pool, 100, 100) + + # Similar to existing BitArray + A = BitVector(undef, 50) + B = similar!(pool, A) # Reuses eltype(A) -> Bool + + # To explicit get Bit-packed from pool irrespective of source + C = similar!(pool, A, Bit) +end + +Note: `zeros!(pool, Bit, ...)` and `ones!(pool, Bit, ...)` are also supported (aliased to `falses!` and `trues!`). +``` + +## How It Works + +The pool maintains a separate `BitTypedPool` specifically for `BitVector` storage. +- **Sentinel**: `acquire!(..., Bit, ...)` dispatches to this special pool. +- **Views**: 1D returns `SubArray{Bool, 1, BitVector, ...}`. +- **Reshaping**: N-D returns `ReshapedArray{Bool, N, SubArray{...}}`. + +This ensures that even for complex shapes, the underlying storage is always a compact `BitVector` reused from the pool. diff --git a/src/AdaptiveArrayPools.jl b/src/AdaptiveArrayPools.jl index b28aa2e..61f691a 100644 --- a/src/AdaptiveArrayPools.jl +++ b/src/AdaptiveArrayPools.jl @@ -5,8 +5,9 @@ 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!, similar!, default_eltype # Convenience functions +export zeros!, ones!, trues!, falses!, similar!, 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 export USE_POOLING, MAYBE_POOLING_ENABLED, POOL_DEBUG export checkpoint!, rewind!, reset! diff --git a/src/acquire.jl b/src/acquire.jl index 98c2298..b8ddcf6 100644 --- a/src/acquire.jl +++ b/src/acquire.jl @@ -6,12 +6,29 @@ @inline allocate_vector(::AbstractTypedPool{T,Vector{T}}, n::Int) where {T} = Vector{T}(undef, n) +# BitTypedPool allocates BitVector (used when acquiring with Bit type) +@inline allocate_vector(::BitTypedPool, n::Int) = BitVector(undef, n) + +# Bit type returns Bool element type for fill operations (zero/one) +@inline Base.zero(::Type{Bit}) = false +@inline Base.one(::Type{Bit}) = true + # Wrap flat view into N-D array (dispatch point for extensions) @inline function wrap_array(::AbstractTypedPool{T,Vector{T}}, flat_view, dims::NTuple{N,Int}) where {T,N} unsafe_wrap(Array{T,N}, pointer(flat_view), dims) end +# BitTypedPool cannot use unsafe_wrap - throw clear error +# Called from _unsafe_acquire_impl! dispatches for Bit type +@noinline function _throw_bit_unsafe_error() + throw(ArgumentError( + "unsafe_acquire!(pool, Bit, ...) is not supported. " * + "BitArray stores data in immutable chunks::Vector{UInt64} that cannot be wrapped with unsafe_wrap. " * + "Use acquire!(pool, Bit, ...) instead, which returns a view." + )) +end + # ============================================================================== # Helper: Overflow-Safe Product # ============================================================================== @@ -228,6 +245,11 @@ end # Similar-style @inline _unsafe_acquire_impl!(pool::AbstractArrayPool, x::AbstractArray) = _unsafe_acquire_impl!(pool, eltype(x), size(x)) +# Bit type: unsafe_acquire! not supported (throw clear error early) +@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::Int) = _throw_bit_unsafe_error() +@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::Vararg{Int,N}) where {N} = _throw_bit_unsafe_error() +@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::NTuple{N,Int}) where {N} = _throw_bit_unsafe_error() + # ============================================================================== # Acquisition API (User-facing with untracked marking) # ============================================================================== @@ -428,9 +450,14 @@ const _acquire_array_impl! = _unsafe_acquire_impl! @inline unsafe_acquire!(::DisabledPool{:cpu}, ::Type{T}, dims::NTuple{N,Int}) where {T,N} = Array{T,N}(undef, dims) @inline unsafe_acquire!(::DisabledPool{:cpu}, x::AbstractArray) = similar(x) +# --- acquire! for DisabledPool{:cpu} with Bit type (returns BitArray) --- +@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, n::Int) = BitVector(undef, n) +@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = BitArray{N}(undef, dims) +@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = BitArray{N}(undef, dims) + # --- Generic DisabledPool fallbacks (unknown backend → error) --- -@inline acquire!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) -@inline unsafe_acquire!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) +@inline acquire!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B) +@inline unsafe_acquire!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B) # --- _impl! delegators for DisabledPool (macro transformation support) --- # Called when: USE_POOLING=true + @maybe_with_pool + MAYBE_POOLING_ENABLED[]=false diff --git a/src/convenience.jl b/src/convenience.jl index 76f615c..a8dd0ca 100644 --- a/src/convenience.jl +++ b/src/convenience.jl @@ -83,6 +83,12 @@ end _zeros_impl!(pool, default_eltype(pool), dims...) end +# Bit type specialization: zeros!(pool, Bit, ...) delegates to falses!(pool, ...) +@inline zeros!(pool::AbstractArrayPool, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = falses!(pool, dims...) +@inline zeros!(pool::AbstractArrayPool, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = falses!(pool, dims) +@inline _zeros_impl!(pool::AbstractArrayPool, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = _falses_impl!(pool, dims...) +@inline _zeros_impl!(pool::AbstractArrayPool, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = _falses_impl!(pool, dims) + # ============================================================================== # ones! - Acquire one-initialized arrays from pool # ============================================================================== @@ -150,6 +156,92 @@ end _ones_impl!(pool, default_eltype(pool), dims...) end +# Bit type specialization: ones!(pool, Bit, ...) delegates to trues!(pool, ...) +@inline ones!(pool::AbstractArrayPool, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = trues!(pool, dims...) +@inline ones!(pool::AbstractArrayPool, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = trues!(pool, dims) +@inline _ones_impl!(pool::AbstractArrayPool, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = _trues_impl!(pool, dims...) +@inline _ones_impl!(pool::AbstractArrayPool, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = _trues_impl!(pool, dims) + +# ============================================================================== +# trues! - Acquire BitArray filled with true from pool +# ============================================================================== + +""" + trues!(pool, dims...) -> BitArray view + trues!(pool, dims::Tuple) -> BitArray view + +Acquire a bit-packed boolean array filled with `true` from the pool. + +Equivalent to Julia's `trues(dims...)` but using pooled memory. +Uses ~8x less memory than `ones!(pool, Bool, dims...)`. + +## Example +```julia +@with_pool pool begin + bv = trues!(pool, 100) # BitVector-backed view, all true + bm = trues!(pool, 10, 10) # BitMatrix-backed reshaped array +end +``` + +See also: [`falses!`](@ref), [`ones!`](@ref), [`acquire!`](@ref) +""" +@inline function trues!(pool::AbstractArrayPool, dims::Vararg{Int,N}) where {N} + _mark_untracked!(pool) + _trues_impl!(pool, dims...) +end +@inline function trues!(pool::AbstractArrayPool, dims::NTuple{N,Int}) where {N} + _mark_untracked!(pool) + _trues_impl!(pool, dims...) +end + +# Internal implementation (for macro transformation) +@inline function _trues_impl!(pool::AbstractArrayPool, dims::Vararg{Int,N}) where {N} + arr = _acquire_impl!(pool, Bit, dims...) + fill!(arr, true) + arr +end +@inline _trues_impl!(pool::AbstractArrayPool, dims::NTuple{N,Int}) where {N} = _trues_impl!(pool, dims...) + +# ============================================================================== +# falses! - Acquire BitArray filled with false from pool +# ============================================================================== + +""" + falses!(pool, dims...) -> BitArray view + falses!(pool, dims::Tuple) -> BitArray view + +Acquire a bit-packed boolean array filled with `false` from the pool. + +Equivalent to Julia's `falses(dims...)` but using pooled memory. +Uses ~8x less memory than `zeros!(pool, Bool, dims...)`. + +## Example +```julia +@with_pool pool begin + bv = falses!(pool, 100) # BitVector-backed view, all false + bm = falses!(pool, 10, 10) # BitMatrix-backed reshaped array +end +``` + +See also: [`trues!`](@ref), [`zeros!`](@ref), [`acquire!`](@ref) +""" +@inline function falses!(pool::AbstractArrayPool, dims::Vararg{Int,N}) where {N} + _mark_untracked!(pool) + _falses_impl!(pool, dims...) +end +@inline function falses!(pool::AbstractArrayPool, dims::NTuple{N,Int}) where {N} + _mark_untracked!(pool) + _falses_impl!(pool, dims...) +end + +# Internal implementation (for macro transformation) +@inline function _falses_impl!(pool::AbstractArrayPool, dims::Vararg{Int,N}) where {N} + arr = _acquire_impl!(pool, Bit, dims...) + fill!(arr, false) + arr +end +@inline _falses_impl!(pool::AbstractArrayPool, dims::NTuple{N,Int}) where {N} = _falses_impl!(pool, dims...) + # ============================================================================== # similar! - Acquire arrays with same type/size as template # ============================================================================== @@ -477,6 +569,18 @@ end @inline ones!(::DisabledPool{:cpu}, ::Type{T}, dims::NTuple{N,Int}) where {T,N} = ones(T, dims...) @inline ones!(p::DisabledPool{:cpu}, dims::NTuple{N,Int}) where {N} = ones(default_eltype(p), dims...) +# --- zeros!/ones! for DisabledPool{:cpu} with Bit type (returns BitArray) --- +@inline zeros!(::DisabledPool{:cpu}, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = falses(dims...) +@inline zeros!(::DisabledPool{:cpu}, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = falses(dims...) +@inline ones!(::DisabledPool{:cpu}, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = trues(dims...) +@inline ones!(::DisabledPool{:cpu}, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = trues(dims...) + +# --- trues!/falses! for DisabledPool{:cpu} --- +@inline trues!(::DisabledPool{:cpu}, dims::Vararg{Int,N}) where {N} = trues(dims...) +@inline trues!(::DisabledPool{:cpu}, dims::NTuple{N,Int}) where {N} = trues(dims...) +@inline falses!(::DisabledPool{:cpu}, dims::Vararg{Int,N}) where {N} = falses(dims...) +@inline falses!(::DisabledPool{:cpu}, dims::NTuple{N,Int}) where {N} = falses(dims...) + # --- similar! for DisabledPool{:cpu} --- @inline similar!(::DisabledPool{:cpu}, x::AbstractArray) = similar(x) @inline similar!(::DisabledPool{:cpu}, x::AbstractArray, ::Type{T}) where {T} = similar(x, T) @@ -504,6 +608,8 @@ end # --- Generic DisabledPool fallbacks (unknown backend → error) --- @inline zeros!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) @inline ones!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) +@inline trues!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) +@inline falses!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) @inline similar!(p::DisabledPool{B}, args...) where {B} = _throw_backend_not_loaded(B) @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) @@ -530,6 +636,14 @@ end @inline _ones_impl!(p::DisabledPool, ::Type{T}, dims::NTuple{N,Int}) where {T,N} = ones!(p, T, dims) @inline _ones_impl!(p::DisabledPool, dims::NTuple{N,Int}) where {N} = ones!(p, dims) +# --- _trues_impl! --- +@inline _trues_impl!(p::DisabledPool, dims::Vararg{Int,N}) where {N} = trues!(p, dims...) +@inline _trues_impl!(p::DisabledPool, dims::NTuple{N,Int}) where {N} = trues!(p, dims) + +# --- _falses_impl! --- +@inline _falses_impl!(p::DisabledPool, dims::Vararg{Int,N}) where {N} = falses!(p, dims...) +@inline _falses_impl!(p::DisabledPool, dims::NTuple{N,Int}) where {N} = falses!(p, dims) + # --- _similar_impl! --- @inline _similar_impl!(p::DisabledPool, x::AbstractArray) = similar!(p, x) @inline _similar_impl!(p::DisabledPool, x::AbstractArray, ::Type{T}) where {T} = similar!(p, x, T) diff --git a/src/macros.jl b/src/macros.jl index b0a0425..d79a65e 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -826,6 +826,9 @@ function _extract_acquire_types(expr, target_pool, types=Set{Any}()) # acquire!(pool, x) - similar-style form push!(types, Expr(:call, :eltype, expr.args[3])) end + # trues!/falses! (always uses Bit type) + elseif fn in (:trues!, :falses!) || fn_name in (:trues!, :falses!) + push!(types, :Bit) # zeros!/ones!/unsafe_zeros!/unsafe_ones! elseif fn in (:zeros!, :ones!, :unsafe_zeros!, :unsafe_ones!) || fn_name in (:zeros!, :ones!, :unsafe_zeros!, :unsafe_ones!) if nargs >= 3 @@ -1034,6 +1037,8 @@ const _ACQUIRE_IMPL_REF = GlobalRef(@__MODULE__, :_acquire_impl!) const _UNSAFE_ACQUIRE_IMPL_REF = GlobalRef(@__MODULE__, :_unsafe_acquire_impl!) const _ZEROS_IMPL_REF = GlobalRef(@__MODULE__, :_zeros_impl!) const _ONES_IMPL_REF = GlobalRef(@__MODULE__, :_ones_impl!) +const _TRUES_IMPL_REF = GlobalRef(@__MODULE__, :_trues_impl!) +const _FALSES_IMPL_REF = GlobalRef(@__MODULE__, :_falses_impl!) 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!) @@ -1057,6 +1062,10 @@ function _transform_acquire_calls(expr, pool_name) expr = Expr(:call, _ZEROS_IMPL_REF, expr.args[2:end]...) elseif fn == :ones! expr = Expr(:call, _ONES_IMPL_REF, expr.args[2:end]...) + elseif fn == :trues! + expr = Expr(:call, _TRUES_IMPL_REF, expr.args[2:end]...) + elseif fn == :falses! + 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 == :unsafe_zeros! @@ -1076,6 +1085,10 @@ function _transform_acquire_calls(expr, pool_name) expr = Expr(:call, _ZEROS_IMPL_REF, expr.args[2:end]...) elseif qn == QuoteNode(:ones!) expr = Expr(:call, _ONES_IMPL_REF, expr.args[2:end]...) + elseif qn == QuoteNode(:trues!) + expr = Expr(:call, _TRUES_IMPL_REF, expr.args[2:end]...) + elseif qn == QuoteNode(:falses!) + 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(:unsafe_zeros!) diff --git a/src/types.jl b/src/types.jl index 9a3ffc7..2b1a070 100644 --- a/src/types.jl +++ b/src/types.jl @@ -210,6 +210,122 @@ TypedPool{T}() where {T} = TypedPool{T}( [0] # _checkpoint_depths: sentinel (depth=0 = no checkpoint) ) +# ============================================================================== +# Bit Sentinel Type +# ============================================================================== + +""" + Bit + +Sentinel type for bit-packed boolean storage via `BitVector`. + +Use `Bit` instead of `Bool` in pool operations to get memory-efficient +bit-packed arrays (1 bit per element vs 1 byte for `Vector{Bool}`). + +## Usage +```julia +@with_pool pool begin + # BitVector view (1 bit per element, ~8x memory savings) + bv = acquire!(pool, Bit, 1000) + + # vs Vector{Bool} (1 byte per element) + vb = acquire!(pool, Bool, 1000) + + # Convenience functions work too + mask = zeros!(pool, Bit, 100) # BitVector filled with false + flags = ones!(pool, Bit, 100) # BitVector filled with true +end +``` + +## Return Types +- **1D**: `SubArray{Bool,1,BitVector,...}` +- **N-D**: `ReshapedArray{Bool,N,...}` (reshaped view of 1D BitVector) + +## Limitation +`unsafe_acquire!(pool, Bit, ...)` is **not supported** because Julia's +`BitArray` stores data in immutable `chunks::Vector{UInt64}` that cannot +be wrapped with `unsafe_wrap`. + +See also: [`acquire!`](@ref), [`BitTypedPool`](@ref) +""" +struct Bit end + +# ============================================================================== +# BitTypedPool - Specialized pool for BitVector/BitArray +# ============================================================================== + +""" + BitTypedPool <: AbstractTypedPool{Bool, BitVector} + +Specialized pool for `BitVector` arrays with memory reuse. + +Unlike `TypedPool{Bool}` which stores `Vector{Bool}` (1 byte per element), +this pool stores `BitVector` (1 bit per element, ~8x memory efficiency). + +## Important Limitation +**`unsafe_acquire!` is NOT supported for BitArray** because Julia's `BitArray` +stores data in a `chunks::Vector{UInt64}` field that cannot be wrapped with +`unsafe_wrap`. Only view-based acquisition via `acquire!(pool, Bit, ...)` is available. + +## Fields +- `vectors`: Backing `BitVector` storage +- `views`: Cached `SubArray` views for zero-allocation 1D access +- `view_lengths`: Cached lengths for fast comparison +- `nd_*`: Empty N-D cache fields (for `empty!` compatibility, unused) +- `n_active`: Count of currently active arrays +- `_checkpoint_*`: State management stacks (1-based sentinel pattern) + +## Usage +```julia +@with_pool pool begin + bv = acquire!(pool, Bit, 100) # SubArray{Bool,1,BitVector,...} + ba = acquire!(pool, Bit, 10, 10) # ReshapedArray{Bool,2,...} + t = trues!(pool, 50) # Filled with true + f = falses!(pool, 50) # Filled with false +end +``` + +See also: [`trues!`](@ref), [`falses!`](@ref) +""" +mutable struct BitTypedPool <: AbstractTypedPool{Bool, BitVector} + # --- Storage --- + vectors::Vector{BitVector} + + # --- 1D Cache (1:1 mapping) --- + views::Vector{SubArray{Bool, 1, BitVector, Tuple{UnitRange{Int64}}, true}} + view_lengths::Vector{Int} + + # --- N-D Array Cache (empty, for empty! compatibility) --- + # BitArray cannot use unsafe_wrap, so no N-D caching is possible. + # These fields exist only for compatibility with empty!(::AbstractTypedPool). + nd_arrays::Vector{Any} + nd_dims::Vector{Any} + nd_ptrs::Vector{UInt} + nd_next_way::Vector{Int} + + # --- State Management (1-based sentinel pattern) --- + n_active::Int + _checkpoint_n_active::Vector{Int} + _checkpoint_depths::Vector{Int} +end + +BitTypedPool() = BitTypedPool( + # Storage + BitVector[], + # 1D Cache + SubArray{Bool, 1, BitVector, Tuple{UnitRange{Int64}}, true}[], + Int[], + # N-D Array Cache (empty, for compatibility) + Any[], + Any[], + UInt[], + Int[], + # State Management (1-based sentinel pattern) + 0, # n_active + [0], # _checkpoint_n_active: sentinel + [0] # _checkpoint_depths: sentinel +) + # ============================================================================== # Fixed Slot Configuration # ============================================================================== @@ -222,7 +338,7 @@ Field names for fixed slot TypedPools. Single source of truth for `foreach_fixed When modifying, also update: struct definition, `get_typed_pool!` dispatches, constructor. Tests verify synchronization automatically. """ -const FIXED_SLOT_FIELDS = (:float64, :float32, :int64, :int32, :complexf64, :complexf32, :bool) +const FIXED_SLOT_FIELDS = (:float64, :float32, :int64, :int32, :complexf64, :complexf32, :bool, :bits) # ============================================================================== # AdaptiveArrayPool @@ -243,6 +359,7 @@ mutable struct AdaptiveArrayPool <: AbstractArrayPool complexf64::TypedPool{ComplexF64} complexf32::TypedPool{ComplexF32} bool::TypedPool{Bool} + bits::BitTypedPool # BitVector pool (1 bit per element) # Fallback: rare types others::IdDict{DataType, Any} @@ -261,6 +378,7 @@ function AdaptiveArrayPool() TypedPool{ComplexF64}(), TypedPool{ComplexF32}(), TypedPool{Bool}(), + BitTypedPool(), IdDict{DataType, Any}(), 1, # _current_depth: 1 = global scope (sentinel) [false] # _untracked_flags: sentinel for global scope @@ -279,6 +397,7 @@ end @inline get_typed_pool!(p::AdaptiveArrayPool, ::Type{ComplexF64}) = p.complexf64 @inline get_typed_pool!(p::AdaptiveArrayPool, ::Type{ComplexF32}) = p.complexf32 @inline get_typed_pool!(p::AdaptiveArrayPool, ::Type{Bool}) = p.bool +@inline get_typed_pool!(p::AdaptiveArrayPool, ::Type{Bit}) = p.bits # Slow Path: rare types via IdDict @inline function get_typed_pool!(p::AdaptiveArrayPool, ::Type{T}) where {T} diff --git a/test/runtests.jl b/test/runtests.jl index 8ac91c8..c4417de 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -25,6 +25,7 @@ else include("test_fixed_slots.jl") include("test_backend_macro_expansion.jl") include("test_convenience.jl") + include("test_bitarray.jl") include("test_coverage.jl") # CUDA extension tests (auto-detect, skip with TEST_CUDA=false) diff --git a/test/test_bitarray.jl b/test/test_bitarray.jl new file mode 100644 index 0000000..a5dbeca --- /dev/null +++ b/test/test_bitarray.jl @@ -0,0 +1,573 @@ +@testset "BitArray Support" begin + + @testset "Bit sentinel type" begin + # Bit is exported and usable + @test Bit isa DataType + + # zero(Bit) and one(Bit) are defined for fill operations + # These are used by zeros!(pool, Bit, ...) and ones!(pool, Bit, ...) + @test zero(Bit) === false + @test one(Bit) === true + + # Verify these work with fill! + bv = BitVector(undef, 10) + fill!(bv, zero(Bit)) + @test !any(bv) + fill!(bv, one(Bit)) + @test all(bv) + end + + @testset "BitTypedPool structure" begin + pool = AdaptiveArrayPool() + + # Verify bits field exists and is correctly typed + @test pool.bits isa AdaptiveArrayPools.BitTypedPool + @test pool.bits.n_active == 0 + @test isempty(pool.bits.vectors) + end + + @testset "acquire!(pool, Bit, n) - 1D" begin + pool = AdaptiveArrayPool() + + bv = acquire!(pool, Bit, 100) + @test length(bv) == 100 + @test eltype(bv) == Bool + @test bv isa SubArray{Bool, 1, BitVector} + @test pool.bits.n_active == 1 + + # Write and read back + bv .= true + @test all(bv) + bv[50] = false + @test !bv[50] + @test count(bv) == 99 + + # Second acquire + bv2 = acquire!(pool, Bit, 50) + @test length(bv2) == 50 + @test pool.bits.n_active == 2 + + # Independent values + bv2 .= false + @test !any(bv2) + @test count(bv) == 99 # bv unchanged + end + + @testset "acquire!(pool, Bit, dims...) - N-D" begin + pool = AdaptiveArrayPool() + + # 2D + ba2 = acquire!(pool, Bit, 10, 10) + @test size(ba2) == (10, 10) + @test eltype(ba2) == Bool + @test ba2 isa Base.ReshapedArray + @test pool.bits.n_active == 1 + + # Test indexing + ba2 .= false + ba2[1, 1] = true + ba2[5, 5] = true + @test count(ba2) == 2 + @test ba2[1, 1] + @test ba2[5, 5] + @test !ba2[2, 2] + + # 3D + ba3 = acquire!(pool, Bit, 4, 5, 3) + @test size(ba3) == (4, 5, 3) + @test pool.bits.n_active == 2 + + # Tuple form + ba_tuple = acquire!(pool, Bit, (3, 4, 2)) + @test size(ba_tuple) == (3, 4, 2) + @test pool.bits.n_active == 3 + end + + @testset "ones!(pool, Bit, dims...) - filled with true" begin + pool = AdaptiveArrayPool() + + # 1D + t1 = ones!(pool, Bit, 100) + @test length(t1) == 100 + @test all(t1) + @test pool.bits.n_active == 1 + + # 2D + t2 = ones!(pool, Bit, 10, 10) + @test size(t2) == (10, 10) + @test all(t2) + @test count(t2) == 100 + + # Tuple form + t3 = ones!(pool, Bit, (5, 5, 4)) + @test size(t3) == (5, 5, 4) + @test all(t3) + end + + @testset "zeros!(pool, Bit, dims...) - filled with false" begin + pool = AdaptiveArrayPool() + + # 1D + f1 = zeros!(pool, Bit, 100) + @test length(f1) == 100 + @test !any(f1) + @test pool.bits.n_active == 1 + + # 2D + f2 = zeros!(pool, Bit, 10, 10) + @test size(f2) == (10, 10) + @test !any(f2) + @test count(f2) == 0 + + # Tuple form + f3 = zeros!(pool, Bit, (5, 5, 4)) + @test size(f3) == (5, 5, 4) + @test !any(f3) + end + + @testset "trues!(pool, dims...) - convenience for BitArray filled with true" begin + pool = AdaptiveArrayPool() + + # 1D + t1 = trues!(pool, 100) + @test length(t1) == 100 + @test all(t1) + @test eltype(t1) == Bool + @test pool.bits.n_active == 1 + + # 2D + t2 = trues!(pool, 10, 10) + @test size(t2) == (10, 10) + @test all(t2) + @test count(t2) == 100 + + # Tuple form + t3 = trues!(pool, (5, 5, 4)) + @test size(t3) == (5, 5, 4) + @test all(t3) + + # Equivalent to ones!(pool, Bit, ...) + t4 = trues!(pool, 50) + t5 = ones!(pool, Bit, 50) + @test all(t4 .== t5) + end + + @testset "falses!(pool, dims...) - convenience for BitArray filled with false" begin + pool = AdaptiveArrayPool() + + # 1D + f1 = falses!(pool, 100) + @test length(f1) == 100 + @test !any(f1) + @test eltype(f1) == Bool + @test pool.bits.n_active == 1 + + # 2D + f2 = falses!(pool, 10, 10) + @test size(f2) == (10, 10) + @test !any(f2) + @test count(f2) == 0 + + # Tuple form + f3 = falses!(pool, (5, 5, 4)) + @test size(f3) == (5, 5, 4) + @test !any(f3) + + # Equivalent to zeros!(pool, Bit, ...) + f4 = falses!(pool, 50) + f5 = zeros!(pool, Bit, 50) + @test all(f4 .== f5) + end + + @testset "State management" begin + # Use @with_pool which manages checkpoint/rewind automatically + @with_pool outer_pool begin + bv1 = acquire!(outer_pool, Bit, 100) + parent1 = parent(bv1) + + @test outer_pool.bits.n_active == 1 + + @with_pool inner_pool begin + bv2 = acquire!(inner_pool, Bit, 200) + @test inner_pool.bits.n_active == 2 + end + # After inner scope rewind + @test outer_pool.bits.n_active == 1 + + # bv1 should still be valid (same parent BitVector object) + bv3 = acquire!(outer_pool, Bit, 150) + @test parent(bv1) === parent1 # Same object identity + end + # Pool goes back to task-local state after scope ends + end + + @testset "checkpoint!/rewind! integration" begin + pool = AdaptiveArrayPool() + + checkpoint!(pool) + @test pool.bits.n_active == 0 + + bv1 = acquire!(pool, Bit, 100) + t1 = ones!(pool, Bit, 50) + f1 = zeros!(pool, Bit, 50) + @test pool.bits.n_active == 3 + + rewind!(pool) + @test pool.bits.n_active == 0 + end + + @testset "reset! and empty!" begin + pool = AdaptiveArrayPool() + + bv1 = acquire!(pool, Bit, 100) + bv2 = acquire!(pool, Bit, 200) + @test pool.bits.n_active == 2 + @test length(pool.bits.vectors) >= 2 + + # reset! preserves vectors + reset!(pool) + @test pool.bits.n_active == 0 + @test length(pool.bits.vectors) >= 2 # vectors preserved + + # empty! clears everything + bv3 = acquire!(pool, Bit, 50) + empty!(pool) + @test pool.bits.n_active == 0 + @test isempty(pool.bits.vectors) + end + + @testset "DisabledPool fallback" begin + # acquire! with Bit + bv = acquire!(DISABLED_CPU, Bit, 100) + @test bv isa BitVector + @test length(bv) == 100 + + # N-D + ba = acquire!(DISABLED_CPU, Bit, 10, 10) + @test ba isa BitArray{2} + @test size(ba) == (10, 10) + + # Tuple form + ba_tuple = acquire!(DISABLED_CPU, Bit, (5, 5)) + @test ba_tuple isa BitArray{2} + @test size(ba_tuple) == (5, 5) + + # ones! with Bit (like trues) + t = ones!(DISABLED_CPU, Bit, 50) + @test t isa BitVector + @test all(t) + + t2d = ones!(DISABLED_CPU, Bit, 5, 5) + @test t2d isa BitArray{2} + @test all(t2d) + + # ones! with Bit - Tuple form (covers convenience.jl:484) + t_tuple = ones!(DISABLED_CPU, Bit, (4, 4)) + @test t_tuple isa BitArray{2} + @test all(t_tuple) + + # zeros! with Bit (like falses) + f = zeros!(DISABLED_CPU, Bit, 50) + @test f isa BitVector + @test !any(f) + + f2d = zeros!(DISABLED_CPU, Bit, 5, 5) + @test f2d isa BitArray{2} + @test !any(f2d) + + # zeros! with Bit - Tuple form (covers convenience.jl:482) + f_tuple = zeros!(DISABLED_CPU, Bit, (4, 4)) + @test f_tuple isa BitArray{2} + @test !any(f_tuple) + + # trues! (convenience for BitVector filled with true) + t_trues = trues!(DISABLED_CPU, 50) + @test t_trues isa BitVector + @test all(t_trues) + + t_trues_2d = trues!(DISABLED_CPU, 5, 5) + @test t_trues_2d isa BitArray{2} + @test all(t_trues_2d) + + t_trues_tuple = trues!(DISABLED_CPU, (4, 4)) + @test t_trues_tuple isa BitArray{2} + @test all(t_trues_tuple) + + # falses! (convenience for BitVector filled with false) + f_falses = falses!(DISABLED_CPU, 50) + @test f_falses isa BitVector + @test !any(f_falses) + + f_falses_2d = falses!(DISABLED_CPU, 5, 5) + @test f_falses_2d isa BitArray{2} + @test !any(f_falses_2d) + + f_falses_tuple = falses!(DISABLED_CPU, (4, 4)) + @test f_falses_tuple isa BitArray{2} + @test !any(f_falses_tuple) + end + + @testset "Memory efficiency vs Vector{Bool}" begin + pool = AdaptiveArrayPool() + + # BitVector should use ~8x less memory than Vector{Bool} + # (1 bit vs 1 byte per element) + bv = acquire!(pool, Bit, 1000) + vb = acquire!(pool, Bool, 1000) + + bv_parent = parent(bv) + vb_parent = parent(vb) + + # BitVector stores 64 bits per chunk (UInt64) + @test sizeof(bv_parent.chunks) < sizeof(vb_parent) + # Approximate: BitVector ~125 bytes (1000/8), Vector{Bool} ~1000 bytes + @test sizeof(bv_parent.chunks) <= div(1000, 8) + 8 # allow some overhead + end + + @testset "@with_pool macro integration" begin + result = @with_pool pool begin + bv = acquire!(pool, Bit, 100) + t = ones!(pool, Bit, 50) + f = zeros!(pool, Bit, 50) + + bv .= true + sum_bv = count(bv) + sum_t = count(t) + sum_f = count(f) + + (sum_bv, sum_t, sum_f) + end + + @test result == (100, 50, 0) + + # Test trues!/falses! within @with_pool + result2 = @with_pool pool begin + t = trues!(pool, 100) + f = falses!(pool, 50) + + @test all(t) + @test !any(f) + + (count(t), count(f)) + end + + @test result2 == (100, 0) + end + + @testset "@maybe_with_pool macro integration" begin + # With pooling enabled (default) + result1 = @maybe_with_pool pool begin + bv = acquire!(pool, Bit, 100) + bv .= true + count(bv) + end + @test result1 == 100 + + # With pooling disabled + AdaptiveArrayPools.MAYBE_POOLING_ENABLED[] = false + try + result2 = @maybe_with_pool pool begin + bv = acquire!(pool, Bit, 100) + @test bv isa BitVector # DisabledPool returns BitVector + bv .= true + count(bv) + end + @test result2 == 100 + + # Test trues!/falses! with pooling disabled + result3 = @maybe_with_pool pool begin + t = trues!(pool, 50) + f = falses!(pool, 30) + @test t isa BitVector # Falls back to Julia's trues() + @test f isa BitVector # Falls back to Julia's falses() + @test all(t) + @test !any(f) + (count(t), count(f)) + end + @test result3 == (50, 0) + + # Test ones!/zeros! with Bit type, pooling disabled + result4 = @maybe_with_pool pool begin + o = ones!(pool, Bit, 40) + z = zeros!(pool, Bit, 20) + @test o isa BitVector # Falls back to Julia's trues() + @test z isa BitVector # Falls back to Julia's falses() + @test all(o) + @test !any(z) + (count(o), count(z)) + end + @test result4 == (40, 0) + finally + AdaptiveArrayPools.MAYBE_POOLING_ENABLED[] = true + end + end + + @testset "Mixed Bool types" begin + pool = AdaptiveArrayPool() + + # Vector{Bool} via acquire! with Bool + vb = acquire!(pool, Bool, 100) + @test vb isa SubArray{Bool, 1, Vector{Bool}} + @test pool.bool.n_active == 1 + + # BitVector via acquire! with Bit + bv = acquire!(pool, Bit, 100) + @test bv isa SubArray{Bool, 1, BitVector} + @test pool.bits.n_active == 1 + + # Both should work independently + vb .= true + bv .= false + @test all(vb) + @test !any(bv) + + # Separate pools + @test pool.bool.n_active == 1 + @test pool.bits.n_active == 1 + end + + @testset "Nested scopes" begin + outer_result = @with_pool outer_pool begin + outer_bv = acquire!(outer_pool, Bit, 100) + outer_bv .= true + + inner_result = @with_pool inner_pool begin + inner_bv = acquire!(inner_pool, Bit, 50) + inner_bv .= false + count(inner_bv) + end + + # outer_bv should still be valid + @test all(outer_bv) + (count(outer_bv), inner_result) + end + + @test outer_result == (100, 0) + end + + @testset "unsafe_acquire! not supported" begin + pool = AdaptiveArrayPool() + + # unsafe_acquire! with Bit should throw a clear error + @test_throws ArgumentError unsafe_acquire!(pool, Bit, 100) + @test_throws ArgumentError unsafe_acquire!(pool, Bit, 10, 10) + + # Tuple form (covers acquire.jl:251) + @test_throws ArgumentError unsafe_acquire!(pool, Bit, (10, 10)) + + # Verify the error message is helpful + try + unsafe_acquire!(pool, Bit, 100) + catch e + @test e isa ArgumentError + @test occursin("unsafe_acquire!", e.msg) + @test occursin("Bit", e.msg) + @test occursin("acquire!", e.msg) # Suggests alternative + end + end + + @testset "API consistency" begin + # Verify the API is consistent across types + pool = AdaptiveArrayPool() + + # All these should work with the same pattern + v_f64 = acquire!(pool, Float64, 10) + v_i32 = acquire!(pool, Int32, 10) + v_bool = acquire!(pool, Bool, 10) + v_bit = acquire!(pool, Bit, 10) + + @test eltype(v_f64) == Float64 + @test eltype(v_i32) == Int32 + @test eltype(v_bool) == Bool + @test eltype(v_bit) == Bool + + # zeros!/ones! work consistently + z_f64 = zeros!(pool, Float64, 10) + z_bit = zeros!(pool, Bit, 10) + o_f64 = ones!(pool, Float64, 10) + o_bit = ones!(pool, Bit, 10) + + @test all(z_f64 .== 0.0) + @test !any(z_bit) + @test all(o_f64 .== 1.0) + @test all(o_bit) + end + + @testset "NTuple form coverage" begin + pool = AdaptiveArrayPool() + + # Test NTuple forms for trues!/falses! (covers _trues_impl! and _falses_impl! NTuple overloads) + t_tuple = trues!(pool, (5, 5)) + @test size(t_tuple) == (5, 5) + @test all(t_tuple) + + f_tuple = falses!(pool, (5, 5)) + @test size(f_tuple) == (5, 5) + @test !any(f_tuple) + + # Test NTuple forms for zeros!/ones! with Bit type + # (covers _zeros_impl! and _ones_impl! with Bit NTuple overloads) + z_bit_tuple = zeros!(pool, Bit, (4, 4)) + @test size(z_bit_tuple) == (4, 4) + @test !any(z_bit_tuple) + + o_bit_tuple = ones!(pool, Bit, (4, 4)) + @test size(o_bit_tuple) == (4, 4) + @test all(o_bit_tuple) + end + + @testset "Generic DisabledPool fallback for unknown backend" begin + # Test that trues!/falses! throw BackendNotLoadedError for unknown backends + unknown_pool = DisabledPool{:unknown_backend}() + + @test_throws AdaptiveArrayPools.BackendNotLoadedError trues!(unknown_pool, 10) + @test_throws AdaptiveArrayPools.BackendNotLoadedError falses!(unknown_pool, 10) + + # Verify error message + try + trues!(unknown_pool, 10) + catch e + @test e isa AdaptiveArrayPools.BackendNotLoadedError + @test e.backend == :unknown_backend + end + end + + @testset "_impl! delegators for DisabledPool" begin + # Test _trues_impl! and _falses_impl! for DisabledPool (macro transformation path) + # These are called when @maybe_with_pool transforms trues!/falses! calls + + # Vararg form + t = AdaptiveArrayPools._trues_impl!(DISABLED_CPU, 10) + @test t isa BitVector + @test all(t) + + f = AdaptiveArrayPools._falses_impl!(DISABLED_CPU, 10) + @test f isa BitVector + @test !any(f) + + # NTuple form + t_tuple = AdaptiveArrayPools._trues_impl!(DISABLED_CPU, (5, 5)) + @test t_tuple isa BitArray{2} + @test all(t_tuple) + + f_tuple = AdaptiveArrayPools._falses_impl!(DISABLED_CPU, (5, 5)) + @test f_tuple isa BitArray{2} + @test !any(f_tuple) + end + + @testset "_impl! with Bit type NTuple for AbstractArrayPool" begin + # Test _zeros_impl! and _ones_impl! with Bit type NTuple form + # These are internal functions called by macro transformation + pool = AdaptiveArrayPool() + + # Direct calls to _impl! functions with Bit type and NTuple + z = AdaptiveArrayPools._zeros_impl!(pool, Bit, (3, 3)) + @test size(z) == (3, 3) + @test !any(z) + + o = AdaptiveArrayPools._ones_impl!(pool, Bit, (3, 3)) + @test size(o) == (3, 3) + @test all(o) + end + +end # BitArray Support diff --git a/test/test_coverage.jl b/test/test_coverage.jl index 581abc7..5262b42 100644 --- a/test/test_coverage.jl +++ b/test/test_coverage.jl @@ -4,6 +4,16 @@ @testset "Coverage Tests" begin + @testset "pooling_enabled dispatch" begin + # AbstractArrayPool dispatch (types.jl:137) + pool = AdaptiveArrayPool() + @test pooling_enabled(pool) === true + + # DisabledPool dispatch (types.jl:138) + @test pooling_enabled(DISABLED_CPU) === false + @test pooling_enabled(DisabledPool{:cuda}()) === false + end + @testset "DisabledPool convenience functions" begin pool = DISABLED_CPU diff --git a/test/test_fixed_slots.jl b/test/test_fixed_slots.jl index efe50ac..e4a2c93 100644 --- a/test/test_fixed_slots.jl +++ b/test/test_fixed_slots.jl @@ -1,25 +1,26 @@ @testset "Fixed Slot Infrastructure" begin - using AdaptiveArrayPools: FIXED_SLOT_FIELDS, foreach_fixed_slot, TypedPool + using AdaptiveArrayPools: FIXED_SLOT_FIELDS, foreach_fixed_slot, TypedPool, AbstractTypedPool @testset "FIXED_SLOT_FIELDS Synchronization" begin pool = AdaptiveArrayPool() - # Forward check: all FIXED_SLOT_FIELDS exist in struct as TypedPool + # Forward check: all FIXED_SLOT_FIELDS exist in struct as AbstractTypedPool + # (includes TypedPool{T} and BitTypedPool) for field in FIXED_SLOT_FIELDS @test hasfield(AdaptiveArrayPool, field) - @test fieldtype(AdaptiveArrayPool, field) <: TypedPool - @test getfield(pool, field) isa TypedPool + @test fieldtype(AdaptiveArrayPool, field) <: AbstractTypedPool + @test getfield(pool, field) isa AbstractTypedPool end - # Reverse check: all TypedPool fields in struct are in FIXED_SLOT_FIELDS + # Reverse check: all AbstractTypedPool fields in struct are in FIXED_SLOT_FIELDS for (name, type) in zip(fieldnames(AdaptiveArrayPool), fieldtypes(AdaptiveArrayPool)) - if type <: TypedPool + if type <: AbstractTypedPool @test name in FIXED_SLOT_FIELDS end end # Count verification - typedpool_count = count(t -> t <: TypedPool, fieldtypes(AdaptiveArrayPool)) + typedpool_count = count(t -> t <: AbstractTypedPool, fieldtypes(AdaptiveArrayPool)) @test typedpool_count == length(FIXED_SLOT_FIELDS) end @@ -99,7 +100,7 @@ @testset "Integration: empty! Uses foreach_fixed_slot" begin pool = AdaptiveArrayPool() - # Acquire arrays of different types + # Acquire arrays of different types (including BitVector) acquire!(pool, Float64, 10) acquire!(pool, Float32, 10) acquire!(pool, Int64, 10) @@ -107,6 +108,7 @@ acquire!(pool, ComplexF64, 10) acquire!(pool, ComplexF32, 10) acquire!(pool, Bool, 10) + acquire!(pool, Bit, 10) # BitTypedPool # Verify all pools have active arrays for field in FIXED_SLOT_FIELDS