Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Bit Operations

This chapter covers operations on bits, the smallest data units. Bit operations are among the most fundamental features of microprocessors.

System programmers must understand bit operations at least to use flag parameters correctly.

Representation of data at the lowest level

Programming languages are abstractions. A user type like Student defined in a programming language is not directly related to the internals of the computer. Programming languages are tools that help humans use the hardware without needing to know the details of the hardware.

Although it is usually not necessary to deal with the hardware directly, it is helpful to understand how data is represented at hardware level.

Transistor

The processing abilities of modern electronic devices are mostly based on the electronic element called the transistor. A significant ability of the transistor is that it can be controlled by other parts of the electronic circuit that the transistor is a part of. In a way, it allows the electronic circuit be aware of itself and be able to change its own state.

Bit

The smallest unit of information is a bit. A bit can be represented by any two-state system (e.g. by a special arrangement of a few transistors of an electronic circuit). A bit can have one of two values: 0 or 1. In the computer's memory, the information that is stored in a bit persists until a new value is stored or until the energy source is disconnected.

Computers do not provide direct access to bits. One reason is that doing so would complicate the design of the computer and as a consequence make the computer more expensive. Another reason is that there are not many concepts that can be represented by a single bit.

Byte

A byte is a combination of 8 bits. The smallest unit of information that can be addressed uniquely is a byte. Computers read from or write to memory at least one byte at a time.

For that reason, although it carries one bit of information (false or true), even bool must be implemented as one byte:

    writefln("%s is %s byte(s)", bool.stringof, bool.sizeof);
bool is 1 byte(s)
Register

Data that are being operated on in a microprocessor are stored in registers. Registers provide very limited but very fast operations.

The size of the registers depend on the architecture of the microprocessor. For example, 32-bit microprocessors commonly have 4-byte registers and 64-bit microprocessors commonly have 8-byte registers. The size of the registers determine how much information the microprocessor can process efficiently at a time and how many memory addresses that it can support.

Every task that is achieved by a programming language ends up being executed by one or more registers of the microprocessor.

Binary number system

The decimal number system which is used in daily life consists of 10 numerals: 0123456789. In contrast, the binary number system which is used by computer hardware consists of 2 numerals: 0 and 1. This is a direct consequence of a bit consisting of two values. If bits had three values then the computers would use a number system based on three numerals.

The digits of the decimal system are named incrementally as ones, tens, hundreds, thousands, etc. For example, the number 1023 can be expanded as in the following way:

1023 == 1 count of thousand, no hundred, 2 counts of ten, and 3 counts of one

Naturally, moving one digit to the left multiplies the value of that digit by 10: 1, 10, 100, 1000, etc.

When the same rules are applied to a system that has two numerals, we arrive at the binary number system. The digits are named incrementally as ones, twos, fours, eights, etc. In other words, moving one digit to the left would multiply the value of that digit by 2: 1, 2, 4, 8, etc. For example, the binary number 1011 can be expanded as in the following way:

1011 == 1 count of eight, no four, 1 count of two, and 1 count of one

To make it easy to refer to digits, they are numbered from the rightmost digit to the leftmost digit, starting by 0. The following table lists the values of all of the digits of a 32-bit unsigned number in the binary system:

Digit Value
312,147,483,648
301,073,741,824
29536,870,912
28268,435,456
27134,217,728
2667,108,864
2533,554,432
2416,777,216
238,388,608
224,194,304
212,097,152
201,048,576
19524,288
18262,144
17131,072
1665,536
1532,768
1416,384
138,192
124,096
112,048
101,024
9512
8256
7128
664
532
416
38
24
12
01

The bits that have higher values are called the upper bits and the bits that have lower values are called the lower bits.

Remembering from the Literals chapter that binary literals are specified by the 0b prefix, the following program demonstrates how the value of a literal would correspond to the rows of the previous table:

import std.stdio;

void main() {
    //               1073741824                     4 1
    //               ↓                              ↓ ↓
    int number = 0b_01000000_00000000_00000000_00000101;
    writeln(number);
}

