Skip to content

Commit

Permalink
More changes
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobnissen committed Apr 27, 2020
1 parent e4168d9 commit 3f2bdc4
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 48 deletions.
70 changes: 28 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
# SmallBitSet.jl
Stack-allocated integer sets in Julia
# StackCollections.jl
Fixed-bit collections in Julia

A small toy project to show the conciseness, abstraction and speed of Julia.
This package implements a few collection types that can be stored in one or a few machine integers:

This module implements one custom type - the `StackSet{M}`. A `StackSet{M}` is a set of integers in 0:(M-1) encoded in the lower M bits of a machine integer.

In 121 lines, this module implements:

* some basic methods: `copy`, `empty`, `isempty`, custom iteration, `length`, `minimum`, `maximum`, `last`, `in`, `push`, `delete` and `pop`.
* some set-specific methods: `allunique`, `union`, `intersect`, `setdiff`, `complement` and `isdisjoint`.
* As well as converters and constructors.

A `StackSet` behaves exactly like you would expect an `AbstractSet` to do - other than that it is immutable:
* `DigitSet`: A set of integers 0:63
* `StackSet`: A set of integers N:N+63
* `StackVector{L}`: A boolean vector with a length of up to 64.

The main features of the types are:
* They are simple to use, implements the basic methods from `Base` you would expect such as `union` for sets and `reverse` for vectors:
```
julia> a = StackSet(2i+3 for i in 2:3:16)
StackSet{64}([7,13,19,25,31])
julia> 7 in a ? length(union(a, a)) : length(intersect(a, complement(a)))
5
julia> a = StackVector{4}([true, true, false, true]); reverse(a)
4-element StackVector{4}:
1
0
1
1
```
* They are safe by default, and throws informative error messages if you attempt illegal or undefined operations.
```

It is safe to use:
julia> push(DigitSet(), 100)
ERROR: ArgumentError: DigitSet can only contain 0:63
```
julia> pop(a, 6)
ERROR: KeyError: key 6 not found
Stacktrace:
[1] pop(::Main.SmallBitSet.StackSet{64}, ::Int64) at /home/jakni/Documents/scripts/play/SmallBitSet.jl/src/SmallBitSet.jl:100
[2] top-level scope at none:0
julia> push(a, -1)
ERROR: ArgumentError: StackSet{64} can only contain 0:63
Stacktrace:
[1] throw_StackSet_digit_err(::Val{64}) at /home/jakni/Documents/scripts/play/SmallBitSet.jl/src/SmallBitSet.jl:21
[2] push(::Main.SmallBitSet.StackSet{64}, ::Int64) at /home/jakni/Documents/scripts/play/SmallBitSet.jl/src/SmallBitSet.jl:90
[3] top-level scope at none:0
* All types are immutable and so easier to reason about. Base methods that usually end with an exclamation mark such as `push!` instead must use `push`.
```

And is *extremely* efficiently implemented, with most set operations done in single clock cycles:

julia> push!(DigitSet(), 100)
ERROR: MethodError: no method matching push!(::DigitSet, ::Int64)
```
julia> a, b = StackSet{11}(1:3:12), StackSet{41}(4:7:40);
julia> f(x, y) = length(setdiff(x, complement(y)));
julia> f(a, b)
1
* They are _highly_ efficiently implemented, with most methods meticulously crafted for maximal performance.
```
julia> f(x, y) = length(setdiff(x, symdiff(x, y)));
julia> code_native(f, Tuple{typeof(a), typeof(b)}, debuginfo=:none)
.text
julia> code_native(f, (DigitSet, DigitSet), debuginfo=:none)
.section __TEXT,__text,regular,pure_instructions
movq (%rsi), %rax
andq (%rdi), %rax
popcntq %rax, %rax
retq
nopl (%rax)
```

This API follows SemVer 2.0.0. The API for this package is defined by the documentation.
2 changes: 1 addition & 1 deletion src/StackCollections.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module t
module StackCollections

