Overview
Ruby implements bitwise operators as methods on Integer objects, providing direct manipulation of individual bits within numeric values. The language supports six primary bitwise operations: AND (&
), OR (|
), XOR (^
), NOT (~
), left shift (<<
), and right shift (>>
). These operators work at the binary level, treating integers as sequences of bits and applying logical or arithmetic operations to each bit position.
Ruby's bitwise operators inherit behavior from the underlying integer representation, working seamlessly with both positive and negative numbers through two's complement arithmetic. The operators return new Integer objects rather than modifying existing values, maintaining Ruby's immutable number semantics.
# Basic bitwise AND operation
binary_a = 0b1010 # 10 in decimal
binary_b = 0b1100 # 12 in decimal
result = binary_a & binary_b
# => 8 (0b1000)
# Shifting operations for power-of-two multiplication
base_value = 5
doubled = base_value << 1 # 10
quadrupled = base_value << 2 # 20
# XOR for toggle operations
flags = 0b11110000
toggle_mask = 0b00001111
toggled = flags ^ toggle_mask
# => 255 (0b11111111)
Bitwise operations find application in flag systems, cryptographic algorithms, performance optimization, bit manipulation puzzles, and low-level system programming. Ruby handles arbitrary precision integers, allowing bitwise operations on numbers of any size without overflow concerns that plague fixed-width integer systems.
The bitwise operators integrate with Ruby's numeric tower, automatically promoting results to appropriate types. Operations between different numeric types follow Ruby's coercion rules, with bitwise operators requiring integer operands or objects that respond to to_int
.
Basic Usage
The AND operator (&
) performs logical conjunction on corresponding bit pairs, returning 1 only when both input bits equal 1. This operation proves essential for masking operations, extracting specific bit patterns, and implementing flag checking systems.
# Extracting lower 4 bits using AND mask
number = 0b11010110
lower_nibble = number & 0b00001111
# => 6 (0b00000110)
# Checking if number is even (last bit is 0)
def even?(n)
(n & 1) == 0
end
even?(42) # => true
even?(17) # => false
The OR operator (|
) performs logical disjunction, setting result bits to 1 when either input bit equals 1. OR operations enable bit setting, flag combination, and value merging operations.
# Setting specific flag bits
permissions = 0b00000000
read_flag = 0b00000100
write_flag = 0b00000010
exec_flag = 0b00000001
full_permissions = permissions | read_flag | write_flag | exec_flag
# => 7 (0b00000111)
# Combining option flags
color_red = 0b001
color_green = 0b010
color_blue = 0b100
purple = color_red | color_blue # => 5 (0b101)
yellow = color_red | color_green # => 3 (0b011)
The XOR operator (^
) produces 1 when input bits differ and 0 when they match. XOR enables toggling operations, parity calculations, and simple encryption techniques.
# Toggle specific bits
status = 0b10101010
toggle_pattern = 0b11110000
new_status = status ^ toggle_pattern
# => 90 (0b01011010)
# Simple encryption/decryption with XOR
def xor_cipher(data, key)
data.bytes.map.with_index { |byte, i| byte ^ key[i % key.length] }
end
message = "HELLO"
key = [0x5A, 0x3C, 0x7E]
encrypted = xor_cipher(message, key)
decrypted = xor_cipher(encrypted.pack('C*'), key)
# decrypted == "HELLO"
The NOT operator (~
) inverts all bits, changing 0 to 1 and 1 to 0. Ruby implements NOT through two's complement arithmetic, producing negative values for positive inputs.
# Bit inversion
original = 0b00001111 # 15
inverted = ~original # -16
# Two's complement demonstration
positive = 42
negative = ~positive + 1 # -42
Left shift (<<
) moves bits leftward, filling vacant positions with zeros. Each left shift by one position multiplies the value by two, providing efficient power-of-two arithmetic.
# Power-of-two multiplication
base = 3
times_four = base << 2 # 12 (3 * 2^2)
times_eight = base << 3 # 24 (3 * 2^3)
# Building bit patterns
pattern = 1
pattern <<= 4 # 16 (0b00010000)
pattern <<= 2 # 64 (0b01000000)
Right shift (>>
) moves bits rightward, performing arithmetic right shift that preserves sign for negative numbers. Right shifting by one position performs integer division by two, discarding fractional parts.
# Integer division by powers of two
dividend = 100
half = dividend >> 1 # 50
quarter = dividend >> 2 # 25
# Sign preservation with negative numbers
negative = -8
shifted = negative >> 1 # -4 (not 2147483644)
Advanced Usage
Bitwise operators combine to create sophisticated bit manipulation patterns. Bit field implementations use multiple operators together to pack and unpack data efficiently, storing several boolean or small integer values within single integers.
class BitField
def initialize(value = 0)
@value = value
end
def set_bit(position)
@value |= (1 << position)
end
def clear_bit(position)
@value &= ~(1 << position)
end
def toggle_bit(position)
@value ^= (1 << position)
end
def bit_set?(position)
(@value & (1 << position)) != 0
end
def extract_range(start, length)
mask = (1 << length) - 1
(@value >> start) & mask
end
def insert_value(value, start, length)
mask = ((1 << length) - 1) << start
@value = (@value & ~mask) | ((value << start) & mask)
end
end
bf = BitField.new
bf.set_bit(3) # Set bit 3
bf.set_bit(7) # Set bit 7
bf.insert_value(5, 0, 3) # Insert value 5 in bits 0-2
# bf.@value is now 0b10001101 (141)
Hash function implementations leverage bitwise operations for efficient key distribution and collision reduction. The XOR operation proves particularly valuable for combining hash components while maintaining good distribution properties.
class CustomHash
def self.hash_combine(*values)
values.reduce(0) do |hash, value|
value_hash = value.hash
hash ^= value_hash + 0x9e3779b9 + (hash << 6) + (hash >> 2)
end
end
def self.fnv1a_hash(string)
hash = 0x811c9dc5
string.each_byte do |byte|
hash ^= byte
hash = (hash * 0x01000193) & 0xffffffff
end
hash
end
end
hash1 = CustomHash.hash_combine("hello", 42, :symbol)
hash2 = CustomHash.fnv1a_hash("test string")
State machine implementations use bitwise flags to represent complex state combinations efficiently. Multiple states can coexist within single integers, with bitwise operations managing state transitions.
class StateMachine
IDLE = 0b0001
RUNNING = 0b0010
PAUSED = 0b0100
ERROR = 0b1000
ACTIVE_STATES = RUNNING | PAUSED
def initialize
@state = IDLE
end
def add_state(state_flag)
@state |= state_flag
end
def remove_state(state_flag)
@state &= ~state_flag
end
def has_state?(state_flag)
(@state & state_flag) != 0
end
def is_active?
(@state & ACTIVE_STATES) != 0
end
def transition_to(new_state)
@state = (@state & ERROR) | new_state
end
def state_description
states = []
states << "IDLE" if has_state?(IDLE)
states << "RUNNING" if has_state?(RUNNING)
states << "PAUSED" if has_state?(PAUSED)
states << "ERROR" if has_state?(ERROR)
states.join(" | ")
end
end
machine = StateMachine.new
machine.transition_to(StateMachine::RUNNING)
machine.add_state(StateMachine::ERROR)
puts machine.state_description # "RUNNING | ERROR"
Cryptographic applications use bitwise operations for key scheduling, substitution operations, and stream cipher implementations. The operations provide controlled bit diffusion and confusion properties essential for cryptographic security.
class SimpleStreamCipher
def initialize(key)
@key = key.bytes
@key_index = 0
end
def encrypt_decrypt(data)
result = []
data.each_byte do |byte|
key_byte = @key[@key_index % @key.length]
encrypted_byte = byte ^ key_byte
# Evolve key state using bitwise operations
@key[@key_index % @key.length] = (key_byte << 1) ^ (key_byte >> 7) ^ encrypted_byte
@key_index = (@key_index + 1) % @key.length
result << encrypted_byte
end
result.pack('C*')
end
end
cipher = SimpleStreamCipher.new("SECRET_KEY")
plaintext = "This is a secret message"
ciphertext = cipher.encrypt_decrypt(plaintext)
cipher2 = SimpleStreamCipher.new("SECRET_KEY")
decrypted = cipher2.encrypt_decrypt(ciphertext)
Performance & Memory
Bitwise operations execute significantly faster than arithmetic alternatives for specific use cases. Left and right shifts provide multiplication and division by powers of two with reduced computational overhead compared to general multiplication and division operations.
require 'benchmark'
def arithmetic_multiply(n, times)
1_000_000.times { n * times }
end
def bitwise_multiply(n, power)
1_000_000.times { n << power }
end
n = 1000
Benchmark.bm(20) do |x|
x.report("arithmetic * 8:") { arithmetic_multiply(n, 8) }
x.report("bitwise << 3:") { bitwise_multiply(n, 3) }
x.report("arithmetic * 16:") { arithmetic_multiply(n, 16) }
x.report("bitwise << 4:") { bitwise_multiply(n, 4) }
end
# Typical results show 2-3x speedup for bitwise operations
Memory efficiency improves dramatically when using bitwise operations to pack multiple values into single integers. Flag systems and enumeration sets benefit from reduced memory footprint and improved cache locality.
# Memory comparison: array vs bitfield for boolean flags
class BooleanArray
def initialize(size)
@flags = Array.new(size, false)
end
def set(index)
@flags[index] = true
end
def get(index)
@flags[index]
end
end
class BitFieldFlags
def initialize(size)
@bits = 0
@size = size
end
def set(index)
@bits |= (1 << index)
end
def get(index)
(@bits & (1 << index)) != 0
end
end
# For 1000 boolean flags:
# BooleanArray: ~8000 bytes (8 bytes per boolean)
# BitFieldFlags: 8 bytes for small counts, scales by powers of two
Bit manipulation algorithms often demonstrate superior performance characteristics for specific problem domains. Population count operations (counting set bits) benefit from specialized bitwise techniques.
def population_count_naive(n)
count = 0
while n > 0
count += 1 if (n & 1) == 1
n >>= 1
end
count
end
def population_count_optimized(n)
count = 0
while n > 0
n &= n - 1 # Clears lowest set bit
count += 1
end
count
end
def population_count_lookup(n)
# Pre-computed table for 8-bit values
@lookup ||= (0..255).map { |i| i.to_s(2).count('1') }
count = 0
while n > 0
count += @lookup[n & 0xFF]
n >>= 8
end
count
end
# Benchmark results typically show:
# Optimized version: 3-5x faster than naive
# Lookup version: 8-12x faster for large numbers
Hash table implementations gain performance benefits from bitwise operations in key distribution and bucket selection algorithms. The operations provide fast modulo alternatives for power-of-two table sizes.
class BitwiseHashTable
def initialize(initial_capacity = 16)
@capacity = initial_capacity
@mask = @capacity - 1 # Works only for power-of-two sizes
@buckets = Array.new(@capacity) { [] }
@size = 0
end
def put(key, value)
index = hash_key(key) & @mask # Fast modulo for power-of-two
bucket = @buckets[index]
existing = bucket.find { |k, v| k == key }
if existing
existing[1] = value
else
bucket << [key, value]
@size += 1
resize if @size > @capacity * 0.75
end
end
def get(key)
index = hash_key(key) & @mask
pair = @buckets[index].find { |k, v| k == key }
pair&.[](1)
end
private
def hash_key(key)
hash = key.hash
# Bit mixing for better distribution
hash ^= hash >> 16
hash ^= hash >> 8
hash ^= hash >> 4
hash
end
def resize
old_buckets = @buckets
@capacity <<= 1 # Double capacity
@mask = @capacity - 1
@buckets = Array.new(@capacity) { [] }
@size = 0
old_buckets.each do |bucket|
bucket.each { |key, value| put(key, value) }
end
end
end
Common Pitfalls
Two's complement arithmetic creates unexpected results when applying bitwise NOT to positive numbers. The NOT operator produces negative values due to sign bit manipulation, not simple bit inversion as expected from other programming contexts.
# Unexpected NOT behavior
positive = 5 # 0b00000101
result = ~positive # -6, not 0b11111010 as might be expected
# The issue: Ruby uses arbitrary precision integers
# ~5 in two's complement becomes -6
# To get bit inversion in a fixed width:
def bit_invert_8bit(value)
(~value) & 0xFF
end
bit_invert_8bit(5) # => 250 (0b11111010)
Operator precedence creates subtle bugs when mixing bitwise operations with arithmetic or comparison operators. Bitwise operators have lower precedence than arithmetic operators, leading to incorrect evaluation order.
# Precedence pitfall
flags = 8
mask = 4
result = flags & mask == 4 # Wrong! Evaluates as: flags & (mask == 4)
# => 0, because (mask == 4) is true, and 8 & true gives unexpected results
# Correct approaches
result = (flags & mask) == 4 # => true
result = flags & mask != 0 # Check if any bits match
Sign extension during right shift operations produces counterintuitive results with negative numbers. Ruby preserves sign bits during arithmetic right shift, which differs from logical right shift behavior expected in unsigned contexts.
# Sign extension surprise
negative = -8 # Binary: ...11111000
shifted = negative >> 1 # -4, not large positive number
# The arithmetic right shift preserves the sign
# -8 >> 1 = -4 (fills with 1s from the left)
# Compare with other languages' unsigned right shift behavior
# To simulate unsigned right shift for specific bit widths:
def unsigned_right_shift_32bit(value, positions)
(value & 0xFFFFFFFF) >> positions
end
Integer coercion rules create type errors when applying bitwise operations to non-integer types. Ruby requires integer operands and does not automatically convert floating-point numbers or other numeric types.
# Type coercion issues
float_val = 3.14
# int_val = float_val & 0xFF # TypeError: no implicit conversion
# Must explicitly convert
int_val = float_val.to_i & 0xFF # => 3
# Objects must implement to_int for coercion
class BitValue
def initialize(value)
@value = value
end
def to_int
@value.to_i
end
end
bit_obj = BitValue.new(42)
result = bit_obj & 15 # Works due to to_int implementation
Large integer operations can produce memory and performance issues when working with extremely large bit patterns. Ruby's arbitrary precision integers consume memory proportional to their magnitude, making bit operations on huge numbers expensive.
# Memory explosion with large integers
huge_number = 2 ** 100_000
# Each bit operation on huge_number requires significant memory
# Better approach: work with smaller chunks
def process_large_bitfield(large_int, chunk_size = 64)
results = []
while large_int > 0
chunk = large_int & ((1 << chunk_size) - 1)
results << process_chunk(chunk)
large_int >>= chunk_size
end
results
end
def process_chunk(chunk)
# Process 64-bit chunks efficiently
chunk & 0xAAAAAAAAAAAAAAAA # Example operation
end
Endianness assumptions break when serializing bitwise operation results across different systems. Ruby's internal integer representation may not match expected byte ordering for network or file protocols.
# Endianness pitfall
value = 0x12345678
bytes = [value].pack('N') # Network (big-endian) byte order
# vs
bytes = [value].pack('V') # VAX (little-endian) byte order
# The packed bytes represent the same number differently
# Network: [0x12, 0x34, 0x56, 0x78]
# VAX: [0x78, 0x56, 0x34, 0x12]
# Ensure consistent byte ordering for portability
def to_big_endian_bytes(value, byte_count)
bytes = []
byte_count.times do |i|
bytes.unshift((value >> (i * 8)) & 0xFF)
end
bytes
end
Mask overflow occurs when bit manipulation operations exceed intended bit width boundaries. Operations may set bits beyond the expected range, corrupting adjacent data fields in packed structures.
# Mask overflow problem
def set_field_value(packed_data, field_start, field_width, value)
# Dangerous: doesn't check if value fits in field_width
mask = ((1 << field_width) - 1) << field_start
packed_data = (packed_data & ~mask) | (value << field_start)
end
# Safe version with bounds checking
def set_field_value_safe(packed_data, field_start, field_width, value)
max_value = (1 << field_width) - 1
raise ArgumentError, "Value #{value} too large for #{field_width}-bit field" if value > max_value
raise ArgumentError, "Negative values not supported" if value < 0
mask = max_value << field_start
(packed_data & ~mask) | ((value & max_value) << field_start)
end
Reference
Bitwise Operators
Operator | Syntax | Description | Example | Result |
---|---|---|---|---|
AND | a & b |
Returns 1 if both bits are 1 | 12 & 10 |
8 |
OR | a + b |
Returns 1 if either bit is 1 | 12 + 10 |
14 |
XOR | a ^ b |
Returns 1 if bits differ | 12 ^ 10 |
6 |
NOT | ~a |
Inverts all bits | ~12 |
-13 |
Left shift | a << n |
Shifts bits left by n positions | 12 << 2 |
48 |
Right shift | a >> n |
Shifts bits right by n positions | 12 >> 2 |
3 |
Common Bit Manipulation Patterns
Operation | Formula | Description |
---|---|---|
Set bit n | value OR (1 << n) |
Sets the nth bit to 1 |
Clear bit n | value & ~(1 << n) |
Sets the nth bit to 0 |
Toggle bit n | value ^ (1 << n) |
Flips the nth bit |
Check bit n | (value & (1 << n)) != 0 |
Tests if nth bit is set |
Clear lowest set bit | value & (value - 1) |
Removes rightmost 1 bit |
Isolate lowest set bit | value & (-value) |
Keeps only rightmost 1 bit |
Check power of two | (value & (value - 1)) == 0 |
True if value is power of 2 |
Extract bit range | (value >> i) & mask |
Extracts bits starting at position i |
Binary Constants and Literals
Format | Example | Decimal Value | Description |
---|---|---|---|
Binary | 0b1010 |
10 |
Binary literal with 0b prefix |
Octal | 0o12 |
10 |
Octal literal with 0o prefix |
Hexadecimal | 0xA |
10 |
Hexadecimal literal with 0x prefix |
Method Availability
Method | Class | Parameters | Returns | Description |
---|---|---|---|---|
& |
Integer | other Integer | Integer | Bitwise AND operation |
+ |
Integer | other Integer | Integer | Bitwise OR operation |
^ |
Integer | other Integer | Integer | Bitwise XOR operation |
~ |
Integer | none | Integer | Bitwise NOT operation |
<< |
Integer | count Integer | Integer | Left shift operation |
>> |
Integer | count Integer | Integer | Right shift operation |
[] |
Integer | bit_position Integer | 0 or 1 | Returns bit at position |
bit_length |
Integer | none | Integer | Number of bits in absolute value |
nobits? |
Integer | mask Integer | Boolean | True if no bits in mask are set |
allbits? |
Integer | mask Integer | Boolean | True if all bits in mask are set |
anybits? |
Integer | mask Integer | Boolean | True if any bits in mask are set |
Conversion Methods
Method | Returns | Description |
---|---|---|
to_s(2) |
String | Binary string representation |
to_s(8) |
String | Octal string representation |
to_s(16) |
String | Hexadecimal string representation |
Integer(string, base) |
Integer | Parse string in specified base |
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
AND, OR, XOR | O(min(log a, log b)) | O(log result) | Dependent on operand sizes |
NOT | O(log a) | O(log a) | Linear in bit length |
Left shift, Right shift | O(log a) | O(log result) | May create larger integers |
Bit test | O(1) | O(1) | Constant time access |
Population count | O(log n) | O(1) | Using bit manipulation tricks |
Error Types
Error | Condition | Example |
---|---|---|
TypeError | Non-integer operand without to_int | 3.14 & 5 |
ArgumentError | Negative shift count | 5 << -1 |
NoMethodError | Bitwise operation on unsupported type | "string" & 5 |
Compatibility Notes
Ruby Version | Changes |
---|---|
2.4+ | Added Integer#allbits?, Integer#anybits?, Integer#nobits? |
2.1+ | Unified Fixnum and Bignum into Integer |
1.9+ | Consistent two's complement behavior |