Goal
The goal of this post is to explain why and how subtraction is executed without the - operation in modern computers.
Background
The CPU uses a set of logic circuits.
- AND
- OR
- XOR
- NOT
- Shift
Early computers had very limited resources, so they had to execute many kinds of operations with only a few logic circuits.
Addition is simple
Addition is very simple. We only use AND, XOR, and shift.
Example:
5 = 0101
3 = 0011
First, we calculate the sum without carry using XOR.
0101
0011
----
0110
Second, we calculate the carry using AND.
0101
0011
----
0001
We shift the carry result to the left because that carry is added to the next bit position.
0001 << 1 = 0010
Finally, we add the XOR result and the shifted carry.
0110
0010
----
1000 = 8 = 5 + 3
So we can perform addition with XOR, AND, and shift.
Subtraction is not simple
In subtraction, we need to handle borrow. For example:
12
- 5
----
2 - 5, we borrow 1 from 12, so 12-5=7
Subtraction is a bit more complex than addition. Should we introduce a new CPU operation? No, we want to keep CPU logic simple. So what should we do? Can we perform subtraction with the existing operations?
Subtraction in binary
We can convert subtraction to addition:
12-5 = 12 + (-5)
So we need a way to represent negative values, and then we can reuse the addition logic.
Complement numbers
A complement is a value that is added to another value to reach a specific total (usually a power of the base). Example:
# base is 10
1000 - 345 = 655
complement of 345 = 655
With a complement, we can convert subtraction to addition:
1000 - 345
= 1000 + 655
= 1655
-> discard the leading digit, then 655
Complement numbers in binary
If we use 8-bit numbers, we can use 2^8 = 256 as the total.
Example:
# -3 in 8-bit
2^8 = 256
256 - 3 = 253
in binary, 11111101 = -3
Let's verify:
3 = 00000011
- 3 = 11111101
00000011
11111101
--------
1 00000000
discard the carry-out bit, and the 8-bit result is 00000000.
We can calculate a negative value in binary as:
-x = ~x + 1
~x flips the bits, and then we add 1.
In Python
In Python, integers are arbitrary precision, so there is no fixed bit width.
In that case, if we keep shifting carry for negative numbers, carry can continue indefinitely. For example:
-3 = ...11111111111111111101
So the binary representation is effectively unbounded in Python. To avoid this, we manually apply a fixed-width mask.
- -1000 <= a, b <= 1000
class Solution:
def getSum(self, a: int, b: int) -> int:
mask = 0xFFFFFFFF # 2^32-1, all 32 bits set to 1
while b != 0:
a, b = (a ^ b) & mask, ((a & b) << 1) & mask
max_int = 0x7FFFFFFF # 2^31-1, max 32-bit signed int
return a if a <= max_int else ~(a ^ mask)
~(a ^ mask):
Example:
a
11111111111111111111111111111101
mask
11111111111111111111111111111111
XOR
00000000000000000000000000000010
This inversion is interpreted in the 32-bit range.
a ^ mask
= 00000000000000000000000000000010
Finally,
# ~x = -(x+1)
~00000000000000000000000000000010
= -3