Why were one's complement and sign-magnitude flawed?
Early classical computers experimented with sign-magnitude and one's complement representations to store negative integers in binary, but both approaches introduced significant hardware and mathematical inefficiencies. These systems suffered from having two distinct representations for zero, required complex logic for basic arithmetic, and wasted valuable bit patterns. As a result, computer architects eventually standardized on two's complement, which eliminated these drawbacks and simplified processor design.
The Problem of Dual Zeros
The most famous flaw shared by both sign-magnitude and one's complement is the existence of two different bit patterns for zero: positive zero (\(+0\)) and negative zero (\(-0\)).
In an \(n\)-bit sign-magnitude system, the most significant bit (MSB) represents the sign (\(0\) for positive, \(1\) for negative), while the remaining \(n-1\) bits represent the magnitude. For a 4-bit number:
- \(+0\) is represented as \(0000_2\)
- \(-0\) is represented as \(1000_2\)
In an \(n\)-bit one's complement system, negative numbers are formed by inverting every bit of the positive counterpart. For a 4-bit number:
- \(+0\) is represented as \(0000_2\)
- \(-0\) is represented as \(1111_2\)
Having two zeros created serious complications for hardware and software:
- Equality checks: A processor could not simply compare two bit patterns for equality to check if a value was zero. It had to test for both \(+0\) and \(-0\).
- Wasted range: One of the \(2^n\) possible bit combinations was consumed by a redundant zero, reducing the total number of unique integers that could be represented. An \(n\)-bit system could only represent \(2^n - 1\) distinct values, spanning from \(-(2^{n-1} - 1)\) to \(+(2^{n-1} - 1)\).
Complex Arithmetic Hardware
Modern processors rely on a single, unified adder circuit to perform both addition and subtraction. Neither sign-magnitude nor one's complement allowed for this simplicity.
Sign-Magnitude Arithmetic
In sign-magnitude, the sign bit is separate from the magnitude. When adding two numbers, the hardware first had to inspect both sign bits:
- If the signs were the same, the magnitudes were added, and the common sign was kept.
- If the signs were different, the hardware had to compare the magnitudes, subtract the smaller magnitude from the larger one, and assign the sign of the larger magnitude to the result.
This required separate adder and subtractor circuits, as well as magnitude comparators, increasing the transistor count, cost, and propagation delay of early arithmetic logic units (ALUs).
One's Complement and End-Around Carry
One's complement improved on sign-magnitude by allowing negative numbers to be added directly using a standard binary adder. However, whenever an addition produced a carry-out from the most significant bit, that carry bit could not be discarded. Instead, it had to be added back into the least significant bit—a process known as an end-around carry.
For example, in 4-bit one's complement, adding \(+5\) (\(0101_2\)) and \(-2\) (\(1101_2\)) yields: \[0101_2 + 1101_2 = 10010_2\]
The carry-out bit (\(1\)) is wrapped around and added to the remaining four bits (\(0010_2\)): \[0010_2 + 0001_2 = 0011_2 = +3\]
Implementing an end-around carry required a second addition step or specialized lookahead circuitry, which slowed down arithmetic operations and complicated hardware design. Furthermore, subtracting a number from itself in one's complement often produced \(-0\) (\(1111_2\)) instead of \(+0\) (\(0000_2\)), requiring extra logic to normalize the result.
Why Two's Complement Replaced Them
By the mid-1960s, two's complement became the universal standard for signed integer representation in classical computing. By defining negative numbers as the bit-inverted value plus one, two's complement solved every major flaw of its predecessors:
- Single zero: Only \(0000_2\) represents zero, freeing up the \(1000_2\) pattern to represent \(-2^{n-1}\) and giving an \(n\)-bit system a full range of \(2^n\) unique values.
- Unified arithmetic: Addition and subtraction use the exact same binary adder circuitry without needing magnitude comparisons or end-around carries; any carry-out from the most significant bit is simply ignored.