This lecture started out with a 30-minute discussion of assertion style. We
referred to the CS 61 Style Guide several times, and the concept
of “Goldilocks assertions”: not too many, not too few. A concrete question was
raised about unused functions; the [[maybe_unused]]
attribute was introduced.
Example 1: Signed integer overflow
int x_incr = x + 1;
assert(x_incr > x);
printf("assertion passed, so %d > %d\n", x_incr, x);
./signedoverflow 1
prints?
assertion passed, so 2 > 1
- Compiled without optimization by clang++,
./signedoverflow 2147483647
prints?
signedoverflow.cc:17: int main(int, char **): Assertion `x_incr > x' failed.
- Because
2147483647 + 1 == -2147483648
in signed computer integer arithmetic!
- Compiled with optimization,
./signedoverflow 2147483647
prints?
assertion passed, so -2147483648 > 2147483647
Sanitizer to the rescue
- Compilers with sanitizers have a different goal—making the code run as fast
as possible, while detecting undefined behavior
$ make SAN=1 signedoverflow
...
$ ./signedoverflow 2147483647
[1msignedoverflow.cc:16:9: [31mruntime error:[30m signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'[m
- Always check your code with sanitizers!
Sanitizers and undefined behavior
- Our makefiles allow you to compile with
make SAN=1
- This turns on sanitizers, which are efficient checkers that catch many undefined behaviors
- Other powerful tools: valgrind, tis-interpreter, etc.
Using computer arithmetic for good
- A bitwise operator is one that operates on the bits of its input(s)
independently
- C and C++ bitwise operators:
~
(unary): complement
&
: bitwise and
|
: bitwise or
^
: bitwise exclusive or
<<
: left shift
>>
: right shift
Truth tables
Beware precedence
- C did not pick the right precedence for bitwise operators
x & 1 != 0
is parsed as x & (1 != 0)
- Most uses of bitwise operators require parentheses
Some mathematical properties
&
, |
, and ^
are commutative: (a&b)==(b&a)
&
, |
, and ^
are associative: (a&(b&c))==((a&b)&c)
&
and |
are distributive: ((a&b)|(a&c)) == (a&(b|c))
, ((a|b)&(a|c)) == (a|(b&c))
&
distributes over ^
: ((a&b)^(a&c)) == (a&(b^c))
0
is the identity for |
and ^
: (x|0)==x
, (x^0)==x
-1
(=~0
) is the identity for &
: (x&-1)==x
0
is the absorbing or annihilating element for &
: (x&0)==0
-1
is the absorbing element for |
: (x|-1)==-1
Left and right shift
a << i
: Move the bits in a
left by i
places, shifting in 0s
a >> i
: Move the bits in a
right by i
places
- If
a
is unsigned, shifting in 0s
- If
a
is signed, shifting in copies of the sign bit
- Like
a = a / 2i
rounded down
- On many computers, left and right shift are much faster than multiplication and (especially) division
i
must be nonnegative and less than 8 * sizeof(a)
Properties of bitwise arithmetic
- Powers of 2 are very important in computer systems programming
- Binary number representation means bitwise expressions often correspond to
other interesting properties
- Some of these properties are analogous to decimal
- For instance, every integer whose decimal representation ends in
0
is
a multiple of 10
- Others are weirder!
Fun with bits
- What general property is tested by
(x & 1) == 0
?
- It tests whether
x
is even
- Can you define unary negation
-x
using other operators (bitwise and +
)?
- What does
x & 15
equal in normal arithmetic?
x % 16
- In general,
(x & (2i - 1))
≡ x
mod 2i
, but is much faster to compute
- What’s the mathematical intuition behind the expression
x - (x & 15)
?
x
rounded down to the nearest multiple of 16
- Like
(x / 16) * 16
, but likely faster
- Equivalent to
x & ~15
!
- What can you learn about
x
from the value x & (x - 1)
?
- Whether
x
is a power of two. (x & (x - 1)) == 0
iff x
is 0 or a power of 2
- What does
x & -x
equal?
- The value of the lowest 1 bit in
x
(5 & -5) == 1
; (10 & -10) == 2
; (8 & -8) == 8
- In unsigned arithmetic, it’s also the gcd (greatest common divisor) of
x
and -x
!
Using bits
- Bitwise operators allow us to efficiently represent small sets
- A set is a collection of elements, each of which might be present or absent
- An integer is a collection of bits, each of which might be 1 or 0
- Set operations, such as union, intersection, and membership-test, correspond
to bitwise operations
Bitset example
- Consider sets taken from the universe {
e
, f
, g
, h
}
- Assign each element
m
a distinct bit value ℬ(m
) (which must be a power of two)
- For example, ℬ(
e
) = 1
, ℬ(f
) = 2
, ℬ(g
) = 4
, ℬ(h
) = 8
- A set of elements is represented as the sum of the ℬ values
- ℬ({}) =
- ℬ({}) =
0
- ℬ({
e
, g
}) =
- ℬ({
e
, g
}) = ℬ(e
) + ℬ(g
) = 5
- ℬ({
e
, f
, g
, h
}) =
- ℬ({
e
, f
, g
, h
}) = 15
- Union and intersection
- ℬ(X ∪ Y) =
- ℬ(X ∪ Y) = (ℬ(X)
|
ℬ(Y))
- ℬ(X ∩ Y) =
- ℬ(X ∩ Y) = (ℬ(X)
&
ℬ(Y))
- Map to bitwise
&
and |
!
- For example, {
e
, f
} ∪ {e
, h
} = {e
, f
, h
}, and indeed (3 | 9) == 11
- Set difference maps to
&
with ~
- ℬ(X ∖ Y) = (ℬ(X)
&
~
ℬ(Y))
- Membership testing maps to
&
m
∈ X ⟺ (ℬ(m
) &
ℬ(X)) != 0
WTF corner
(You don’t need to understand this, but it is fun!)
unsigned long wtf(unsigned long x) {
unsigned long y = x & -x;
unsigned long c = x + y;
return (((x ^ c) >> 2) / y) | c;
}
Bit twiddling hacks — Gosper’s hack