The output:

1073741829

Note that the literal consists of only three nonzero bits. The value that is printed is the sum of the values that correspond to those bits from the previous table: 1073741824 + 4 + 1 == 1073741829.

The sign bit of signed integer types

The uppermost bit of a signed type determines whether the value is positive or negative:

    int number = 0b_10000000_00000000_00000000_00000000;
    writeln(number);
-2147483648

However, the uppermost bit is not entirely separate from the value. For example, as evidenced above, the fact that all of the other bits of the number being 0 does not mean that the value is -0. (In fact, -0 is not a valid value for integers.) I will not get into more detail in this chapter other than noting that this is due to the twos complement representation, which is used by D as well.

What is important here is that 2,147,483,648; the highest value in the previous table, is only for unsigned integer types. The same experiment with uint would print that exact value:

    uint number = 0b_10000000_00000000_00000000_00000000;
    writeln(number);
2147483648

Partly for that reason, unless there is a reason not to, bit operations must always be executed on unsigned types: ubyte, ushort, uint, and ulong.

Hexadecimal number system

As can be seen in the literals above, consisting only of 0s and 1s, the binary system may not be readable especially when the numbers are large. For that reason, the more readable hexadecimal system has been widely adopted especially in computer technologies.

The hexadecimal system has 16 numerals. Since alphabets do not have more than 10 numerals, this system borrows 6 letters from the Latin alphabet and uses them along with regular numerals: 0123456789abcdef. The numerals a, b, c, d, e, and f have the values 10, 11, 12, 13, 14, and 15, respectively. The letters ABCDEF can be used as well.

Similar to other number systems, the value of every digit is 16 times the value of the digit on its right-hand side: 1, 16, 256, 4096, etc. For example, the values of all of the digits of an 8-digit unsigned hexadecimal number are the following:

Digit Value
7268,435,456
616,777,216
51,048,576
465,536
34,096
2256
116
01

Remembering that hexadecimal literals are specified by the 0x prefix, we can see how the values of the digits contribute to the overall value of a number:

    //           1048576 4096 1
    //                 ↓  ↓  ↓
    uint number = 0x_0030_a00f;
    writeln(number);
3186703

The value that is printed is by the contributions of all of the nonzero digits: 3 count of 1048576, a count of 4096, and f count of 1. Remembering that a represents 10 and f represents 15, the value is 3145728 + 40960 + 15 == 3186703.

It is straightforward to convert between binary and hexadecimal numbers. In order to convert a hexadecimal number to binary, the digits of the hexadecimal number are converted to their binary representations individually. The corresponding representations in the three number systems are as in the following table:

Hexadecimal Binary Decimal
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
a101010
b101111
c110012
d110113
e111014
f111115

For example, the hexadecimal number 0x0030a00f can be written in the binary form by converting its digits individually according to the previous table:

    // hexadecimal:     0    0    3    0    a    0    0    f
    uint binary = 0b_0000_0000_0011_0000_1010_0000_0000_1111;

Converting from binary to hexadecimal is the reverse: The digits of the binary number are converted to their hexadecimal representations four digits at a time. For example, here is how to write in hexadecimal the same binary value that we have used earlier:

    // binary:           0100 0000 0000 0000 0000 0000 0000 0101
    uint hexadecimal = 0x___4____0____0____0____0____0____0____5;
Bit operations

After going over how values are represented by bits and how numbers are represented in binary and hexadecimal, we can now see operations that change values at bit-level.

Because there is no direct access to individual bits, even though these operations are at bit-level, they affect at least 8 bits at a time. For example, for a variable of type ubyte, a bit operation would be applied to all of the 8 bits of that variable.

As the uppermost bit is the sign bit for signed types, I will ignore signed types and use only uint in the examples below. You can repeat these operations with ubyte, ushort, and ulong; as well as byte, short, int, and long as long as you remember the special meaning of the uppermost bit.

