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: 0 additions & 1 deletion .github/workflows/TagBot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
ssh: ${{ secrets.DOCUMENTER_KEY }}
dispatch: true
changelog: false
82 changes: 61 additions & 21 deletions docs/src/features/bit-arrays.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BitVector Support
# BitArray Support

AdaptiveArrayPools.jl includes specialized support for `BitArray` (specifically `BitVector`), enabling **~8x memory savings** for boolean arrays compared to standard `Vector{Bool}`.
AdaptiveArrayPools.jl includes specialized support for `BitArray` (including `BitVector` and N-dimensional `BitArray{N}`), enabling **~8x memory savings** for boolean arrays compared to standard `Vector{Bool}`.

## The `Bit` Sentinel Type

Expand All @@ -14,31 +14,34 @@ To distinguish between standard boolean arrays (`Vector{Bool}`, 1 byte/element)
## Usage

### 1D Arrays (BitVector)
For 1D arrays, `acquire!` returns a view into a pooled `BitVector`.
For 1D arrays, `acquire!` returns a native `BitVector`. This design choice enables full SIMD optimization, making operations significantly faster (10x~100x) than using views.

```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

# Supports standard operations with full SIMD acceleration
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.
### N-D Arrays (BitArray)
For multi-dimensional arrays, `acquire!` returns a `BitArray{N}` (specifically `BitMatrix` for 2D). This preserves the packed memory layout and SIMD benefits while providing N-D indexing.

```julia
@with_pool pool begin
# 100x100 bit matrix
# 100x100 bit matrix (returns BitMatrix)
mask = zeros!(pool, Bit, 100, 100)

mask[5, 5] = true

# 3D BitArray
volume = acquire!(pool, Bit, 10, 10, 10)
end
```

Expand All @@ -50,29 +53,66 @@ For specific `BitVector` operations, prefer `trues!` and `falses!` which mirror
@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)
C = similar!(pool, A, Bit)
end
```

Note: `zeros!(pool, Bit, ...)` and `ones!(pool, Bit, ...)` are also supported (aliased to `falses!` and `trues!`).

## Performance & Safety

### Why Native BitArray?
The pool returns native `BitVector`/`BitArray` types instead of `SubArray` views for **performance**.
Operations like `count()`, `sum()`, and bitwise broadcasting are **10x~100x faster** on native bit arrays because they utilize SIMD instructions on packed 64-bit chunks.

### N-D Caching & Zero Allocation

The pool uses an N-way associative cache to efficiently reuse `BitArray{N}` instances:

| Scenario | Allocation |
|----------|------------|
| First call with new dims | ~944 bytes (new `BitArray{N}` created) |
| Subsequent call with same dims | **0 bytes** (cached instance reused) |
| Same ndims, different dims | **0 bytes** (dims/len fields modified in-place) |
| Different ndims | ~944 bytes (new `BitArray{N}` created and cached) |

Unlike regular `Array` where dimensions are immutable, `BitArray` allows in-place modification of its `dims` and `len` fields. The pool exploits this to achieve **zero allocation** on repeated calls with matching dimensionality.

```julia
@with_pool pool begin
# First call: allocates BitMatrix wrapper (~944 bytes)
m1 = acquire!(pool, Bit, 100, 100)

# Rewind to reuse the same slot
rewind!(pool)

# Same dims: 0 allocation (exact cache hit)
m2 = acquire!(pool, Bit, 100, 100)

rewind!(pool)

# Different dims but same ndims: 0 allocation (dims modified in-place)
m3 = acquire!(pool, Bit, 50, 200)
end
```

## How It Works
### ⚠️ Important: Do Not Resize

While the returned arrays are standard `BitVector` types, they share their underlying memory chunks with the pool.

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{...}}`.
!!! warning "Do Not Resize"
**NEVER** resize (`push!`, `pop!`, `resize!`) a pooled `BitVector` or `BitArray`.

This ensures that even for complex shapes, the underlying storage is always a compact `BitVector` reused from the pool.
The underlying memory is owned and managed by the pool. Resizing it will detach it from the pool or potentially corrupt the shared state. Treat these arrays as **fixed-size** scratch buffers only.
3 changes: 3 additions & 0 deletions src/AdaptiveArrayPools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ include("utils.jl")
# Acquisition operations: get_view!, acquire!, unsafe_acquire!, aliases
include("acquire.jl")

# BitArray-specific acquisition (SIMD-optimized BitVector operations)
include("bitarray.jl")

# Convenience functions: zeros!, ones!, similar!
include("convenience.jl")

Expand Down
27 changes: 0 additions & 27 deletions src/acquire.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,12 @@
@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
# ==============================================================================
Expand Down Expand Up @@ -245,11 +228,6 @@ 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)
# ==============================================================================
Expand Down Expand Up @@ -450,11 +428,6 @@ 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!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B)
@inline unsafe_acquire!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B)
Expand Down
Loading