Skip to content

Commit cf2a9a3

Browse files
authored
Merge pull request #15 from ProjectTorreyPines/feat/unsafe_BitArray_support
(refac): return `BitVector` for performance
2 parents 03da49d + 8e0ff19 commit cf2a9a3

12 files changed

Lines changed: 654 additions & 149 deletions

File tree

.github/workflows/TagBot.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,3 @@ jobs:
1919
token: ${{ secrets.GITHUB_TOKEN }}
2020
ssh: ${{ secrets.DOCUMENTER_KEY }}
2121
dispatch: true
22-
changelog: false

docs/src/features/bit-arrays.md

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# BitVector Support
1+
# BitArray Support
22

3-
AdaptiveArrayPools.jl includes specialized support for `BitArray` (specifically `BitVector`), enabling **~8x memory savings** for boolean arrays compared to standard `Vector{Bool}`.
3+
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}`.
44

55
## The `Bit` Sentinel Type
66

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

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

1919
```julia
2020
@with_pool pool begin
2121
# Acquire a BitVector of length 1000
2222
bv = acquire!(pool, Bit, 1000)
23-
23+
2424
# Use like normal
2525
bv .= true
2626
bv[1] = false
27-
28-
# Supports standard operations
27+
28+
# Supports standard operations with full SIMD acceleration
2929
count(bv)
3030
end
3131
```
3232

33-
### N-D Arrays (BitArray / Reshaped)
34-
For multi-dimensional arrays, `acquire!` returns a `ReshapedArray` wrapper around the linear `BitVector`. This maintains zero-allocation efficiency while providing N-D indexing.
33+
### N-D Arrays (BitArray)
34+
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.
3535

3636
```julia
3737
@with_pool pool begin
38-
# 100x100 bit matrix
38+
# 100x100 bit matrix (returns BitMatrix)
3939
mask = zeros!(pool, Bit, 100, 100)
40-
40+
4141
mask[5, 5] = true
42+
43+
# 3D BitArray
44+
volume = acquire!(pool, Bit, 10, 10, 10)
4245
end
4346
```
4447

@@ -50,29 +53,66 @@ For specific `BitVector` operations, prefer `trues!` and `falses!` which mirror
5053
@with_pool pool begin
5154
# Filled with false (equivalent to `falses(256)`)
5255
mask = falses!(pool, 256)
53-
56+
5457
# Filled with true (equivalent to `trues(256)`)
5558
flags = trues!(pool, 256)
56-
59+
5760
# Multidimensional
5861
grid = trues!(pool, 100, 100)
59-
62+
6063
# Similar to existing BitArray
6164
A = BitVector(undef, 50)
6265
B = similar!(pool, A) # Reuses eltype(A) -> Bool
63-
66+
6467
# To explicit get Bit-packed from pool irrespective of source
65-
C = similar!(pool, A, Bit)
68+
C = similar!(pool, A, Bit)
6669
end
70+
```
6771

6872
Note: `zeros!(pool, Bit, ...)` and `ones!(pool, Bit, ...)` are also supported (aliased to `falses!` and `trues!`).
73+
74+
## Performance & Safety
75+
76+
### Why Native BitArray?
77+
The pool returns native `BitVector`/`BitArray` types instead of `SubArray` views for **performance**.
78+
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.
79+
80+
### N-D Caching & Zero Allocation
81+
82+
The pool uses an N-way associative cache to efficiently reuse `BitArray{N}` instances:
83+
84+
| Scenario | Allocation |
85+
|----------|------------|
86+
| First call with new dims | ~944 bytes (new `BitArray{N}` created) |
87+
| Subsequent call with same dims | **0 bytes** (cached instance reused) |
88+
| Same ndims, different dims | **0 bytes** (dims/len fields modified in-place) |
89+
| Different ndims | ~944 bytes (new `BitArray{N}` created and cached) |
90+
91+
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.
92+
93+
```julia
94+
@with_pool pool begin
95+
# First call: allocates BitMatrix wrapper (~944 bytes)
96+
m1 = acquire!(pool, Bit, 100, 100)
97+
98+
# Rewind to reuse the same slot
99+
rewind!(pool)
100+
101+
# Same dims: 0 allocation (exact cache hit)
102+
m2 = acquire!(pool, Bit, 100, 100)
103+
104+
rewind!(pool)
105+
106+
# Different dims but same ndims: 0 allocation (dims modified in-place)
107+
m3 = acquire!(pool, Bit, 50, 200)
108+
end
69109
```
70110

71-
## How It Works
111+
### ⚠️ Important: Do Not Resize
112+
113+
While the returned arrays are standard `BitVector` types, they share their underlying memory chunks with the pool.
72114

