Demystifying Bitwise Operators: A Visual Perspective
Max Feige
Posted on May 14, 2023
Introduction
Have you ever found bitwise operators mysterious, despite their simple nature? Their function is clear, but their application and the impact they have on their inputs have eluded me. However, after my recent foray into GLSL shaders it dawned on me that they would be an excellent tool to visualize what these bitwise operators are actually doing. This led to my experiment of using shaders to generate images of bitwise operators.
Image Generation
Each generated image is a square with dimensions 1024×1024 pixels. The generation process involves evaluating each pixel individually. We take into account the current x and y coordinates, and apply our selected operation to them. This operation produces a result between 0 and 1023. To convert this result into a measure of brightness, we divide by the maximum possible value (1023). The process, in pseudocode, would look something akin to this:
Operators
Let’s dive into the world of bitwise operators: the | (OR) operator, the & (AND) operator, and the ^ (XOR) operator.
OR
The OR (|) operator looks at each bit in each number, and if it see’s any 1’s it writes a 1 to the resultant bit. Otherwise its a 0. Let’s illustrate this with an example:
010110 |
100100 =
110110
Not too complicated right? Let’s see what our operator looks like:
Wow! That’s a lot brighter than what I expected. Now there’s a few patterns here you may notice.
Our image seems to be sort composed of squares within squares. Each of these squares is the result of the X or Y coordinate having a bit changing from 0 to 1. The line you see in the middle horizontally represents our Y coordinate 10th bit turning from 0 to 1. Likewise the line vertically represents our X coordinate’s 10th bit turning from 0 to 1.
Let’s delve into a few other intriguing emergent patterns. Our image tends to get brighter as we go further up and further to the right. This is because, in general, the larger a number is the more bits it will have. In fact, the very last number in our range (1023) is composed entirely of 1’s — meaning no matter what number we OR with it the result will be 1023.
There’s also this sort of dark diagonal line going on. This line forms because of a few special mathematical properties. The first is that (x | y) will always result in a number greater than or equal to the maximum of x and y, i.e.
(x | y ) ≥ max(x,y)
To decrease a number is to take one of its bits and turn it from on to off. The OR operator is unable to turn bits off, so it cannot decrease any numbers and thus we must have a number that is larger (or at least equal to) both of the inputs.
So that dark diagonal line is actually forming when X and Y are nearly equal each other. In particular, when X and Y are equal we could say that (x|y) = X = Y. In other words, the diagonal line represents the numbers that affect each other the least in terms of the OR operation!
Next we have an image I found a bit more interesting:
AND
The AND (&) operator scrutinizes each bit in every number. If it spots any 0’s, the resultant bit is set to 0. Otherwise, the resultant bit is set to 1. For example
010110 &
100100 =
000100
Let’s take a look at how this operation appears
Again we see the pattern of square subdivisions – for the exact same reason too. Whenever the X or Y coordinate has a bit that flips, the bitwise operation changes drastically.
You’ll notice a plethora of darker regions in this image, in stark contract to the OR image. Yet these patterns appear for a similar reason.
Just like the OR operation we can come up with a mathematical equation
(x & y ) ≤ min(x,y)
Which means our AND operation only has one pixel at peak brightness – all the way at the top right corner!
Here we see the opposite diagonal pattern of the OR operator. Instead of being at a dark value on the diagonal we see a bright value! This is because the same mathematical fact that was true for OR is true for AND:
(x & y ) = x = y
The difference this time being that because AND has a tendency to decrease numbers, we see a bright line along the diagonal where it doesn’t decrease them.
In fact, if you were to pick out each point that is exactly on the diagonal, you would find that for both the OR image and the AND image that they have the same brightness!
XOR
The XOR (^) operator observes each bit in every number, and checks to see if they differ. If so, it writes a 1 to the resultant bit. Otherwise, it writes a 0. As an example
010110 ^
100100 =
110010
Let’s see XOR in action
This is the most complex of the three basic operations. We still have the same blocky section as before, but a much more interesting pattern
We have a dark diagonal and a light diagonal that cross through each other right in the origin. This is because, when x=y,
(x ^ y ) = 0
What about the light diagonal? Well for the XOR operator to produce a bright color, we would require the binary representations of our two numbers to be opposites – i.e. for every 0 in x there should be a 1 in y and vice versa.
We can also see that, looking at a single square, two of the four quadrants are darker than the others – the upper right and lower left quadrant. This is again because our numbers correlate more in these sections. In both quadrants, as X increases so does Y.
The XOR operator presents us some delightful complexity with its crisscrossing diagonals. It’s my favorite of the three by far.
CODE
You can find code for the shader’s here. Some slight editing will be needed to select the appropriate bitwise operator. Each shader is also available on Github.
Conclusion
I hope this visual exploration of bitwise operators has shed some light on their intricacies! I originally started on this adventure after reading about bithacks. I’ve crafted a series of other images from OR, XOR, and AND operators based on some different binary representations of numbers, which I’ll dive into on my next post. For now, here’s a sneak peek!
Posted on May 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.