Foundationstwo's complementsignednegative numbersarithmeticrepresentation

Two's Complement: One Circuit for Addition and Subtraction

Negative numbers are not stored with a minus sign. They are stored so that ordinary binary addition produces the right answer without the hardware ever knowing a value was negative — which is why subtraction needs no separate circuit.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
How does hardware represent negative numbers, and why does that particular representation make the arithmetic circuitry simpler?
What you wrote
Integers can be negative. The sign is part of the value, and the hardware presumably keeps track of it somewhere.
What the hardware does
There is no sign flag. Negative values are encoded so that the existing adder produces the correct result for both signs, and subtraction is implemented as addition of a transformed operand. The adder does not know or care whether its inputs were meant to be signed.
It explains a family of behaviours that otherwise look arbitrary: why the negative range extends one further than the positive, why the absolute value of the most negative integer is a genuine edge case, why unsigned comparison bugs are so easy to write, and why overflow wraps the way it does.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Why not just use a sign bit?

The obvious design — reserve the top bit for the sign and store the magnitude below it — is called sign-magnitude, and it fails for practical reasons rather than aesthetic ones. It produces two encodings of zero, positive and negative, so every equality comparison needs a special case. Worse, addition needs to inspect both signs and decide whether to add or subtract magnitudes, which means real logic rather than a plain adder.

Two's complement solves both. To negate a value, invert every bit and add one. The representation has a single zero, the top bit still indicates negativity as a side effect, and — the decisive property — ordinary unsigned binary addition produces the correct signed result with no modification. The same adder circuit serves both interpretations.

The table below shows the three candidate schemes on four bits. Watch the zero column and the addition column: two's complement is the only one where the hardware needs no special handling for either.

Three ways to encode a signed integer in four bits
SchemeHow −5 is storedEncodings of zeroWhat addition costs in hardware
Sign-magnitude1101 (sign bit + magnitude 5)Two: 0000 and 1000Inspect both signs, then add or subtract magnitudes — real logic
One's complement1010 (invert 5)Two: 0000 and 1111Plain addition, plus an end-around carry correction step
Two's complement1011 (invert 5, add 1)One: 0000Plain unsigned addition, unmodified — no correction at all

Subtraction disappears into addition

The payoff is that a CPU does not need a subtractor. To compute a - b, the hardware negates b — invert the bits, add one — and feeds it to the adder. The "add one" is free because the adder already has a carry-in input, so negation costs one row of NOT gates and setting a bit that already existed.

This is why Building an Adder: Where Arithmetic Comes From is genuinely the foundational arithmetic lesson: once you have an adder, you have subtraction, comparison (subtract and inspect the flags) and the basis of multiplication. It is also why the same instruction encoding often serves both signed and unsigned addition — the bit pattern produced is identical, and only the *interpretation* of overflow differs.

The worked example below shows the mechanical process. Note that the final carry out of the top bit is simply discarded, and that discarding it is exactly what makes the arithmetic come out right.

8-bit two's complement: computing 42 − 17 as 42 + (−17)
Step 1: represent 17
    17  =  0001 0001

Step 2: negate it (invert all bits, then add one)
  invert  =  1110 1110
  add 1   =  1110 1111   <-  this is -17

Step 3: add 42 and -17 with a plain unsigned adder
     0010 1010    (42)
   + 1110 1111    (-17)
   ------------
   1 0001 1001
   ^
   carry out is discarded

  Result: 0001 1001 = 25   correct

The adder did no sign checking. It added two 8-bit patterns and
threw away the ninth bit; the encoding makes that produce the
right signed answer.

The asymmetry, and the edge case it creates

Two's complement has one visible quirk: the range is not symmetric. In eight bits the values run from −128 to +127, so there is one more negative number than positive. That falls directly out of having a single zero — the encodings have to go somewhere, and with 0000 0000 used once rather than twice, one extra pattern is available on the negative side.

The consequence is a real edge case rather than a curiosity. The most negative value has no positive counterpart, so negating it cannot produce a representable result. Applying the invert-and-add-one procedure to it returns the same value unchanged, which means an absolute-value function can return a negative number — a genuine source of bugs in range-checking and parsing code.

The related trap is mixing signed and unsigned in a comparison. Most languages convert the signed operand to unsigned, at which point a small negative number becomes an enormous positive one and the comparison silently produces the opposite of what was intended. This is a frequent cause of bounds checks that pass when they should fail.