73-
The pool maintains a separate `BitTypedPool` specifically for `BitVector` storage.
74-
- **Sentinel**: `acquire!(..., Bit, ...)` dispatches to this special pool.
75-
- **Views**: 1D returns `SubArray{Bool, 1, BitVector, ...}`.
76-
- **Reshaping**: N-D returns `ReshapedArray{Bool, N, SubArray{...}}`.
115+
!!! warning "Do Not Resize"
116+
**NEVER** resize (`push!`, `pop!`, `resize!`) a pooled `BitVector` or `BitArray`.
77117

78-
This ensures that even for complex shapes, the underlying storage is always a compact `BitVector` reused from the pool.
118+
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.

src/AdaptiveArrayPools.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ include("utils.jl")
2828
# Acquisition operations: get_view!, acquire!, unsafe_acquire!, aliases
2929
include("acquire.jl")
3030

31+
# BitArray-specific acquisition (SIMD-optimized BitVector operations)
32+
include("bitarray.jl")
33+
3134
# Convenience functions: zeros!, ones!, similar!
3235
include("convenience.jl")
3336

src/acquire.jl

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,12 @@
66
@inline allocate_vector(::AbstractTypedPool{T,Vector{T}}, n::Int) where {T} =
77
Vector{T}(undef, n)
88

9-
# BitTypedPool allocates BitVector (used when acquiring with Bit type)
10-
@inline allocate_vector(::BitTypedPool, n::Int) = BitVector(undef, n)
11-
12-
# Bit type returns Bool element type for fill operations (zero/one)
13-
@inline Base.zero(::Type{Bit}) = false
14-
@inline Base.one(::Type{Bit}) = true
15-
169
# Wrap flat view into N-D array (dispatch point for extensions)
1710
@inline function wrap_array(::AbstractTypedPool{T,Vector{T}},
1811
flat_view, dims::NTuple{N,Int}) where {T,N}
1912
unsafe_wrap(Array{T,N}, pointer(flat_view), dims)
2013
end
2114

22-
# BitTypedPool cannot use unsafe_wrap - throw clear error
23-
# Called from _unsafe_acquire_impl! dispatches for Bit type
24-
@noinline function _throw_bit_unsafe_error()
25-
throw(ArgumentError(
26-
"unsafe_acquire!(pool, Bit, ...) is not supported. " *
27-
"BitArray stores data in immutable chunks::Vector{UInt64} that cannot be wrapped with unsafe_wrap. " *
28-
"Use acquire!(pool, Bit, ...) instead, which returns a view."
29-
))
30-
end
31-
3215
# ==============================================================================
3316
# Helper: Overflow-Safe Product
3417
# ==============================================================================
@@ -245,11 +228,6 @@ end
245228
# Similar-style
246229
@inline _unsafe_acquire_impl!(pool::AbstractArrayPool, x::AbstractArray) = _unsafe_acquire_impl!(pool, eltype(x), size(x))
247230

248-
# Bit type: unsafe_acquire! not supported (throw clear error early)
249-
@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::Int) = _throw_bit_unsafe_error()
250-
@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::Vararg{Int,N}) where {N} = _throw_bit_unsafe_error()
251-
@inline _unsafe_acquire_impl!(::AbstractArrayPool, ::Type{Bit}, ::NTuple{N,Int}) where {N} = _throw_bit_unsafe_error()
252-
253231
# ==============================================================================
254232
# Acquisition API (User-facing with untracked marking)
255233
# ==============================================================================
@@ -450,11 +428,6 @@ const _acquire_array_impl! = _unsafe_acquire_impl!
450428
@inline unsafe_acquire!(::DisabledPool{:cpu}, ::Type{T}, dims::NTuple{N,Int}) where {T,N} = Array{T,N}(undef, dims)
451429
@inline unsafe_acquire!(::DisabledPool{:cpu}, x::AbstractArray) = similar(x)
452430

453-
# --- acquire! for DisabledPool{:cpu} with Bit type (returns BitArray) ---
454-
@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, n::Int) = BitVector(undef, n)
455-
@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, dims::Vararg{Int,N}) where {N} = BitArray{N}(undef, dims)
456-
@inline acquire!(::DisabledPool{:cpu}, ::Type{Bit}, dims::NTuple{N,Int}) where {N} = BitArray{N}(undef, dims)
457-
458431
# --- Generic DisabledPool fallbacks (unknown backend → error) ---
459432
@inline acquire!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B)
460433
@inline unsafe_acquire!(::DisabledPool{B}, _args...) where {B} = _throw_backend_not_loaded(B)

0 commit comments

Comments
 (0)