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
| Operation | Symbol | Example | Result |
|---|
| AND | & | 1010 & 1100 | 1000 |
| OR | | | 1010 | 1100 | 1110 |
| XOR | ^ | 1010 ^ 1100 | 0110 |
| NOT | ~ | ~1010 | 0101 |
| Left shift | « | 0001 « 2 | 0100 |
| Right shift | » | 1000 » 2 | 0010 |
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
)
|
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:
| Aspect | Relational | Bitmap |
|---|
| Storage | 3 tables + indexes | 1 uint64 field |
| Check permission | JOIN query | Bit operation O(1) |
| Grant permission | INSERT | OR operation |
| Network overhead | Multiple DB roundtrips | None (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
}
|
For 10 million users, 64 tags:
| Approach | Storage | Query Complexity |
|---|
| Traditional table | 10M × 64 rows | O(n) scan |
| Bitmap | 64 × 1.25MB = 80MB | O(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 Case | Bit Operation Advantage |
|---|
| Permission system | O(1) check, 8 bytes stores 64 permissions |
| Tags/Categories | Fast filtering, compound conditions |
| Bloom filter | Space-efficient existence check |
| State machines | Compact multi-state representation |
Key takeaways:
- Bit operations bring CPU instruction-level optimization to application layer
- Space compression + constant time = win-win
- Great for “one of many” or “many of many” scenarios
- Combined with atomics enables lock-free concurrency