Bitwise operations
Source: CS:APP3e, Bryant and O’Hallaron, Data Lab (from Lab Assignments)
The task is to implement each function, using:
- Only straightline code (no loops or conditionals)
-
Only the following operators: ! ̃ & ˆ + « » - No constants longer than 8 bits
Some functions include additional constraints (see below). Constraints regarding straightline code and long constants are relaxed for the floating-point challenges (see below).
Solutions
bitAnd()
Given two 32-bit signed integers x
and y
, perform a bitwise
AND operation using only ~
and |
- Example: bitAnd(6, 5) => 4
-
Legal ops: ~ - Max ops: 8
- Rating: 1
Explanation: De Morgan’s laws state that
- NOT (A AND B) = (NOT A) OR (NOT B)
- NOT (A OR B) = (NOT A) AND (NOT B)
Therefore A AND B = NOT ((NOT A) OR (NOT B))
1
2
3
int bitAnd(int x, int y) {
return ~ (~x | ~y);
}
allEvenBits()
Given a 32-bit signed integer x
, return 1 if all even-numbered bits
in x
are set to 1.
- Examples:
- allEvenBits(0xFFFFFFFE) = 0
- allEvenBits(0x55555555) = 1
-
Legal ops: ! ~ & ^ + « » - Max ops: 12
- Rating: 2
Explanation: we are not permitted to use a constant value larger than 8 bits, so we cannot mask directly with 0x55555555 (0b_01010101_01010101_01010101_01010101). But we’re not prohibited from constructing such a mask from 8-bit values, for example from 0x55 (0b1010101) shifted left 24, 16, 8, and 0 places respectively.
We first mask with (our constructed) 0x55555555, to determine which
of the even bits in x
are set to 1. Even bits set to 1 in x
will be set to 1 in the product of the masking operation.
Take this result evenBits
and perform evenBits
XOR 0x55555555.
This will produce either an integer value greater than 0, meaning
that at least one even bit in x
was NOT set to 1, or an integer
value of 0, meaning that all of the even bits in x
were set to 1.
Negate this value and we have our specified return value.
Example: 0xFFFFFFFE = 0 (false)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
E E E E E E E E E E E E E E E E
0b_11111111_11111111_11111111_11111110 // x = 0xFFFFFFFE
0b_01010101_01010101_01010101_01010101 // mask = 0x55555555
0b_01010101_01010101_01010101_01010100 // evenBits = x & mask
* // even bits not set to 1 in `x`
0b_00000000_00000000_00000000_00000001 // notAllEvenBitsAreOne = evenBits ^ mask
Example: 0x55555555 = 1 (true)
E E E E E E E E E E E E E E E E
0b_01010101_01010101_01010101_01010101 // x = 0x55555555
0b_01010101_01010101_01010101_01010101 // mask = 0x55555555
0b_01010101_01010101_01010101_01010101 // evenBits = x & mask
// even bits not set to 1 in `x`
0b_00000000_00000000_00000000_00000000 // notAllEvenBitsAreOne = evenBits ^ mask
1
2
3
4
5
6
int allEvenBits(int x) {
int mask = 0x55 << 24 | 0x55 << 16 | 0x55 << 8 | 0x55,
evenBits = x & mask,
notAllEvenBitsAreOne = evenBits ^ mask;
return !notAllEvenBitsAreOne;
}
bitMask()
Given two 32-bit signed integers highbit
and lowbit
representing
a range of bits within a 32-bit pattern, return a 32-bit signed
integer in which all bits from the low bit to the high bit (inclusive)
are set to 1.
- Example: bitMask(5, 3) = 0x38 (0b111000)
- Assume 0 <= lowbit <= 31, and 0 <= highbit <= 31
- If lowbit > highbit, then mask should be all zeroes
-
Legal ops: ! ~ & ^ + « » - Max ops: 16
- Rating: 3
Explanation: we create two masks:
lowBits
: all bits below the specified low bit are set to 1highBits
: all bits above the specified high bit are set to 1
Perform lowBits OR highBits. In the result, zeros occupy the range from the specified low bit to the specified high bit. Negate this pattern and we have our return value.
Example: bitMask(5, 3) = 0x38 (0b111000)
1
2
3
4
5
6
7
0b_00000000_00000000_00000000_00000111 // lowBitsMask = (1 << lowbit) - 1
0b_00000011_11111111_11111111_11111111 // highBits = (1 << (31 - highbit)) - 1
0b_11111111_11111111_11111111_11000000 // highBitsMask = highBits << (highbit + 1)
0b_11111111_11111111_11111111_11000111 // resultMask = lowBitsMask | highBitsMask
0b_00000000_00000000_00000000_00111000 // ~resultMask
This is not an elegant solution. In the limited time available, I was
unable to devise a solution that did not rely on subtraction.
Working around the prohibition of the -
operator makes it ugly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int bitMask(int highbit, int lowbit) {
// Operator `-` is forbidden. (~1 + 1) is equivalent to - 1`.
int x = (~1 + 1);
int lowBitsMask = (1 << lowbit) + x;
// Operator `-` is forbidden. (31 + (~highbit + 1)) is
// equivalent to (31 - highbit).
int highBits = (1 << (31 + (~highbit + 1))) + x;
int highBitsMask = highBits << (highbit + 1),
resultMask = lowBitsMask | highBitsMask;
return ~resultMask;
}
replaceByte()
Given 32-bit signed integers x
, n
, and c
, replace byte n
in
x
with c
.
- Bytes numbered from 0 (LSB) to 3 (MSB)
- Example: replaceByte(0x12345678, 1, 0xab) = 0x1234ab78
- Assume 0 <= n <= 3 and 0 <= c <= 255
-
Legal ops: ! ~ & ^ + « » - Max ops: 10
- Rating: 3
Explanation:
First, we need a mask with 8 bits set to 1 in the position of the
byte we will replace. We create it by shifting the integer byte
position n
(0-3) left three places (because we are working with
32-bit integers, which are 4 bytes long, with byte positions from
0-3). Then, we shift 0xff (0b11111111) by the result.
1
2
3
4
0xff << (0 << 3) => 0b_00000000_00000000_00000000_11111111 // mask for byte 0
0xff << (1 << 3) => 0b_00000000_00000000_11111111_00000000 // mask for byte 1
0xff << (2 << 3) => 0b_00000000_11111111_00000000_00000000 // mask for byte 2
0xff << (3 << 3) => 0b_11111111_00000000_00000000_00000000 // mask for byte 3
Example: n
= 1
1
2
3
byteMask = 0xff << (1 << 3) // 0b_00000000_00000000_11111111_00000000
^^^^^^^^
BYTE 3 BYTE 2 BYTE 1 BYTE 0
Second, we need a mask with c
, the replacement byte, at byte
position n
. We create it by shifting the byte number n
(0-3)
left three places, then shifting c
by the result.
Example: n
= 1, c
= 0xab
1
2
3
cShiftedToByte = 0xab << (1 << 3) // 0b_00000000_00000000_10101011_00000000
^^^^^^^^
BYTE 1
Third, we need to mask out the bits in x
that we will replace.
We do this by masking x
with the complement of byteMask
.
Example: x
= 0x12345678, n
= 1, c
= 0xab
1
2
3
4
5
6
x = 0x12345678 // 0b_00010010_00110100_01010110_01111000
~byteMask // 0b_00000000_00000000_00000000_11111111
xByteZeroed = x & ~byteMask // 0b_00010010_00110100_00000000_01111000
^^^^^^^^
BYTE 1
Finally, we need to replace the bits at byte position n
with c
.
We do this by performing xMasked OR cShiftedToByte
:
Example: x
= 0x12345678, n
= 1, c
= 0xab
1
2
3
xByteZeroed = x & ~byteMask // 0b_00010010_00110100_00000000_01111000
cShiftedToByte = 0xab << (1 << 3) // 0b_00000000_00000000_10101011_00000000
xByteZeroed | cShiftedToByte // 0b_00010010_00110100_10101011_01111000 // 0x1234ab78
1
2
3
4
5
6
int replaceByte(int x, int n, int c) {
int byteMask = 0xff << (n << 3); // 11111111 in byte position `n`
int cShiftedToByte = c << (n << 3); // value of `c` in byte position `n`
int xByteZeroed = x & ~byteMask; // `x` with 00000000 at byte `n`
return xByteZeroed | cShiftedToByte; // `x` with `c` at byte `n`
}
bitParity()
Given a 32-bit signed integer x
, return 1 if x
contains an odd
number of zeroes.
- Examples:
- bitParity(5) = 0
- bitParity(7) = 1
-
Legal ops: ! ~ & ^ + « » - Max ops: 20
- Rating: 4
Explanation: each step sets x
to the value of x
XOR the current
value of x shifted by half as many places as remain in the bit
pattern. By retaining bits set to 1 in either of its operands, these
XOR operations resolve a bit pattern that can be masked with an
integer value of 1 to produce a return value of either 0 or 1.
Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0b_00000000_00000000_00000000_00000101 // x = 5
^ 0b_00000000_00000000_00000000_00000000 // x >> 16
0b_00000000_00000000_00000000_00000101 // x = 5
^ 0b_00000000_00000000_00000000_00000000 // x >> 8
0b_00000000_00000000_00000000_00000101 // x = 5
^ 0b_00000000_00000000_00000000_00000000 // x >> 4
0b_00000000_00000000_00000000_00000101 // x = 5
^ 0b_00000000_00000000_00000000_00000001 // x >> 2
0b_00000000_00000000_00000000_00000100 // x = 4
^ 0b_00000000_00000000_00000000_00000010 // x >> 1
0b_00000000_00000000_00000000_00000110 // x = 6
& 0b_00000000_00000000_00000000_00000001 // 1
0b_00000000_00000000_00000000_00000000 // result: 0
0b_00000000_00000000_00000000_00000111 // x = 7
^ 0b_00000000_00000000_00000000_00000000 // x >> 16
0b_00000000_00000000_00000000_00000111 // x = 7
^ 0b_00000000_00000000_00000000_00000000 // x >> 8
0b_00000000_00000000_00000000_00000111 // x = 7
^ 0b_00000000_00000000_00000000_00000000 // x >> 4
0b_00000000_00000000_00000000_00000111 // x = 7
^ 0b_00000000_00000000_00000000_00000001 // x >> 2
0b_00000000_00000000_00000000_00000110 // x = 6
^ 0b_00000000_00000000_00000000_00000011 // x >> 1
0b_00000000_00000000_00000000_00000101 // x = 5
& 0b_00000000_00000000_00000000_00000001 // 1
0b_00000000_00000000_00000000_00000001 // result: 1
1
2
3
4
5
6
7
8
int bitParity(int x) {
x ^= (x >> 16);
x ^= (x >> 8);
x ^= (x >> 4);
x ^= (x >> 2);
x ^= (x >> 1);
return x & 1;
}
tmin()
Return the minimum value of a two’s complement integer.
-
Legal ops: ! ~ & ^ + « » - Max ops: 4
- Rating: 1
Explanation: minimum value representable by a 32-bit signed integer is -2^31 or -2147483648. In binary two’s complement encoding, this is represented as 0b_10000000_00000000_00000000_00000000. We obtain this by left-shifting the integer value 1 by 31 places.
1
2
3
int tmin(void) {
return 1 << 31;
}
isNegative()
Given a 32-bit signed integer x
, return 1 if x < 0, or 0 otherwise.
- Example: isNegative(-1) = 1
-
Legal ops: ! ~ & ^ + « » - Max ops: 6
- Rating: 2
Explanation: a signed integer in two’s complement encoding is
negative if the most significant bit is set to 1. Test the most
significant bit by masking x
with 0x80000000 (which we derive
by shifting 1 left 31 places, since we are not permitted to use
long constants). This will yield a value representing either zero
or a non-negative value. Negate its negation (in boolean terms)
using the not operator twice (!!
) to get the specified return
value.
Example:
1
2
3
0b_10000000_00000000_00000000_11111111 // x = -1
& 0b_10000000_00000000_00000000_00000000 // 0x80000000
0b_10000000_00000000_00000000_00000000
1
2
3
int isNegative(int x) {
return !!(x & (1 << 31));
}
addOK()
Given two 32-bit signed integers x
and y
, return 1 if x
+ y
can be computed without overflow, otherwise 0.
- Examples:
- addOK(0x80000000,0x80000000) = 0
- addOK(0x80000000,0x70000000) = 1
-
Legal ops: ! ~ & ^ + « » - Max ops: 20
- Rating: 3
Explanation: an overflow will occur if the sign bits (most significant bits) are identical and the sign of their sum is different.
1
2
3
4
5
6
7
8
int addOK(int x, int y) {
int signBitX = x >> 31,
signBitY = y >> 31,
signBitSum = (x + y) >> 31,
xySignBitsAreSame = ~(signBitX ^ signBitY),
signBitOfSumIsDifferent = (signBitX ^ signBitSum);
return !(xySignBitsAreSame & signBitOfSumIsDifferent);
}
absVal()
Given a 32-bit signed integer x
, return the absolute value of x
.
- Example: absVal(-1) = 1.
- You may assume -TMax <= x <= TMax
-
Legal ops: ! ~ & ^ + « » - Max ops: 10
- Rating: 4
Explanation: shift x
right 31 places to create a 32-bit mask in
which all bits are set to the value of the sign bit of x
. This
gives us the following:
1
2
0b_011111111_11111111_11111111_11111111 (0xFFFFFFFF) if x < 0
0b_00000000_00000000_000000000_00000000 (0x00000000) if x >= 0
Adding x
to this mask, then performing an XOR operation with the
sum and the mask yields the absolute value.
Example: x = -1
1
2
3
0b_00000000_00000000_00000000_11111111 // mask
0b_00000000_00000000_00000000_11111110 // x + mask
0b_00000000_00000000_00000000_00000001 // (x + mask) ^ mask
1
2
3
4
int absVal(int x) {
int mask = x >> 31;
return (x + mask) ^ mask;
}
float_neg()
Given a 32-bit unsigned integer uf
representing a 32-bit floating
point value, return an unsigned integer representing the bit-level
representation of -uf.
- When argument is NaN, return argument.
-
Legal ops: Any integer/unsigned operations incl. , &&. also if, while - Max ops: 10
- Rating: 2
Explanation: we need to catch two special cases:
1
2
3
4
5
0x7FC00000 // 0b_01111111_11000000_00000000_00000000
SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
0xFFC00000 // 0b_11111111_11000000_00000000_00000000
SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
In both cases, the exponent is 11111111 and the fraction is greater than 0. This is a value that cannot be represented in 32 bits, and is here considered not a number. In this case, we return the argument. Otherwise, we simply flip the sign bit, by performing an XOR operation with 0x80000000.
1
2
3
4
5
6
unsigned float_neg(unsigned uf) {
unsigned NaN1 = 0x7FC00000, // 0b_01111111_11000000_00000000_00000000
NaN2 = 0xffc00000; // 0b_11111111_11000000_00000000_00000000
if (uf == NaN1 || uf == NaN2) return uf;
return uf ^ 0x80000000;
}
float_half()
Given a 32-bit unsigned integer uf
representing a 32-bit floating
point value, return an unsigned integer representing the bit-level
representation of 0.5 * uf.
- When argument is NaN, return argument
-
Legal ops: Any integer/unsigned operations incl. , &&. also if, while - Max ops: 30
- Rating: 4
1
2
3
4
unsigned float_half(unsigned uf) {
// I didn't solve this one.
return 2;
}