This Julia package defines three immutable collections types, SmallVector,
PackedVector and SmallBitSet. They don't allocate and are often much faster
than their allocating counterparts Vector and BitSet. Unlike the static vectors
provided by StaticArrays.jl,
StaticVectors.jl and
SIMD.jl,
the length of a SmallVector or PackedVector is variable with a user-defined limit.
If the package BangBang.jl
is loaded, then many functions defined by this package are also available
in a !!-form. For example, both push and push!! add an element
to a SmallVector, PackedVector or SmallBitSet.
Below are examples and benchmarks. For details see the documentation.
A vector of type SmallVector{N,T} can hold up to N elements of type T.
Both N and T can be arbitrary. (If T is not a concrete type, however,
then creating a small vector does allocate.)
julia> v = SmallVector{8,Int8}(2*x for x in 1:3)
3-element SmallVector{8, Int8}:
2
4
6
julia> setindex(v, 7, 3)
3-element SmallVector{8, Int8}:
2
4
7
julia> w = SmallVector{9}((1, 2.5, 4))
3-element SmallVector{9, Float64}:
1.0
2.5
4.0
julia> v+2*w
3-element SmallVector{8, Float64}:
4.0
9.0
14.0Non-numeric element types are possible. (One may have to define
a default element used for padding via SmallCollections.default(T).
For Char, String and Symbol they are pre-defined.)
julia> u = SmallVector{6}(['a', 'b', 'c'])
3-element SmallVector{6, Char}:
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
julia> popfirst(u)
(['b', 'c'], 'a')
julia> map(uppercase, u)
3-element SmallVector{6, Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)A PackedVector can store bit integers or Bool values.
The elements of a PackedVector{U,M,T} are stored in a common bit mask of type U
with M bits for each entry. When retrieving elements, they are of type T.
Compared to a SmallVector, a PackedVector may have faster insert and delete operations.
Arithmetic operations are usually slower unless M is the size of a hardware integer.
julia> v = PackedVector{UInt64,5,Int}(4:6)
3-element PackedVector{UInt64, 5, Int64}:
4
5
6
julia> capacity(v) # 64 bits available, 5 for each entry
12
julia> pushfirst(v, 7)
4-element PackedVector{UInt64, 5, Int64}:
7
4
5
6
julia> duplicate(v, 2)
4-element PackedVector{UInt64, 5, Int64}:
4
5
5
6
julia> 3*v # note the overflow in the last entry
3-element PackedVector{UInt64, 5, Int64}:
12
15
-14The default SmallBitSet type SmallBitSet{UInt64} can hold integers
between 1 and 64.
julia> s = SmallBitSet([1, 4, 7])
SmallBitSet{UInt64} with 3 elements:
1
4
7
julia> t = SmallBitSet([3, 4, 5])
SmallBitSet{UInt64} with 3 elements:
3
4
5
julia> union(s, t)
SmallBitSet{UInt64} with 5 elements:
1
3
4
5
7
julia> push(s, 9)
SmallBitSet{UInt64} with 4 elements:
1
4
7
9
julia> filter(iseven, s)
SmallBitSet{UInt64} with 1 element:
4Smaller or larger sets are possible by choosing a different unsigned bit integer
as bitmask type, for example UInt16 or UInt128 or types like UInt256 defined
by the package BitIntegers.jl.
julia> using BitIntegers
julia> SmallBitSet{UInt256}(n for n in 1:256 if isodd(n) && isinteger(sqrt(n)))
SmallBitSet{UInt256} with 8 elements:
1
9
25
49
81
121
169
225The timings are for pairwise adding the elements of two Vectors,
each containing 1000 vectors with element type T.
For Vector and SmallVector the length of each pair of elements is variable and
chosen randomly between 1 and N. For SVector{N,T} (from StaticArrays.jl),
Values{N,T} (from StaticVectors.jl) and Vec{N,T} (from SIMD.jl) the vectors have
fixed length N.
(N, T) |
Vector{T} |
SmallVector{N,T} |
SVector{N,T} |
Values{N,T} |
Vec{N,T} |
|---|---|---|---|---|---|
| (8, Float64) | 66.682 μs | 3.341 μs | 3.452 μs | 3.197 μs | 3.046 μs |
| (8, Int64) | 48.642 μs | 4.962 μs | 3.196 μs | 4.551 μs | 2.954 μs |
| (16, Int32) | 49.449 μs | 3.866 μs | 3.284 μs | 3.623 μs | 3.757 μs |
| (32, Int16) | 55.027 μs | 5.046 μs | 4.212 μs | 3.618 μs | 3.548 μs |
Here we compare a PackedVector{UInt128, 4, Int8} (that can hold 32 elements) to a SmallVector{32, Int8}
and to a Vector{Int8} with 30 elements.
The function duplicate(v, i) is equivalent to insert(v, i+1, v[i]).
For the operations listed in the table below we have chosen the mutating variant for Vector.
| operation | Vector |
SmallVector |
PackedVector |
|---|---|---|---|
| getindex | 2.902 ns | 2.647 ns | 3.167 ns |
| setindex | 2.638 ns | 5.279 ns | 6.861 ns |
| add | 12.419 ns | 2.375 ns | 4.222 ns |
| scalar_mul | 9.762 ns | 4.749 ns | 4.223 ns |
| push | 8.241 ns | 5.541 ns | 8.970 ns |
| pushfirst | 8.750 ns | 4.221 ns | 4.223 ns |
| pop | 8.600 ns | 6.000 ns | 4.933 ns |
| popfirst | 11.267 ns | 4.667 ns | 3.867 ns |
| insert | 12.928 ns | 24.804 ns | 7.328 ns |
| deleteat | 12.933 ns | 18.200 ns | 5.667 ns |
| duplicate | 13.546 ns | 20.845 ns | 4.486 ns |
The timings are for taking the pairwise union of the elements of two Vectors,
each containing 1000 sets of the indicated type.
Each set contains up to b integers between 1 and b = 8*sizeof(U)-1.
U |
Set{Int16} |
BitSet |
SmallBitSet |
|---|---|---|---|
| UInt8 | 366.256 μs | 69.439 μs | 95.698 ns |
| UInt16 | 801.736 μs | 68.195 μs | 311.559 ns |
| UInt32 | 1.537 ms | 68.354 μs | 400.259 ns |
| UInt64 | 2.836 ms | 68.751 μs | 640.833 ns |
| UInt128 | 5.686 ms | 68.846 μs | 1.540 μs |
| UInt256 | 11.579 ms | 69.398 μs | 2.441 μs |
| UInt512 | 23.819 ms | 92.041 μs | 4.866 μs |
Versions: Julia v1.10.4, Chairmarks v1.2.1, SmallCollections v0.3.0, StaticArrays v1.9.7, StaticVectors v1.0.5, SIMD v3.5.0, BitIntegers v0.3.1
Computer: Intel Core i3-10110U CPU @ 2.10GHz with 8GB RAM
The benchmark code can be found in the benchmark directory.