Let's first define a function which will be useful later when examining how bit operators work. This function will print a value in binary, hexadecimal, and decimal systems:

import std.stdio;

void print(uint number) {
    writefln("  %032b %08x %10s", number, number, number);
}

void main() {
    print(123456789);
}

Here is the same value printed in the binary, hexadecimal, and decimal number systems:

  00000111010110111100110100010101 075bcd15  123456789
Complement operator: ~

Not to be confused with the binary ~ operator that is used for array concatenation, this is the unary ~ operator.

This operator converts each bit of a value to its opposite: The bits that are 0 become 1, and the bits that are 1 become 0.

    uint value = 123456789;
    print(value);
    writeln("~ --------------------------------");
    print(~value);

The effect is obvious in the binary representation. Every bit has been reversed (under the dashed line):

  00000111010110111100110100010101 075bcd15  123456789
~ --------------------------------
  11111000101001000011001011101010 f8a432ea 4171510506

Here is the summary of how the unary ~ operator works:

~0 → 1
~1 → 0
And operator: &

& is a binary operator, written between two expressions. The microprocessor considers two corresponding bits of the two expressions separately from all of the other bits: Bits 31, 30, 29, etc. of the expressions are evaluated separately. The value of each resultant bit is 1 if both of the corresponding bits of the expressions are 1; 0 otherwise.

    uint lhs = 123456789;
    uint rhs = 987654321;

    print(lhs);
    print(rhs);
    writeln("& --------------------------------");
    print(lhs & rhs);

The following output contains first the left-hand side expression (lhs) and then the right-hand side expression (rhs). The result of the & operation is under the dashed line:

  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
& --------------------------------
  00000010010110100100100000010001 025a4811   39471121

Note that the bits of the result that have the value 1 are the ones where the corresponding bits of the expressions are both 1.

This operator is called the and operator because it produces 1 when both the left-hand side and the right-hand side bits are 1. Among the four possible combinations of 0 and 1 values, only the one where both of the values are 1 produces 1:

0 & 0 → 0
0 & 1 → 0
1 & 0 → 0
1 & 1 → 1

Observations:

Or operator: |

| is a binary operator, written between two expressions. The microprocessor considers two corresponding bits of the two expressions separately from all of the other bits. The value of each resultant bit is 0 if both of the corresponding bits of the expressions are 0; 1 otherwise.

    uint lhs = 123456789;
    uint rhs = 987654321;

    print(lhs);
    print(rhs);
    writeln("| --------------------------------");
    print(lhs | rhs);
  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
| --------------------------------
  00111111110111111110110110110101 3fdfedb5 1071639989

Note that the bits of the result that have the value 0 are the ones where the corresponding bits of the expressions are both 0. When the corresponding bit in the left-hand side or in the right-hand side is 1, then the result is 1:

0 | 0 → 0
0 | 1 → 1
1 | 0 → 1
1 | 1 → 1

Observations:

Xor operator: ^

Xor is the short for exclusive or. This is a binary operator as well. It produces 1 if the corresponding bits of the two expressions are different:

    uint lhs = 123456789;
    uint rhs = 987654321;

    print(lhs);
    print(rhs);
    writeln("^ --------------------------------");
    print(lhs ^ rhs);
  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
^ --------------------------------
  00111101100001011010010110100100 3d85a5a4 1032168868

Note that the bits of the result that have the value 1 are the ones where the corresponding bits of the expressions are different from each other.

0 ^ 0 → 0
0 ^ 1 → 1
1 ^ 0 → 1
1 ^ 1 → 0

Observation:

Regardless of its value, xorring a variable with itself always produces 0:

    uint value = 123456789;

    print(value ^ value);
  00000000000000000000000000000000 00000000          0
Right-shift operator: >>

This operator shifts the bits of an expression by the specified number of bits to the right. The rightmost bits, which do not have room to shift into, get dropped from the value. For unsigned types, the leftmost bits are filled with zeros.

