Bitwise Operations
When I was in 9th grade, I once spent the day going to school with my cousin. At lunchtime, one of her friends was showing off a newfound skill:
"I can count to ten in German! Eins, zwei, drei, vier, funf, sechs, sieben, acht, neun, zehn!"
"That's nothing," I said, "I can count to ten in binary. 1, 10, 11, 100, 101, 110, 111, 1000, 1001, 1010!"
No one got it. And unfortunately, it seems like binary is also foreign to a lot of software developers as well.
What is Binary?
Binary is the two-digit radix (or base) system. In the decimal system - what most people use on a daily basis - we use ten different digits (the Arabic numerals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9) to represent number values. Numbers in the rightmost position before the decimal point represent that digit's value multiplied by ten to the zeroth power, and each position to the right increases the power of ten by one. So 12 is actually another way of saying 1*10^1 + 2*10^0; 433 is equivalent to 4*10^2 + 3*10^1 + 3*10^0; and -82 is equivalent to -8*10^1 + -2*10^0.
In binary, we use only two digits - the Arabic numerals 1 and 0. So in order to represent values greater than 1, we use the same system of digit position representing the base raised to a certain power as a coefficient for each digit. 1 in binary is another way of writing 1*2^0, while 10 is equivalent to 1*2^1, and 1001 is equivalent to 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0. If you calculate that last expression, we can see that 1001 in binary is equal to nine.
What does this matter?
The short answer? For most programming purposes, it doesn't - a binary number has a different symbolic representation than a decimal number, but it has the exact same mathematical properties. In binary, 101 (i.e., five) * 10 (i.e., two) = 1010 (i.e., ten), just like in the decimal system. And in fact, higher-level languages like JavaScript convert all of their mathematical operations on decimal numbers to binary before doing their calculations. Using my speedtest comparison code from my previous post, we can even see that the difference in speed using bitwise operations (i.e., special binary operations, which we'll go over later) like << perform basically equivalently to their corresponding decimal operations:
The long answer? Because computer code is ultimately written is bits of 0 and 1, when your computer executes code, it's doing mathematical operations in binary. So, at a base level it's often easier to think about and use code that actually states those operations outright. For instance, in my post on pseudorandom number generators I linked to a ruby program that used bitwise operations:
In the first highlighted line, "x >> 1" is a bitwise operation that means "shift the binary representation of x to the right one place and remove the leftmost bit". So we could calculate 5 >> 1 as follows: 5 is written as 101 in binary. Moving every digit over one would leave us with 10.1, and we would remove the rightmost 1 and be left with 10, which is two. So 5 >> 1 equals 2.
What the >> operation actually is doing is you are finding the number to the left of the operator's value modulo 2 to the power of the number to the right of the operator's value (in elementary math terms, modulo basically means divide and find the remainder), subtracting it from the number, and then dividing the result by two to the power of the number to the right. Which is sort of complicated to say and a bit wordy in code as well, so we just use >> instead.
But it's the the ^ symbol in the second highlighted line where the rubber really hits the road. ^ is the XOR operation, which is also known as the nim-sum. When you XOR two numbers, you take their binary representations and, for each bit position, you see if there is exactly one 1 between the two representations. And if there is, the XOR sum gets a 1 in that position, and if not, it gets a 0. For example:
10101
+11101
---------
01000
Figuring out how to do this in actual code can be a bit complicated. Here's one way I wrote up:
To be fair, this time difference is due to the fact that we're calling a function, defining variables and performing various operations on them...but that's also the point. Bitwise operations like XOR can easily be accomplished at the assembly level with your regular logic gates; there's no need to spell these things out at a higher level of abstraction.
On top of simplicity and speed, there's lots of neat things you can do with bitwise operators like the XOR (for instance, cryptography applications). Stay tuned for part 2, where we'll go into more detail on various bitwise operators and their applications!
Comments
Post a Comment