Module 2: Logic
Bitwise logic
Bitwise operations
We have learned the logical operators in C++
x = y && z;
x = y || z;
x = !y;
which computes logical "and", "or" and "not". In addition we have the following operators
x = y & x;
x = y | z;
x = ~y;
x = y ^ z;
which computes logical "and", "or", "not" and "xor" on the bits (=base $2$ digits) of a word in memory. Note that exclusive-or does not have a counterpart for bool.
In addition we have the bitwise operations
x = x << n;
y = y >> n;
which shifts bits $n$ steps to the left/right. These operations are a subset of the bitwise operations that you will normally find among bitwise instructions implemented on the processor.
Clearing and setting bits
Using the bitwise logical operations we are able to clear/set/toggle individual bits of a word. For example the operations
x = x & ~(1 << 10);
x = x | (1 << 10);
x = x ^ (1 << 10);
clears/sets and toggles bit number $10$ in x. In this example
x = x & 0xF0FF;
x = x | 0x0F00;
x = x ^ 0x0F00;
we clear/set/toggle bits $8,9,10$ and $11$. Note that writing a number starting with $0x$ indicates a hexadecimal number in C++. Your compiler may also allow $0b$ to allow for binary number. Also, use
std::cout << std::hex << x;
to write hexadecimally to the console.
More bitwise examples
Multiply, divide and modulo $2^n$:
y = x << n;
y = x >> n;
y = x & ((1<<n) - 1);
Swapping two integers:
x ^= y;
y ^= x;
x ^= y;
Finding position of most significant bit set. This amounts to rounding $log_2(x)$ down to nearest integer.
int c=-1;
while(x) {
x >>= 1;
c++;
}
// c contains position of MSB of x
Computing the exponential $y=2^x$
y = 1 << x;
Clearing the least significant bit
x = x & (x-1);
Counting number of bits set
int c = 0;
while(x) {
c++;
// we clear least signifiant bit
x = x & (x-1);
}
// c contains the number of set bits in y
A simple application
Say you have a large data set of data, say 12GB. You are worried about corruption, and so you want to keep a backup on a different disk, totalling 24GB of storage. We are not happy about that, and would actually like to store considerably less than 24GB. Here is a simple solution. Divide your data set into two chunks of 6GB each and call these X and Y. Then instead, for each byte x and y in X and Y, respectively, we store the byte
z = x^y
which will take up 18GB in total. We will now have stored enough information to reconstruct both X and Y in most cases. If some of the data is corrupted we, we can recompute the old values by doing
oldy = x ^ z
oldx = y ^ z
for each byte of data. Here x and y are possibly corrupted, and oldx and oldy are the reconstructed correct values. We will have correctly reconstructed each byte x and y provided that not both bytes have been corrupted. This follows from the calculation x^z = x^x^y=0^y=y and y^z=y^x^y=x^0=x.
This example can be generalised to any number of data chunks, with higher probability of failure for more chunks. For instance if we divide the 12GB data into 6 chunks of 2GB (stored on separate disks) we only need to store 2GB of backup-data, totalling 14GB of storage. With failure of one disk, we can be certain to reconstruct all the data. The only problem we have is corruption on several disks in the exact same location in the data.