├── .github └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── bitty.nimble ├── src └── bitty.nim └── tests ├── bench.nim ├── config.nims └── test.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | nim-version: ['1.4.0', '2.0.2'] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | 22 | - uses: actions/checkout@v3 23 | with: 24 | path: "${{ github.event.repository.name }}" 25 | 26 | - name: Install Choosenim 27 | run: | 28 | curl -O https://nim-lang.org/choosenim/init.sh 29 | sh init.sh -y 30 | 31 | - name: Add Nim to PATH (linux) 32 | if: runner.os == 'Linux' 33 | run: echo "/home/runner/.nimble/bin" >> $GITHUB_PATH 34 | 35 | - name: Add Nim to PATH (windows) 36 | if: runner.os == 'Windows' 37 | run: | 38 | echo "C:\Users\runneradmin\.nimble\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 39 | shell: pwsh 40 | 41 | - name: Install Atlas 42 | run: | 43 | git clone https://github.com/nim-lang/atlas.git 44 | cd atlas 45 | git checkout cbba9fa77fa837931bf3c58e20c1f8cb15a22919 46 | nim c src/atlas.nim 47 | 48 | - name: Add Atlas to PATH (linux) 49 | if: runner.os == 'Linux' 50 | run: mv atlas/src/atlas /home/runner/.nimble/bin 51 | 52 | - name: Add Atlas to PATH (windows) 53 | if: runner.os == 'Windows' 54 | run: mv atlas/src/atlas.exe C:\Users\runneradmin\.nimble\bin 55 | shell: pwsh 56 | 57 | - name: Init Atlas 58 | run: atlas init 59 | 60 | - name: Install this Commit 61 | run: atlas use file://${{ github.event.repository.name }} 62 | 63 | - name: List nim.cfg 64 | run: cat nim.cfg 65 | 66 | - name: Install Version 67 | run: choosenim ${{ matrix.nim-version }} 68 | 69 | - name: Run tests 70 | run: | 71 | cd "${{ github.event.repository.name }}" 72 | # List test files here 73 | 74 | nim c -r tests/test.nim 75 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: src/${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | *.pdb 10 | *.ilk 11 | .* 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitty - Tightly packed 1d and 2d bit arrays. 2 | 3 | * `atlas use bitty` 4 | * `nimble install bitty` 5 | 6 | ![Github Actions](https://github.com/treeform/bitty/workflows/Github%20Actions/badge.svg) 7 | 8 | [API reference](https://treeform.github.io/bitty) 9 | 10 | This library has no dependencies other than the Nim standard library. 11 | 12 | ## About 13 | 14 | This library gives you 1d and 2d bit arrays and operations to perform on them. 15 | 16 | If you have type like `seq[bool]` in your code, switching to bit arrays should improve your memory usage by 8x. Using tightly packed bit arrays can also improve cache performance if you are doing a lot of sequential read access on a large amount of bits. But tightly packed bit arrays can also be slower if you are doing a lot of random write access. Always measure! 17 | -------------------------------------------------------------------------------- /bitty.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.4" 2 | author = "Andre von Houck" 3 | description = "Utilities with dealing with 1d and 2d bit arrays." 4 | license = "MIT" 5 | 6 | srcDir = "src" 7 | 8 | requires "nim >= 1.4.0" 9 | -------------------------------------------------------------------------------- /src/bitty.nim: -------------------------------------------------------------------------------- 1 | import hashes, bitops 2 | 3 | type BitArray* = ref object 4 | ## Creates an array of bits all packed in together. 5 | bits: seq[uint64] 6 | len*: int 7 | 8 | func divUp(a, b: int): int = 9 | ## Like div, but rounds up instead of down. 10 | let extra = if a mod b > 0: 1 else: 0 11 | return a div b + extra 12 | 13 | func newBitArray*(len: int = 0): BitArray = 14 | ## Create a new bit array. 15 | result = BitArray() 16 | result.len = len 17 | result.bits = newSeq[uint64](len.divUp(64)) 18 | 19 | func setLen*(b: BitArray, len: int) = 20 | ## Sets the length. 21 | b.len = len 22 | b.bits.setLen(len.divUp(64)) 23 | 24 | when defined(release): 25 | {.push checks: off.} 26 | 27 | func firstFalse*(b: BitArray): (bool, int) = 28 | for i, bits in b.bits: 29 | if bits == 0: 30 | return (true, i * 64) 31 | if bits != uint64.high: 32 | let matchingBits = firstSetBit(not bits) 33 | return (true, i * 64 + matchingBits - 1) 34 | (false, 0) 35 | 36 | func unsafeGet*(b: BitArray, i: int): bool = 37 | ## Access a single bit (unchecked). 38 | let 39 | bigAt = i div 64 40 | littleAt = i mod 64 41 | mask = 1.uint64 shl littleAt 42 | return (b.bits[bigAt] and mask) != 0 43 | 44 | func unsafeSetFalse*(b: BitArray, i: int) = 45 | ## Set a single bit to false (unchecked). 46 | let 47 | bigAt = i div 64 48 | littleAt = i mod 64 49 | mask = 1.uint64 shl littleAt 50 | b.bits[bigAt] = b.bits[bigAt] and (not mask) 51 | 52 | func unsafeSetTrue*(b: BitArray, i: int) = 53 | ## Set a single bit to true (unchecked). 54 | let 55 | bigAt = i div 64 56 | littleAt = i mod 64 57 | mask = 1.uint64 shl littleAt 58 | b.bits[bigAt] = b.bits[bigAt] or mask 59 | 60 | iterator trueIndexes*(b: BitArray): int = 61 | var j: int 62 | for i, bits in b.bits: 63 | if bits == 0: 64 | continue 65 | j = 0 66 | while j < 64: 67 | let v = bits and (uint64.high shl j) 68 | if v == 0: 69 | break 70 | j = firstSetBit(v) 71 | yield i * 64 + j - 1 72 | 73 | when defined(release): 74 | {.pop.} 75 | 76 | func `[]`*(b: BitArray, i: int): bool = 77 | ## Access a single bit. 78 | if i < 0 or i >= b.len: 79 | raise newException(IndexDefect, "Index out of bounds") 80 | b.unsafeGet(i) 81 | 82 | func `[]=`*(b: BitArray, i: int, v: bool) = 83 | # Set a single bit. 84 | if i < 0 or i >= b.len: 85 | raise newException(IndexDefect, "Index out of bounds") 86 | if v: 87 | b.unsafeSetTrue(i) 88 | else: 89 | b.unsafeSetFalse(i) 90 | 91 | func `==`*(a, b: BitArray): bool = 92 | ## Are two bit arrays the same. 93 | if b.isNil or a.len != b.len: 94 | return false 95 | for i in 0 ..< a.bits.len: 96 | if a.bits[i] != b.bits[i]: 97 | return false 98 | return true 99 | 100 | func `and`*(a, b: BitArray): BitArray = 101 | ## And(s) two bit arrays returning a new bit array. 102 | if a.len != b.len: 103 | raise newException(ValueError, "Bit arrays are not same length") 104 | result = newBitArray(a.len) 105 | for i in 0 ..< a.bits.len: 106 | result.bits[i] = a.bits[i] and b.bits[i] 107 | 108 | func `or`*(a, b: BitArray): BitArray = 109 | ## Or(s) two bit arrays returning a new bit array. 110 | if a.len != b.len: 111 | raise newException(ValueError, "Bit arrays are not same length") 112 | result = newBitArray(a.len) 113 | for i in 0 ..< a.bits.len: 114 | result.bits[i] = a.bits[i] or b.bits[i] 115 | 116 | func `not`*(a: BitArray): BitArray = 117 | ## Not(s) or inverts a and returns a new bit array. 118 | result = newBitArray(a.len) 119 | for i in 0 ..< a.bits.len: 120 | result.bits[i] = not a.bits[i] 121 | 122 | func `$`*(b: BitArray): string = 123 | ## Turns the bit array into a string. 124 | result = newStringOfCap(b.len) 125 | for i in 0 ..< b.len: 126 | if b.unsafeGet(i): 127 | result.add "1" 128 | else: 129 | result.add "0" 130 | 131 | func add*(b: BitArray, v: bool) = 132 | ## Add a bit to the end of the array. 133 | let 134 | i = b.len 135 | b.len += 1 136 | if b.len.divUp(64) > b.bits.len: 137 | b.bits.add(0) 138 | if v: 139 | let 140 | bigAt = i div 64 141 | littleAt = i mod 64 142 | mask = 1.uint64 shl littleAt 143 | b.bits[bigAt] = b.bits[bigAt] or mask 144 | 145 | func count*(b: BitArray): int = 146 | ## Returns the number of bits set. 147 | for i in 0 ..< b.bits.len: 148 | result += countSetBits(b.bits[i]) 149 | 150 | func clear*(b: BitArray) = 151 | ## Unsets all of the bits. 152 | for i in 0 ..< b.bits.len: 153 | b.bits[i] = 0 154 | 155 | func hash*(b: BitArray): Hash = 156 | ## Computes a Hash for the bit array. 157 | hash((b.bits, b.len)) 158 | 159 | iterator items*(b: BitArray): bool = 160 | for i in 0 ..< b.len: 161 | yield b.unsafeGet(i) 162 | 163 | iterator pairs*(b: BitArray): (int, bool) = 164 | for i in 0 ..< b.len: 165 | yield (i, b.unsafeGet(i)) 166 | 167 | type BitArray2d* = ref object 168 | ## Creates an array of bits all packed in together. 169 | bits: BitArray 170 | stride: int 171 | 172 | func newBitArray2d*(stride, len: int): BitArray2d = 173 | ## Create a new bit array. 174 | result = BitArray2d() 175 | result.bits = newBitArray(stride * len) 176 | result.stride = stride 177 | 178 | func `[]`*(b: BitArray2d, x, y: int): bool = 179 | b.bits[x * b.stride + y] 180 | 181 | func `[]=`*(b: BitArray2d, x, y: int, v: bool) = 182 | b.bits[x * b.stride + y] = v 183 | 184 | func `and`*(a, b: BitArray2d): BitArray2d = 185 | ## And(s) two bit arrays returning a new bit array. 186 | result = BitArray2d() 187 | result.bits = a.bits and b.bits 188 | result.stride = a.stride 189 | 190 | func `or`*(a, b: BitArray2d): BitArray2d = 191 | ## Or(s) two bit arrays returning a new bit array. 192 | result = BitArray2d() 193 | result.bits = a.bits or b.bits 194 | result.stride = a.stride 195 | 196 | func `not`*(a: BitArray2d): BitArray2d = 197 | ## Not(s) or inverts a and returns a new bit array. 198 | result = BitArray2d() 199 | result.bits = not a.bits 200 | result.stride = a.stride 201 | 202 | func `==`*(a, b: BitArray2d): bool = 203 | ## Are two bit arrays the same. 204 | a.stride == b.stride and b.bits == a.bits 205 | 206 | func hash*(b: BitArray2d): Hash = 207 | ## Computes a Hash for the bit array. 208 | hash((b.bits, b.bits.len, b.stride)) 209 | 210 | func `$`*(b: BitArray2d): string = 211 | ## Turns the bit array into a string. 212 | result = newStringOfCap(b.bits.len) 213 | result.add ("[\n") 214 | for i in 0 ..< b.bits.len: 215 | if i != 0 and i mod b.stride == 0: 216 | result.add "\n" 217 | if i mod b.stride == 0: 218 | result.add " " 219 | if b.bits[i]: 220 | result.add "1" 221 | else: 222 | result.add "0" 223 | result.add ("\n]\n") 224 | -------------------------------------------------------------------------------- /tests/bench.nim: -------------------------------------------------------------------------------- 1 | import benchy, bitty, random, sets, flatty/memoryused 2 | 3 | type TestEnum = enum 4 | Value0 5 | Value1 6 | Value2 7 | Value3 8 | Value4 9 | Value5 10 | Value6 11 | Value7 12 | Value8 13 | Value9 14 | Value10 15 | Value11 16 | Value12 17 | Value13 18 | Value14 19 | Value15 20 | Value16 21 | Value17 22 | Value18 23 | Value19 24 | Value20 25 | Value21 26 | Value22 27 | Value23 28 | Value24 29 | Value25 30 | Value26 31 | Value27 32 | Value28 33 | Value29 34 | Value30 35 | Value31 36 | Value32 37 | Value33 38 | Value34 39 | Value35 40 | Value36 41 | Value37 42 | Value38 43 | Value39 44 | Value40 45 | Value41 46 | Value42 47 | Value43 48 | Value44 49 | Value45 50 | Value46 51 | Value47 52 | Value48 53 | Value49 54 | Value50 55 | Value51 56 | Value52 57 | Value53 58 | Value54 59 | Value55 60 | Value56 61 | Value57 62 | Value58 63 | Value59 64 | Value60 65 | Value61 66 | Value62 67 | Value63 68 | Value64 69 | Value65 70 | Value66 71 | Value67 72 | Value68 73 | Value69 74 | Value70 75 | Value71 76 | Value72 77 | Value73 78 | Value74 79 | Value75 80 | Value76 81 | Value77 82 | Value78 83 | Value79 84 | Value80 85 | Value81 86 | Value82 87 | Value83 88 | Value84 89 | Value85 90 | Value86 91 | Value87 92 | Value88 93 | Value89 94 | Value90 95 | Value91 96 | Value92 97 | Value93 98 | Value94 99 | Value95 100 | Value96 101 | Value97 102 | Value98 103 | Value99 104 | 105 | var 106 | testEnumSet: set[TestEnum] 107 | testEnumHashSet: HashSet[TestEnum] 108 | testEnumBitArray = newBitArray(TestEnum.high.ord + 1) 109 | 110 | timeIt "set": 111 | for i in 0 ..< 1000: 112 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 113 | testEnumSet.incl(rand(TestEnum.low .. TestEnum.high)) 114 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 115 | discard rand(TestEnum.low .. TestEnum.high) in testEnumSet 116 | testEnumSet = {} 117 | 118 | timeIt "hashset": 119 | for i in 0 ..< 1000: 120 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 121 | testEnumHashSet.incl(rand(TestEnum.low .. TestEnum.high)) 122 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 123 | discard rand(TestEnum.low .. TestEnum.high) in testEnumSet 124 | testEnumHashSet.clear() 125 | 126 | timeIt "bitty": 127 | for i in 0 ..< 1000: 128 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 129 | testEnumBitArray[rand(TestEnum.low .. TestEnum.high).ord] = true 130 | for i in 0 ..< rand(0 ..< TestEnum.high.ord): 131 | discard testEnumBitArray[rand(TestEnum.low .. TestEnum.high).ord] 132 | testEnumBitArray.clear() 133 | 134 | echo memoryUsed(testEnumHashSet) 135 | echo memoryUsed(testEnumBitArray) 136 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import bitty 2 | 3 | block: 4 | var a = newBitArray(4) 5 | a[0] = true 6 | a[2] = true 7 | assert $a == "1010" 8 | assert a[0] == true 9 | assert a[1] == false 10 | 11 | block: 12 | var a = newBitArray(4) 13 | a[0] = true 14 | a[1] = true 15 | a[0] = false 16 | assert $a == "0100" 17 | assert a[0] == false 18 | assert a[1] == true 19 | 20 | block: 21 | var a = newBitArray(64) 22 | a[0] = true 23 | a[63] = true 24 | assert $a == "1000000000000000000000000000000000000000000000000000000000000001" 25 | assert a[0] == true 26 | assert a[63] == true 27 | 28 | block: 29 | var a = newBitArray() 30 | a.add true 31 | a.add false 32 | a.add true 33 | a.add false 34 | assert $a == "1010" 35 | assert a[0] == true 36 | assert a[1] == false 37 | 38 | block: 39 | var a = newBitArray() 40 | a.add true 41 | a.add false 42 | a.add true 43 | a.add false 44 | var b = newBitArray() 45 | b.add false 46 | b.add true 47 | assert a.hash() != b.hash() 48 | 49 | block: 50 | var a = newBitArray(4) 51 | var b = newBitArray(4) 52 | assert a == b 53 | 54 | block: 55 | var a = newBitArray() 56 | a.add true 57 | a.add false 58 | a.add true 59 | a.add false 60 | var b = newBitArray() 61 | b.add false 62 | b.add true 63 | b.add false 64 | b.add true 65 | assert $(a and b) == "0000" 66 | 67 | block: 68 | var a = newBitArray() 69 | a.add true 70 | a.add false 71 | a.add true 72 | a.add false 73 | var b = newBitArray() 74 | b.add false 75 | b.add true 76 | b.add false 77 | b.add true 78 | assert $(a or b) == "1111" 79 | 80 | block: 81 | var a = newBitArray() 82 | a.add true 83 | a.add false 84 | a.add true 85 | a.add false 86 | assert $(not a) == "0101" 87 | 88 | block: 89 | var a = newBitArray() 90 | let c = 1_000 91 | for _ in 0 ..< c: 92 | a.add true 93 | assert a.count() == c 94 | a.clear() 95 | assert a.count() == 0 96 | 97 | block: 98 | var a = newBitArray2d(5, 5) 99 | a[1, 1] = true 100 | a[2, 2] = true 101 | a[3, 3] = true 102 | assert $(a) == """[ 103 | 00000 104 | 01000 105 | 00100 106 | 00010 107 | 00000 108 | ] 109 | """ 110 | 111 | block: 112 | var a = newBitArray2d(2, 2) 113 | a[0, 0] = true 114 | a[1, 1] = true 115 | var b = newBitArray2d(2, 2) 116 | b[0, 0] = true 117 | b[1, 0] = true 118 | assert $(a and b) == """[ 119 | 10 120 | 00 121 | ] 122 | """ 123 | 124 | block: 125 | var a = newBitArray2d(2, 2) 126 | a[0, 0] = true 127 | a[1, 1] = true 128 | var b = newBitArray2d(2, 2) 129 | b[0, 0] = true 130 | b[1, 0] = true 131 | assert $(a or b) == """[ 132 | 10 133 | 11 134 | ] 135 | """ 136 | 137 | block: 138 | var a = newBitArray2d(5, 5) 139 | a[1, 1] = true 140 | a[2, 2] = true 141 | a[3, 3] = true 142 | 143 | assert $(not a) == """[ 144 | 11111 145 | 10111 146 | 11011 147 | 11101 148 | 11111 149 | ] 150 | """ 151 | --------------------------------------------------------------------------------