Contents

Bit Operations in Practice: From Bitmap Indexes to High-Performance Permission Systems

Bit operations aren’t just interview questions. From Redis Bitmaps and Kafka ACLs to Linux file permissions, bit operations are fundamental to high-performance systems. This post demonstrates bit operations in real systems through practical examples.

1. Bit Operations Quick Review

1.1 Basic Operations

OperationSymbolExampleResult
AND&1010 & 11001000
OR|1010 | 11001110
XOR^1010 ^ 11000110
NOT~~10100101
Left shift«0001 « 20100
Right shift»1000 » 20010

1.2 Common Tricks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Set bit n to 1
x |= (1 << n)

// Clear bit n
x &= ^(1 << n)

// Toggle bit n
x ^= (1 << n)

// Check bit n
(x >> n) & 1

// Get lowest set bit
x & (-x)

// Clear lowest set bit
x & (x - 1)

2. Case 1: Linux File Permissions

2.1 Permission Model

1
2
3
4
5
6
7
8
9
-rwxr-xr--  1 user group  4096 Dec 10 10:00 file.txt
 ^^^
 421 (owner: read + write + execute = 7)
    ^^^
    421 (group: read + execute = 5)
       ^^^
       421 (others: read = 4)

# Permission value: 754

2.2 Go Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type Permission uint32

const (
    PermRead    Permission = 1 << 0  // 001 = 1
    PermWrite   Permission = 1 << 1  // 010 = 2
    PermExecute Permission = 1 << 2  // 100 = 4
)

// Check permission
func (p Permission) Has(perm Permission) bool {
    return p&perm == perm
}

// Grant permission
func (p *Permission) Grant(perm Permission) {
    *p |= perm
}

// Revoke permission
func (p *Permission) Revoke(perm Permission) {
    *p &= ^perm
}

// Usage example
func main() {
    var perm Permission = PermRead | PermWrite  // 011 = 3
    
    fmt.Println(perm.Has(PermRead))     // true
    fmt.Println(perm.Has(PermExecute))  // false
    
    perm.Grant(PermExecute)
    fmt.Printf("%03b\n", perm)  // 111
    
    perm.Revoke(PermWrite)
    fmt.Printf("%03b\n", perm)  // 101
}

3. Case 2: RBAC Permission System

3.1 Scenario Analysis

Imagine a SaaS system with these permissions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const (
    PermViewDashboard   uint64 = 1 << 0
    PermEditProfile     uint64 = 1 << 1
    PermCreateProject   uint64 = 1 << 2
    PermDeleteProject   uint64 = 1 << 3
    PermInviteUser      uint64 = 1 << 4
    PermRemoveUser      uint64 = 1 << 5
    PermManageBilling   uint64 = 1 << 6
    PermAccessAdmin     uint64 = 1 << 7
    // ... up to 64 permissions
)

// Predefined roles
const (
    RoleViewer uint64 = PermViewDashboard
    
    RoleEditor uint64 = PermViewDashboard | 
                        PermEditProfile | 
                        PermCreateProject
    
    RoleAdmin uint64 = PermViewDashboard | 
                       PermEditProfile | 
                       PermCreateProject | 
                       PermDeleteProject |
                       PermInviteUser | 
                       PermRemoveUser
    
    RoleOwner uint64 = 0xFFFFFFFFFFFFFFFF  // All permissions
)

3.2 High-Performance Permission Checks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type User struct {
    ID          int64
    Permissions uint64  // Only 8 bytes stores all permissions!
}

// O(1) permission check
func (u *User) Can(perm uint64) bool {
    return u.Permissions&perm == perm
}

// Check multiple permissions (all must match)
func (u *User) CanAll(perms ...uint64) bool {
    var required uint64
    for _, p := range perms {
        required |= p
    }
    return u.Permissions&required == required
}

// Check multiple permissions (any matches)
func (u *User) CanAny(perms ...uint64) bool {
    var required uint64
    for _, p := range perms {
        required |= p
    }
    return u.Permissions&required != 0
}

3.3 Comparison with Traditional Approach

Traditional (relational tables):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- Query user permissions
SELECT p.name 
FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE ur.user_id = ?

-- Check permission
SELECT COUNT(*) 
FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
WHERE ur.user_id = ? AND rp.permission_id = ?

Performance comparison:

AspectRelationalBitmap
Storage3 tables + indexes1 uint64 field
Check permissionJOIN queryBit operation O(1)
Grant permissionINSERTOR operation
Network overheadMultiple DB roundtripsNone (already in memory)

3.4 Database Storage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Save to database
func (u *User) Save(db *sql.DB) error {
    _, err := db.Exec(
        "UPDATE users SET permissions = ? WHERE id = ?",
        u.Permissions,  // Store uint64 directly
        u.ID,
    )
    return err
}