struct Unsafe end
const unsafe = Unsafe()
Expand Down
2 changes: 1 addition & 1 deletion src/digitset.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ end
pop(s::AbstractStackSet, v::Int) = in(s, v) ? delete(s, v, unsafe) : throw(KeyError(v))
delete(s::DigitSet, v::Int) = ifelse(v Sys.WORD_SIZE, s, delete(s, v, unsafe))
function delete(s::DigitSet, v::Int, ::Unsafe)
mask = (typemax(UInt) - 1) << (unsigned(v) & (Sys.WORD_SIZE - 1))
mask = ~(UInt(1) << (unsigned(v) & (Sys.WORD_SIZE - 1)))
DigitSet(s.x & mask)
end

Expand Down
6 changes: 2 additions & 4 deletions src/stackvector.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Base.hash(x::StackVector{L}, h::UInt) where L
end

StackVector{L}() where L = StackVector{L}(UInt(0), unsafe)
StackVector(x...) = StackVector{Sys.WORD_SIZE}(x...)
StackVector(x...) = StackVector{Sys.WORD_SIZE}(x)

function StackVector{L}(itr) where L
bits = zero(UInt)
Expand Down Expand Up @@ -106,9 +106,7 @@ function Base.reverse(s::StackVector)
x = ((x & 0xaaaaaaaaaaaaaaaa) >>> 1) | ((x & 0x5555555555555555) << 1)
x = ((x & 0xcccccccccccccccc) >>> 2) | ((x & 0x3333333333333333) << 2)
x = ((x & 0xf0f0f0f0f0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f0f0f0f0f) << 4)
x = ((x & 0xff00ff00ff00ff00) >>> 8) | ((x & 0x00ff00ff00ff00ff) << 8)
x = ((x & 0xffff0000ffff0000) >>> 16) | ((x & 0x0000ffff0000ffff) << 16)
x = ((x & 0xffffffff00000000) >>> 32) | ((x & 0x00000000ffffffff) << 32)
x = bswap(x)
x >>>= sizeof(UInt) << 3 - length(s)
typeof(s)(x, unsafe)
end
Expand Down
76 changes: 76 additions & 0 deletions test/digitset.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
random_set() = DigitSet(rand(UInt))


@testset "Construction" begin
@test DigitSet() === DigitSet()
s = DigitSet([5, 1, 9, 8])
s2 = DigitSet([9, 8, 1, 5])
@test s === s2

@test_throws ArgumentError DigitSet([-1, 5, 12])
@test DigitSet([1, Int(5), 12]) === DigitSet([1, 12, 5, UInt(12)])
@test_throws ArgumentError DigitSet([5, UInt8(12), 64])
@test DigitSet([5, 12, 64]) === DigitSet([12, 5, 12, 5, 12])
end

@testset "Basic" begin
e = DigitSet()
@test isempty(e)
@test DigitSet() === e
@test empty(e) === e
@test empty(random_set()) === e

s = DigitSet([7, 28, 41, 11])
s2 = DigitSet([7, 28, 41, 12])
s3 = DigitSet([7, 28, 12])
@test s != s2
@test s != s3
@test length(e) == 0
@test length(s) == 4
@test length(s2) == 4
@test length(s3) == 3
end

@testset "Membership" begin
for i in -1:64
test !(i in DigitSet())
end

s = DigitSet([51, 11, 6, 32, 1, 0, 40])
s2 = Set(DigitSet)
for i in 0:63
if i in s2
@test (i in s)
else
@test !(i in s)
end
end

@test (51 in s)
@test !(50 in s)

@test !(5 in DigitSet())
@test !(0 in DigitSet())

#=
Iteration
Membership (in), minimum, maximum
Modification
push
filter
pop
delete
Set operations
issubset
isdisjoint
union
intersect
symdiff
setdiff
=#
18 changes: 18 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module TestStackCollections

using Test
using StackCollections

@testset "DigitSet" begin
include("digitset.jl")
end

@testset "StackSet" begin
include("stackset.jl")
end

@testset "StackVector" begin
include("stackvector.jl")
end

end # module

0 comments on commit 3f2bdc4

Please sign in to comment.