The following example produces a result by shifting a value by two bits to the right:

    uint value = 123456789;
    print(value);
    print(value >> 2);

In the following output, I highlighted both the bits that are going to be lost due to dropping off from the right-hand side and the leftmost bits that get the value 0:

  00000111010110111100110100010101 075bcd15  123456789
  00000001110101101111001101000101 01d6f345   30864197

Note that the bits that are not highlighted have been shifted two bit positions to the right.

The new bits that enter from the left-hand side are 0 only for unsigned types. For signed types, the value of the leftmost bits are determined by a process called sign extension. Sign extension preserves the value of the sign bit of the original expression. The value of that bit is used for all of the bits that enter from the left.

Let's see this effect on a value of a signed type where the sign bit is 1 (i.e. the value is negative):

    int value = 0x80010300;
    print(value);
    print(value >> 3);

Because the leftmost bit of the original value is 1, all of the new bits of the result are 1 as well:

  10000000000000010000001100000000 80010300 2147549952
  11110000000000000010000001100000 f0002060 4026540128

When the leftmost bit is 0, then all new bits are 0:

    int value = 0x40010300;
    print(value);
    print(value >> 3);
  01000000000000010000001100000000 40010300 1073808128
  00001000000000000010000001100000 08002060  134226016
Unsigned right-shift operator: >>>

This operator works similarly to the regular right-shift operator. The difference is that the new leftmost bits are always 0 regardless of the type of the expression and the value of the leftmost bit:

    int value = 0x80010300;
    print(value);
    print(value >>> 3);
  10000000000000010000001100000000 80010300 2147549952
  00010000000000000010000001100000 10002060  268443744
Left-shift operator: <<

This operator works as the reverse of the right-shift operator. The bits are shifted to the left:

    uint value = 123456789;
    print(value);
    print(value << 4);

The bits on the left-hand side are lost and the new bits on the right-hand side are 0:

  00000111010110111100110100010101 075bcd15  123456789
  01110101101111001101000101010000 75bcd150 1975308624
Operators with assignment

All of the binary operators above have assignment counterparts: &=, |=, ^=, >>=, >>>=, and <<=. Similar to the operators that we saw in the Integers and Arithmetic Operations chapter, these operators assign the result back to the left-hand operand.

Let's see this on the &= operator:

    value = value & 123;
    value &= 123;         // the same as above
Semantics

Merely understanding how these operators work at bit-level may not be sufficient to see how they are useful in programs. The following sections describe common ways that these operators are used in.

| is a union set

The | operator produces the union of the 1 bits in the two expressions.

As an extreme example, let's consider two values that both have alternating bits set to 1. The union of these values would produce a result where all of the bits are 1:

    uint lhs = 0xaaaaaaaa;
    uint rhs = 0x55555555;

    print(lhs);
    print(rhs);
    writeln("| --------------------------------");
    print(lhs | rhs);
  10101010101010101010101010101010 aaaaaaaa 2863311530
  01010101010101010101010101010101 55555555 1431655765
| --------------------------------
  11111111111111111111111111111111 ffffffff 4294967295
& is an intersection set

The & operator produces the intersection of the 1 bits in the two expressions.

As an extreme example, let's consider the last two values again. Since none of the 1 bits of the previous two expressions match the ones in the other expression, all of the bits of the result are 0:

    uint lhs = 0xaaaaaaaa;
    uint rhs = 0x55555555;

    print(lhs);
    print(rhs);
    writeln("& --------------------------------");
    print(lhs & rhs);
  10101010101010101010101010101010 aaaaaaaa 2863311530
  01010101010101010101010101010101 55555555 1431655765
& --------------------------------
  00000000000000000000000000000000 00000000          0
|= sets selected bits to 1

To understand how this works, it helps to see one of the expressions as the actual expression and the other expression as a selector for the bits to set to 1:

    uint expression = 0x00ff00ff;
    uint bitsToSet = 0x10001000;

    write("before     :"); print(expression);
    write("to set to 1:"); print(bitsToSet);

    expression |= bitsToSet;
    write("after      :"); print(expression);

