bitarray

repository·master·Indexed 21 days ago

https://github.com/ilanschnell/bitarray

A C-implemented Python library providing an efficient representation of boolean arrays. It supports high-performance bitwise operations, slicing, the Python buffer protocol for memory sharing, and canonical Huffman coding. The library includes support for both big-endian and little-endian representations and is compatible with free-threaded CPython 3.14+.

Tokens
16.8K
Snippets
57
Records
75
Agent score
72%

What's inside bitarray

  1. Understand the serialization header format

    master

    When using serialize(), the first byte of the resulting bytes object is a header byte used to reconstruct the bitarray.

    • x[0] % 16: The number of pad bits (0..7) within the last byte.
    • x[0] // 16: The bit-endianness (0 for little-endian, 1 for big-endian).

    Valid header values are in the ranges 0 .. 7 or 16 .. 23. For an empty bitarray, the only valid header values are 0 or 16.

  2. Concurrent mutation and exceptions

    master

    While bitarray is memory-safe during concurrent mutation, the results may be non-deterministic.

    • Exceptions: If a bitarray is modified by another thread during an operation, you may encounter IndexError or ValueError (e.g., if an index or mask becomes invalid mid-operation).
    • Code Dictionaries: When using encode(), decode(), or decodetree(), the code dictionaries and their bitarray values are not snapshotted atomically. If you mutate the code dictionary concurrently, the operation may observe a mixture of states or raise an exception. Do not mutate code dictionaries while they are being used for encoding/decoding.
    • Iterators: Changing a bitarray during iteration may cause items to be omitted, repeated, or cause the iteration to end earlier or later than expected.
  3. How the variable-length bitarray format works

    master

    The variable-length format is similar to LEB128 and is optimized for memory efficiency, especially for small bitarrays.

    Encoding Structure:

    • Bit Capacity: A single byte can store up to 4 elements. Every subsequent byte stores up to 7 additional elements.
    • Continuation Bit: The most significant bit (MSB) of each byte is used as a flag: 1 indicates that more bytes follow, and 0 indicates the last byte of the sequence.
    • Padding Information: The first byte contains 3 bits that specify the number of padding bits added to the end of the bitarray to align it with the byte boundaries.

    Example Encoding Process for bitarray('01010110111001110'):

    1. Group bits: The bits are grouped into chunks of 4, then 7, then 7 (e.g., 0101, 0110111, 001110).
    2. Pad: The last group is padded with zeros to complete the 7-bit chunk (e.g., 001110 becomes 0011100).
    3. Add Padding Count: The number of padding bits (in this case, 1) is added to the front of the first group (e.g., 001 + 0101 = 0010101).
    4. Add High Bits: The MSB is set to 1 for all bytes except the final one.
    5. Result: The resulting hex stream is 0x95 0xb7 0x1c.
    from bitarray import bitarray
    from bitarray.util import vl_encode
    
    # Encoding a specific bitarray results in a self-terminating byte stream
    encoded = vl_encode(bitarray('01010110111001110'))
    # Output: b'\x95\xb7\x1c'
  4. Understand sparse bitarray implementations

    master

    The repository provides two distinct approaches for implementing sparse bitarrays, which are efficient when the bitarray contains mostly zeros or mostly ones.

    1. Flips implementation (flips.py): Represents the bitarray using a list of positions where a bit flips (changes from 0 to 1 or 1 to 0).
    2. Ones implementation (ones.py): Represents the bitarray using a sorted list of the positions of all '1' bits, along with the total length of the array.

    Both implementations share common functionality defined in common.py.

  5. Understand bit-endianness in bitarray

    master

    Bit-endianness determines how bits within a byte are mapped to indices in a bitarray.

    • Big-endian (default): The most-significant bit comes first. a[0] is the lowest address and the most significant bit.
    • Little-endian: The least-significant bit comes first. a[0] is the lowest address and the least significant bit.

    When to care about endianness:

    • If you are performing bitwise operations (|, ^, &=, |=, ^=, ~), bitarrays must have the same endianness.
    • If you are interacting with machine representations using .tobytes(), .frombytes(), .tofile(), .fromfile(), or memoryview().
    • If you are using utility functions like int2ba or ba2int.

    If you are only performing logical computations on indices, endianness is transparent and has no effect on the results.

    from bitarray import bitarray
    
    # Default is big-endian
    a = bitarray(b'A')
    print(a.endian)  # 'big'
    print(a)         # bitarray('01000001')
    
    # Explicit little-endian
    b = bitarray(b'A', endian='little')
    print(b.endian)  # 'little'
    print(b)         # bitarray('10000010')
  6. Decode bitarrays using prefix codes

    master

    To decode a bitarray into symbols, you can use a prefix code (a dictionary mapping symbols to bitarrays) or a decodetree object.

    1. Using a dictionary: Pass the dictionary directly to .decode().
    2. Using a decodetree: For better performance, convert the dictionary into a decodetree object first using decodetree(code) and pass that to .decode().

    .decode() returns a decodeiterator which yields the decoded symbols.

    import bitarray
    
    # Define a prefix code
    code = {
        'A': bitarray.bitarray('0'),
        'B': bitarray.bitarray('10'),
        'C': bitarray.bitarray('11')
    }
    
    a = bitarray.bitarray('01011')
    
    # Decode using the dictionary
    for symbol in a.decode(code):
        print(symbol)
    
    # Optimized decoding with decodetree
    tree = bitarray.decodetree(code)
    for symbol in a.decode(tree):
        print(symbol)
  7. How sparse compression (sc) works

    master

    The sparse compression algorithm divides the bitarray into blocks and chooses a block type based on the local population of 1 bits. This allows the algorithm to handle large bitarrays that contain both dense and sparse regions efficiently.

    Binary Blob Structure

    The encoded blob consists of:

    1. Header: Encodes the bit-endianness and the total length (number of bits).
    2. Blocks: An arbitrary number of blocks, each starting with a header encoding the block type and the size of the following data.

    Block Types Reference

    TypeHead ByteCountBytes per IndexEncoded Block SizeDecoded Block Size
    00x00..0x9f0..4096N/A (raw)1..40970..4096
    10xa0..0xbf0..3111..3232
    20xc20..25522..5128,192
    30xc30..25532..7672,097,152
    40xc40..25542..1022536,870,912

    Note: The head byte 0x00 (Type 0 with 0 raw bytes) acts as the stop byte for the decoder.

  8. Thread safety and synchronization in bitarray

    master

    Bitarray uses Python critical sections to ensure memory safety and operation-level consistency.

    What is safe

    • Memory Safety: Concurrent reads and writes will not cause invalid memory access.
    • Operation Consistency: Individual operations (like a.setall(0)) are atomic in terms of the result; a concurrent reader will see either the old state or the new state, never a partially completed state.
    • Multi-operand operations: Operations involving two bitarrays (comparisons, bitwise ops, concatenation, count_and()) protect access to both buffers.

    What is NOT safe

    • Atomic Sequences: A sequence of Python statements is NOT atomic. For example, checking if a: followed by a.pop() is a race condition because another thread could modify a between the two calls.
    • Shared Iterators: Sharing a single iterator (like searchiterator) between threads is not recommended. next() calls from multiple threads do not guarantee exact-once delivery.
    • Shared Buffers: If multiple objects (e.g., a bitarray and a bytearray) share the same underlying memory, they use different locks. Concurrent modification through different objects sharing a buffer is not guaranteed to be coherent.

    When to use user-level locks

    Use threading.Lock when you need to perform a transaction involving multiple operations or when you need to ensure a specific sequence of events remains uninterrupted.

    import threading
    
    lock = threading.Lock()
    a = bitarray(10)
    
    # Use a lock to make a sequence of operations atomic
    with lock:
        if a:
            a.pop()
  9. Basic usage of bitarray objects

    master

    A bitarray behaves similarly to a Python list but is optimized for booleans (8 bits per byte). You can initialize them from strings (whitespace is ignored), iterables, or a specified length.

    Key behaviors:

    • Indexing a single item returns an integer (0 or 1).
    • Slicing returns a new bitarray.
    • append(), extend(), and remove() are available for modification.
    • count(value) returns the number of occurrences of a bit.
    from bitarray import bitarray
    
    a = bitarray()         # create empty bitarray
    a.append(1)
    a.extend([1, 0])
    
    x = bitarray(2 ** 20)  # bitarray of length 1048576 (initialized to 0)
    
    b = bitarray('1001 011')   # initialize from string (whitespace is ignored)
    
    lst = [1, 0, False, True, True]
    c = bitarray(lst)      # initialize from iterable
    
    print(a[2])           # returns 0 (integer)
    print(a[2:4])         # returns bitarray('01')
    print(a.count(1))     # returns count of 1s
  10. Check for free-threading support in CPython

    master

    Bitarray (v3.10.0+) supports free-threaded CPython 3.14 and later. You can check if your Python interpreter was built with free-threading support using sysconfig.get_config_var("Py_GIL_DISABLED").

    On Python 3.14+, you can also check the current runtime state of the GIL using sys._is_gil_enabled(). Note that a free-threaded build can still be run with the GIL enabled, so these two checks serve different purposes.

    import sysconfig
    # Check if the build supports free-threading
    print(sysconfig.get_config_var("Py_GIL_DISABLED"))
    
    import sys
    # Check if the GIL is currently enabled in the runtime
    print(sys._is_gil_enabled())
  11. Encode and decode messages using canonical Huffman codes

    master

    Once you have a canonical Huffman dictionary, you can use the standard bitarray.encode() and bitarray.decode() methods to process messages. To decode without the full dictionary, you can use the canonical_decode() utility with the count and symbol lists generated during the creation process.

    from bitarray import bitarray
    from bitarray.util import canonical_huffman, canonical_decode
    
    # 1. Setup: Create the code
    cnt = {'a': 5, 'b': 3, 'c': 1, 'd': 1, 'r': 2}
    codedict, count, symbol = canonical_huffman(cnt)
    
    # 2. Encoding
    msg = "abracadabra"
    a = bitarray()
    a.encode(codedict, msg)
    # a is now bitarray('01011001110011110101100')
    
    # 3. Decoding using the full dictionary
    assert ''.join(a.decode(codedict)) == msg
    
    # 4. Decoding using only count and symbol (more memory efficient)
    assert ''.join(canonical_decode(a, count, symbol)) == msg