The two edge cases worth knowing by heart
1// 1. The most negative value has no positive counterpart.
2// In 32-bit two's complement the range is -2147483648 .. 2147483647.
3const INT32_MIN = -2147483648
4// abs(INT32_MIN) is not representable. Invert-and-add-one on
5// 1000...0000 returns 1000...0000 -- the same value, still negative.
6// In C or C++ this is undefined behaviour; in Java it silently
7// returns INT32_MIN; in Python integers grow so the case vanishes.
8
9// 2. Mixed signed/unsigned comparison converts the signed side.
10// A negative number becomes a very large positive one.
11function looksSafe(index: number, length: number): boolean {
12 // If this were C with an unsigned length, index = -1 would be
13 // converted to 4294967295 and this check would PASS.
14 return index < length
15}
16// The fix is to check both ends explicitly, and to prefer signed
17// types for anything that can meaningfully be negative:
18function actuallySafe(index: number, length: number): boolean {
19 return index >= 0 && index < length
20}

Key points

  • Two's complement negates by inverting every bit and adding one, giving exactly one encoding of zero.
  • Its decisive property is that ordinary unsigned binary addition produces the correct signed result unmodified.
  • Subtraction is implemented as addition of a negated operand, so no separate subtractor circuit is needed.
  • The range is asymmetric — one more negative value than positive — because zero occupies only one encoding.
  • The most negative value cannot be negated, and mixed signed/unsigned comparison silently converts the signed side.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Value → encoding: a negative number is stored as the bitwise inverse of its magnitude plus one.
  2. 2
    Subtract instruction → negation: the hardware inverts the second operand with a row of NOT gates.
  3. 3
    Carry-in → adder: the "+1" is supplied through the adder's existing carry-in, costing no extra hardware.
  4. 4
    Adder → result: the sum is computed as if both operands were unsigned; the carry out of the top bit is discarded.
  5. 5
    Flags → subsequent branch: the same operation sets condition flags that a later conditional branch reads — see Control Hazards: The CPU Does Not Know Where You Are Going.
What people conclude from this — wrongly
  • Believing the top bit is a sign flag the hardware consults; it is a consequence of the encoding, not a separate field.
  • Assuming the signed range is symmetric and writing bounds checks that omit the extra negative value.
  • Expecting absolute value to always return a non-negative result.
  • Assuming a comparison between a signed and an unsigned value compares them numerically as written.

Consequences, controls and cost

What it causes
  • • One adder circuit serves addition, subtraction and comparison, which saves substantial silicon.
  • • Signed and unsigned addition produce identical bit patterns; only overflow detection differs between them.
  • • Absolute value has an unrepresentable case that behaves differently in every language.
  • • Mixed signed/unsigned comparisons produce silently wrong results, a recurring source of failed bounds checks.
What you can do
  • • Prefer signed types for quantities that can be negative, and avoid mixing signedness in a single comparison.
  • • Guard both ends of a range explicitly rather than relying on a single upper-bound check.
  • • Enable sign-comparison and sign-conversion warnings; compilers detect most of these cases.
  • • Treat the most negative value as an explicit case in any code that negates or takes absolute values.
How to see it
  • • Compile with sign-conversion warnings enabled and treat them as errors in code that does bounds checking.
  • • Unit-test the extreme values of every integer type you use — most sign bugs appear only at the boundaries.
  • • Inspect the disassembly for a subtraction and confirm it became an add with an inverted operand.
What it costs
  • • The asymmetric range is a permanent wart that every language must decide how to handle, and they disagree.
  • • Using wider types to avoid edge cases costs memory and cache footprint.
  • • Signed arithmetic in C and C++ makes overflow undefined, which enables optimisation but removes the wrapping guarantee unsigned types have.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALTwo's complement is used by essentially all contemporary hardware, and C++20 mandates it for signed integer representation. Historical machines used sign-magnitude and one's complement.
  • PLATFORM-SPECIFICWhat happens at the edges is language-defined, not hardware-defined: signed overflow is undefined behaviour in C and C++, wraps in Java and C#, and cannot occur in Python because integers are arbitrary precision.

Misconceptions

Claim
“The first bit is a sign bit the CPU checks.”
Reality
The top bit does indicate negativity, but nothing checks it during addition. The encoding is chosen so plain unsigned addition gives the right signed answer, which is exactly why no check is needed.
Claim
“Signed and unsigned addition are different instructions.”
Reality
On most architectures they are the same instruction producing the same bits. What differs is which overflow condition is meaningful, and therefore which flag a subsequent branch should test.
Claim
“abs() always returns a non-negative number.”
Reality
Not for the most negative value of a fixed-width signed type, which has no positive counterpart. Behaviour there is language-defined and includes returning the same negative value.

Where the rest of this lives

Programming Languages & Runtime Internals
Integer semantics and undefined behaviour

The hardware wraps predictably, but what the *language* permits differs sharply — signed overflow is undefined in C and C++ specifically so the compiler may assume it never happens. That domain does not exist yet.