The before and after values of the bits that are affected are highlighted:

before     :  00000000111111110000000011111111 00ff00ff   16711935
to set to 1:  00010000000000000001000000000000 10001000  268439552
after      :  00010000111111110001000011111111 10ff10ff  285151487

In a sense, bitsToSet determines which bits to set to 1. The other bits are not affected.

&= clears selected bits

One of the expressions can be seen as the actual expression and the other expression can be seen as a selector for the bits to clear (to set to 0):

    uint expression = 0x00ff00ff;
    uint bitsToClear = 0xffefffef;

    write("before       :"); print(expression);
    write("bits to clear:"); print(bitsToClear);

    expression &= bitsToClear;
    write("after        :"); print(expression);

The before and after values of the bits that are affected are highlighted:

before       :  00000000111111110000000011111111 00ff00ff   16711935
bits to clear:  11111111111011111111111111101111 ffefffef 4293918703
after        :  00000000111011110000000011101111 00ef00ef   15663343

In a sense, bitsToClear determines which bits to set to 0. The other bits are not affected.

& determines whether a bit is 1 or not

If one of the expressions has only one bit set to 1, then it can be used to query whether the corresponding bit of the other expression is 1:

    uint expression = 123456789;
    uint bitToQuery = 0x00010000;

    print(expression);
    print(bitToQuery);
    writeln(expression & bitToQuery ? "yes, 1" : "not 1");

The bit that is being queried is highlighted:

  00000111010110111100110100010101 075bcd15  123456789
  00000000000000010000000000000000 00010000      65536
yes, 1

Let's query another bit of the same expression by this time having another bit of bitToQuery set to 1:

    uint bitToQuery = 0x00001000;
00000111010110111100110100010101 075bcd15  123456789
00000000000000000001000000000000 00001000       4096
not 1

When the query expression has more than one bit set to 1, then the query would determine whether any of the corresponding bits in the other expression are 1.

Right-shifting by one is the equivalent of dividing by two

Shifting all of the bits of a value by one position to the right produces half of the original value. The reason for this can be seen in the digit-value table above: In that table, every bit has half the value of the bit that is on its left.

Shifting a value to the right multiple bits at a time means dividing by 2 for that many times. For example, right-shifting by 3 bits would divide a value by 8:

    uint value = 8000;

    writeln(value >> 3);
1000

According to how the twos complement system works, right-shifting has the same effect on signed values:

    int value = -8000;

    writeln(value >> 3);
-1000
Left-shifting by one is the equivalent of multiplying by two

Because each bit is two times the value of the bit on its right, shifting a value one bit to the left means multiplying that value by two:

    uint value = 10;

    writeln(value << 5);

Multiplying by 2 a total of 5 times is the same as multiplying by 32:

320
Common uses
Flags

Flags are single-bit independent data that are kept together in the same variable. As they are only one bit wide each, they are suitable for representing binary concepts like enabled/disabled, valid/invalid, etc.

Such one-bit concepts are sometimes encountered in D modules that are based on C libraries.

Flags are usually defined as non-overlapping values of an enum type.

As an example, let's consider a car racing game where the realism of the game is configurable:

These configuration options can be specified at run time by the following enum values:

enum Realism {
    fuelUse    = 1 << 0,
    bodyDamage = 1 << 1,
    tireUse    = 1 << 2,
    skidMarks  = 1 << 3
}

Note that all of those values consist of single bits that do not conflict with each other. Each value is determined by left-shifting 1 by a different number of bits. The corresponding bit representations are the following:

fuelUse   : 0001
bodyDamage: 0010
tireUse   : 0100
skidMarks : 1000

Since their 1 bits do not match others', these values can be combined by the | operator to be kept in the same variable. For example, the two configuration options that are related to tires can be combined as in the following code:

    Realism flags = Realism.tireUse | Realism.skidMarks;
    writefln("%b", flags);