// Load from database
func LoadUser(db *sql.DB, id int64) (*User, error) {
    var u User
    err := db.QueryRow(
        "SELECT id, permissions FROM users WHERE id = ?",
        id,
    ).Scan(&u.ID, &u.Permissions)
    return &u, err
}

4. Case 3: Bitmap Index

4.1 Scenario: User Tag System

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// User tags (up to 64)
const (
    TagPremium      uint64 = 1 << 0
    TagVerified     uint64 = 1 << 1
    TagNewUser      uint64 = 1 << 2
    TagHighValue    uint64 = 1 << 3
    TagChurningRisk uint64 = 1 << 4
    // ...
)

type UserIndex struct {
    // Each tag has a bitmap
    // bitmap[i] bit j means user j has tag i
    bitmaps [64][]uint64
    userCount int
}

4.2 Fast Filtering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Find all "Premium AND Verified AND NOT ChurningRisk" users
func (idx *UserIndex) Query(must, mustNot uint64) []int {
    var result []int
    
    // Calculate match for each block
    blocks := (idx.userCount + 63) / 64
    for b := 0; b < blocks; b++ {
        var match uint64 = 0xFFFFFFFFFFFFFFFF
        
        // AND conditions
        for i := 0; i < 64; i++ {
            if must&(1<<i) != 0 {
                match &= idx.bitmaps[i][b]
            }
        }
        
        // NOT conditions
        for i := 0; i < 64; i++ {
            if mustNot&(1<<i) != 0 {
                match &= ^idx.bitmaps[i][b]
            }
        }
        
        // Extract matching user IDs
        for match != 0 {
            pos := bits.TrailingZeros64(match)
            result = append(result, b*64+pos)
            match &= match - 1  // Clear lowest set bit
        }
    }
    
    return result
}

4.3 Performance Analysis

For 10 million users, 64 tags:

ApproachStorageQuery Complexity
Traditional table10M × 64 rowsO(n) scan
Bitmap64 × 1.25MB = 80MBO(n/64) bit ops

Actual speed: Modern CPUs process 64 bits at once; with SIMD it’s even faster.

5. Case 4: Bloom Filter

5.1 Principle

Bloom filters use multiple hashes + bitmap for efficient “might exist” checks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type BloomFilter struct {
    bits    []uint64
    numHash int
}

func (bf *BloomFilter) Add(item string) {
    for i := 0; i < bf.numHash; i++ {
        hash := bf.hash(item, i)
        idx := hash / 64
        bit := hash % 64
        bf.bits[idx] |= 1 << bit
    }
}

func (bf *BloomFilter) MayContain(item string) bool {
    for i := 0; i < bf.numHash; i++ {
        hash := bf.hash(item, i)
        idx := hash / 64
        bit := hash % 64
        if bf.bits[idx]&(1<<bit) == 0 {
            return false  // Definitely doesn't exist
        }
    }
    return true  // Might exist
}

5.2 Use Cases

  • Redis: Check if key exists
  • Databases: Skip SSTables without target data
  • Crawlers: URL deduplication
  • Recommendation systems: Filter already recommended content

6. Practical Techniques

6.1 More Than 64 Permissions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type LargePermission struct {
    bits []uint64
}

func (p *LargePermission) Set(n int) {
    idx := n / 64
    bit := n % 64
    // Dynamic expansion
    for len(p.bits) <= idx {
        p.bits = append(p.bits, 0)
    }
    p.bits[idx] |= 1 << bit
}

func (p *LargePermission) Has(n int) bool {
    idx := n / 64
    bit := n % 64
    if idx >= len(p.bits) {
        return false
    }
    return p.bits[idx]&(1<<bit) != 0
}

6.2 Atomic Operations (Concurrent-Safe)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import "sync/atomic"

type AtomicPermission struct {
    bits uint64
}

func (p *AtomicPermission) Grant(perm uint64) {
    for {
        old := atomic.LoadUint64(&p.bits)
        new := old | perm
        if atomic.CompareAndSwapUint64(&p.bits, old, new) {
            return
        }
    }
}

func (p *AtomicPermission) Has(perm uint64) bool {
    return atomic.LoadUint64(&p.bits)&perm == perm
}

6.3 JSON Serialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Permission uint64

func (p Permission) MarshalJSON() ([]byte, error) {
    // Convert to permission name list for readability
    names := []string{}
    if p&PermRead != 0 {
        names = append(names, "read")
    }
    if p&PermWrite != 0 {
        names = append(names, "write")
    }
    // ...
    return json.Marshal(names)
}

7. Summary

Use CaseBit Operation Advantage
Permission systemO(1) check, 8 bytes stores 64 permissions
Tags/CategoriesFast filtering, compound conditions
Bloom filterSpace-efficient existence check
State machinesCompact multi-state representation

Key takeaways:

  1. Bit operations bring CPU instruction-level optimization to application layer
  2. Space compression + constant time = win-win
  3. Great for “one of many” or “many of many” scenarios
  4. Combined with atomics enables lock-free concurrency