# \[ANN\] EnumSets.jl

**URL:** https://discourse.julialang.org/t/ann-enumsets-jl/123671
**Category:** Package Announcements
**Tags:** set, enum
**Created:** [December 10, 2024, 5:51pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671 "2024-12-10T17:51:24Z")
**Posts on this page:** 7
**Page:** 1

<div class="post-metadata">

### Author: ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)
#### Post date: [December 10, 2024, 5:51pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/1 "2024-12-10T17:51:24Z")

</div>

I am pleased to announce [EnumSets.jl](https://github.com/jw3126/EnumSets.jl). From the readme:

# EnumSets

[![Build Status](https://github.com/jw3126/EnumSets.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/jw3126/EnumSets.jl/actions/workflows/CI.yml?query=branch%3Amain)  
[![Coverage](https://codecov.io/gh/jw3126/EnumSets.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/jw3126/EnumSets.jl)

This packages allows to create a very fast immutable type that represents a set of enum values.

```julia
julia> using EnumSets

julia> @enum Lang Python Julia C

julia> @enumset LangSet <: EnumSet{Lang}

julia> s = LangSet((Python, Julia))
LangSet with 2 elements:
  Python
  Julia

julia> push(s, C) # s is immutable, but we can create modified copies
LangSet with 3 elements:
  Python
  Julia
  C

julia> s
LangSet with 2 elements:
  Python
  Julia

julia> s2 = LangSet((C, Python))
LangSet with 2 elements:
  Python
  C

julia> s ∪ s2
LangSet with 3 elements:
  Python
  Julia
  C

julia> s ∩ s2
LangSet with 1 element:
  Python

...

```

## Performance

```julia
using EnumSets

@enum Alphabet A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

function workout(sets)
    s = first(sets)
    b = false
    ESet = eltype(sets)
    E = eltype(ESet)
    for s1 in sets
        for s2 in sets
            for e in instances(E)
                s = (s ∩ s2) ∪ s1
                s = s ∪ s1
                s = symdiff(s, ESet((e,)))
                b = b ⊻ (s1 ⊆ s2)
                b = b ⊻ (s1 ⊊ s2)
                b = b ⊻ (e in s)
            end
        end
    end
    s, b
end

@enumset AlphabetSet <: EnumSet{Alphabet}

sets = [AlphabetSet(rand(instances(Alphabet)) for _ in 0:length(instances(Alphabet))) for _ in 1:100]
basesets = map(Set, sets)

# warmup
workout(sets, )
workout(basesets, )
# benchmark
println(eltype(sets))
res1 = @time workout(sets, )
println(eltype(basesets))
res2 = @time workout(basesets, )

@assert res1 == res2 # both yield the same result

```

```julia-auto
# AlphabetSet
# 0.000279 seconds (1 allocation: 16 bytes)
# Set{Alphabet}
# 0.503022 seconds (8.15 M allocations: 756.469 MiB, 13.51% gc time)

```

---

<div class="post-metadata">

### Author: ![greatpet](https://avatars.discourse-cdn.com/v4/letter/g/e495f1/32.png) [@greatpet](https://discourse.julialang.org/u/greatpet)
#### Post date: [December 10, 2024, 10:10pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/2 "2024-12-10T22:10:59Z")

</div>

If I take a guess, are you representing a set of enum values by storing a boolean array (or possibly a static array / tuple) indicating whether each enum value is present in the set?

---

<div class="post-metadata">

### Author: ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)
#### Post date: [December 11, 2024, 6:54am UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/3 "2024-12-11T06:54:11Z")

</div>

Not quite, I use the bits of an integer to store membership. Say an `NTuple{64, Bool}` would take 64 **bytes** of storage, while an `UInt64` is able to represent the same information in only 64 **bits**. Also using integers gives direct hardware support for many operations. E.g. an intersection is just a single and.

---

<div class="post-metadata">

### Author: ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)
#### Post date: [December 11, 2024, 12:25pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/4 "2024-12-11T12:25:35Z")

</div>

If you care about speed in particular, then consider replacing [EnumSets.jl/src/EnumSets.jl at 54ac214573eb812bc51a22f880fbaa169fa25642 · jw3126/EnumSets.jl · GitHub](https://github.com/jw3126/EnumSets.jl/blob/54ac214573eb812bc51a22f880fbaa169fa25642/src/EnumSets.jl#L124) with the variant from base/BitSet [julia/base/bitset.jl at d269d7d375827a0279dc1fee7bb24c9418f06f03 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/blob/d269d7d375827a0279dc1fee7bb24c9418f06f03/base/bitset.jl#L333)

---

<div class="post-metadata">

### Author: ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)
#### Post date: [December 11, 2024, 1:38pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/5 "2024-12-11T13:38:08Z")

</div>

Thanks for the suggestion. What are the key speed advantages of the bitset code you link? Is it about `@inbounds` or is there additional stuff I should borrow from?

---

<div class="post-metadata">

### Author: ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)
#### Post date: [December 11, 2024, 3:26pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/6 "2024-12-11T15:26:56Z")

</div>

In order to iterate over the indices of all set bits in an integer, you keep the current index as state, and to get the next one you increment until you find a set bit. Hence, you can expect roughly one branch-miss per output bit.

The alternative keeps as state the remaining integer, with all consumed bits chopped off. To get the next one, you simply count the number of trailing zero-bits (`Base.trailing_zeros`), and to get the next state you clear the lowest set bit (`Base._blsr`). Both of these have dedicated instructions in x86. You can expect roughly one branch-miss per integer, i.e. per `for s in A`.

You should expect ~10x speedup from that.

---

<div class="post-metadata">

### Author: ![jw3126](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jw3126/32/3086_2.png) [@jw3126](https://discourse.julialang.org/u/jw3126)
#### Post date: [December 11, 2024, 3:57pm UTC](https://discourse.julialang.org/t/ann-enumsets-jl/123671/7 "2024-12-11T15:57:52Z")

</div>

Awesome thanks a lot for these insights!