The bits of these two flags would be side-by-side in the variable flags:

1100

Later, these flags can be queried by the & operator:

    if (flags & Realism.fuelUse) {
        // ... code related to fuel consumption ...
    }

    if (flags & Realism.tireUse) {
        // ... code related to tire consumption ...
    }

The & operator produces 1 only if the specified flag is set in flags.

Also note that the result is usable in the if condition due to automatic conversion of the nonzero value to true. The conditional expression is false when the result of & is 0 and true otherwise. As a result, the corresponding code block is executed only if the flag is enabled.

Masking

In some libraries and some protocols an integer value may carry more than one piece of information. For example, the upper 3 bits of a 32-bit value may have a certain meaning, while the lower 29 bits may have another meaning. These separate parts of data can be extracted from the variable by masking.

The four octets of an IPv4 address are an example of this concept. The octets are the individual values that make up the common dotted representation of an IPv4 address. They are all kept in a single 32-bit value. For example, the IPv4 address 192.168.1.2 is the 32-bit value 0xc0a80102:

c0 == 12 * 16 + 0 = 192
a8 == 10 * 16 + 8 = 168
01 ==  0 * 16 + 1 =   1
02 ==  0 * 16 + 2 =   2

A mask consists of a number of 1 bits that would cover the specific part of a variable. "And"ing the value by the mask extracts the part of the variable that is covered by that mask. For example, the mask value of 0x000000ff would cover the lower 8 bits of a value:

    uint value = 123456789;
    uint mask  = 0x000000ff;

    write("value :"); print(value);
    write("mask  :"); print(mask);
    write("result:"); print(value & mask);

The bits that are covered by the mask are highlighted. All of the other bits are cleared:

value :  00000111010110111100110100010101 075bcd15  123456789
mask  :  00000000000000000000000011111111 000000ff        255
result:  00000000000000000000000000010101 00000015         21

Let's apply the same method to the 0xc0a80102 IPv4 address with a mask that would cover the uppermost 8 bits:

    uint value = 0xc0a80102;
    uint mask  = 0xff000000;

    write("value :"); print(value);
    write("mask  :"); print(mask);
    write("result:"); print(value & mask);

This mask covers the uppermost 8 bits of the value:

value :  11000000101010000000000100000010 c0a80102 3232235778
mask  :  11111111000000000000000000000000 ff000000 4278190080
result:  11000000000000000000000000000000 c0000000 3221225472

However, note that the printed result is not the expected 192 but 3221225472. That is because the masked value must also be shifted all the way to the right-hand side. Shifting the value 24 bit positions to the right would produce the value that those 8 bits represent:

    uint value = 0xc0a80102;
    uint mask  = 0xff000000;

    write("value :"); print(value);
    write("mask  :"); print(mask);
    write("result:"); print((value & mask) >> 24);
value :  11000000101010000000000100000010 c0a80102 3232235778
mask  :  11111111000000000000000000000000 ff000000 4278190080
result:  00000000000000000000000011000000 000000c0        192
Exercises
  1. Write a function that returns an IPv4 address in its dotted form:
    string dotted(uint address) {
        // ...
    }
    
    unittest {
        assert(dotted(0xc0a80102) == "192.168.1.2");
    }
    
  2. Write a function that converts four octet values to the corresponding 32-bit IPv4 address:
    uint ipAddress(ubyte octet3,    // most significant octet
                   ubyte octet2,
                   ubyte octet1,
                   ubyte octet0) {  // least significant octet
        // ...
    }
    
    unittest {
        assert(ipAddress(192, 168, 1, 2) == 0xc0a80102);
    }
    
  3. Write a function that can be used for making a mask. It should start with the specified bit and have the specified width:
    uint mask(int lowestBit, int width) {
        // ...
    }
    
    unittest {
        assert(mask(2, 5) ==
               0b_0000_0000_0000_0000_0000_0000_0111_1100);
        //                                            ↑
        //                              lowest bit is 2,
        //                              and the mask is 5-bit wide
    }