Offset binary vs two's complement: two ways to represent signed numbers

There are several ways to represent signed numbers in binary, and each makes different trade-offs. The most familiar one is two’s complement, which almost every integer type in every language uses today. But there are two other schemes worth knowing about: sign-magnitude (the intuitive “just add a sign bit” approach, abandoned decades ago because it breaks arithmetic) and offset binary (also called biased exponent or offset-k, where k denotes the offset), which IEEE-754 uses for the exponent of every floating-point number. In this article we’ll walk through all three and learn why sign-magnitude isn’t good enough, how two’s complement fixes its problems, and why IEEE-754 still picked offset binary over two’s complement for its exponents.

A naive first attempt: sign-magnitude

The most intuitive way to extend binary to signed numbers is to reserve the leftmost bit as a sign flag — 0 for positive, 1 for negative — and use the remaining bits to encode the magnitude as an unsigned number. This scheme is called sign-magnitude.

For a 4-bit example, +5 and -5 look like this:

+510=01012510=11012\begin{aligned} +5_{10} &= 0101_2 \\ -5_{10} &= 1101_2 \end{aligned}

Same magnitude 101, just a different top bit. Nice and readable. But this scheme has two serious drawbacks that make it unsuitable for real hardware.

Two zeros

Because the sign bit is independent of the magnitude, there are two representations for zero:

+010=00002010=10002\begin{aligned} +0_{10} &= 0000_2 \\ -0_{10} &= 1000_2 \end{aligned}

Zero is supposed to be one thing. Having two bit patterns for it forces the CPU to special-case equality checks, where 0000 == 1000 must still evaluate to true. It also wastes one of the 16 possible patterns in 4-bit — under sign-magnitude schema we only have 15 distinct values in the range [-7; 7], not the full 16.

Arithmetic doesn’t just work

With unsigned binary, a + b is straightforward — you add bit by bit, propagate carries, done. Sign-magnitude breaks this. Consider 3 + (-3) in 4-bit sign-magnitude:

100112(+3)+  110112(3)111102(?)\begin{aligned} & \phantom{1}0011_2 \quad (+3) \\ +\; & \phantom{1}1011_2 \quad (-3) \\ \hline & \phantom{1}1110_2 \quad (?) \end{aligned}

Reading 1110 as sign-magnitude gives -6 — wildly wrong. Plain binary addition doesn’t know the top bit is supposed to be a sign; it just adds it along with everything else. To make sign-magnitude arithmetic work, the CPU has to inspect both operands’ sign bits, decide whether to add or subtract the magnitudes, possibly compare magnitudes to determine the sign of the result, and handle the two-zeros case separately. All of this translates into extra circuitry and slower operations.

These problems are what motivated the shift to a scheme where the sign is not a separate flag but an arithmetic consequence of the encoding itself — two’s complement.

A quick primer on two’s complement

Two’s complement is the standard encoding for signed integers on modern CPUs, and it is simpler than it sounds.

In two’s complement the leftmost bit (the most significant bit) plays a dual role: it participates in the computation like any other bit, and — when the value is interpreted as a signed integer — it also tells you the sign (0 for non-negative, 1 for negative). It is not a separate flag that gets peeled off; the sign falls out of arithmetic because the top bit’s positional weight is negative.

Let’s take the binary number 1011 and assume it’s a signed integer in two’s complement. To decode it, we use similar positional notation as with unsigned binary, where each bit is given a power of two as its weight — but the top bit’s power is negated. For 4 bits it looks like this:

b3(23)+b222+b121+b020b_3 \cdot (-2^3) + b_2 \cdot 2^2 + b_1 \cdot 2^1 + b_0 \cdot 2^0

Notice what changed compared to plain unsigned binary: the three lower bits b2,b1,b0b_2, b_1, b_0 keep their usual positive weights (+4+4, +2+2, +1+1), but the top bit b3b_3 is multiplied by 23=8-2^3 = -8 instead of +23=+8+2^3 = +8. That single sign flip on the top position is the whole scheme. Generalizing, for an n-bit two’s complement number, the top bit has weight 2n1-2^{n-1} instead of the usual +2n1+2^{n-1}, and all other bits keep their normal positive weights.

Plugging 1011 into the formula gives 8+0+2+1=5-8 + 0 + 2 + 1 = -5 — not 11 (what you’d get by reading 1011 as plain unsigned binary: 8+0+2+18 + 0 + 2 + 1) and not -3 (what sign-magnitude would give: top bit 1 meaning negative, lower bits 011 encoding magnitude 3).

How to encode a negative number

In the previous section we decoded a bit pattern that was already sitting in memory. But how do we go the other way — start with a decimal number like -5 and produce the 4-bit pattern that stores it? The positional formula tells you what a pattern means, but it doesn’t directly tell you how to build one for a given negative value.

Fortunately, there’s a simple recipe that produces the right pattern every time: take the positive version of the number, invert all bits (0 → 1, 1 → 0), and add 1. The result is the two’s complement encoding of the negative value. (We’ll see later, in the modular-arithmetic section, why this recipe works — for now just treat it as a mechanical procedure.)

Let’s use it to find the representation of -5 in 4 bits:

  1. Start with the binary for +5:
+510=01012+5_{10} = 0101_2
  1. Invert all bits:
01012101020101_2 \rightarrow 1010_2
  1. Add 1:
10102+00012=101121010_2 + 0001_2 = 1011_2

So -5 is stored as 1011 in 4-bit two’s complement. Double-checking with the weighted-bits formula: 8+0+2+1=5-8 + 0 + 2 + 1 = -5.

Why this scheme is used

Two’s complement lets a CPU use the same addition circuit (a physical hardware block made of logic gates that performs bit-by-bit addition with carry propagation) for both signed and unsigned arithmetic.

Let’s perform addition of 5 + (-5):

101012(+5)+  110112(5)100002\begin{aligned} & \phantom{1}0101_2 \quad (+5) \\ +\; & \phantom{1}1011_2 \quad (-5) \\ \hline & 10000_2 \end{aligned}

The carry out of the top bit is discarded, leaving 0000 — exactly zero. The hardware doesn’t need to check whether the operands are signed, and it doesn’t need a separate subtraction path. Plain binary addition just works.

Two other properties fall out of this encoding. First, the range for n bits is [2n1; 2n11][-2^{n-1};\ 2^{n-1}-1] — for 4 bits that’s [-8; 7], for 8 bits [-128; 127]. Notice it is asymmetric: one more negative value than positive, because the slot that would represent -0 under a sign-magnitude scheme is repurposed for the most negative number instead. Second, there is exactly one zero pattern 0000 rather than two 0000 and 1000.

The unifying principle: modular arithmetic

Now let’s look at why two’s complement works the way it does. The one idea that explains almost everything about the format is this: n-bit arithmetic is modular arithmetic on a circle of 2n2^n positions. Almost every other property of the format — the flip-and-add-1 recipe, the single-circuit argument, the wrap-around at overflow — follows directly from that single framing.

What “modular arithmetic” means

When we say “n-bit arithmetic is modular,” we mean that all values and all operations happen on a loop of 2n2^n positions rather than on an infinite number line. Picture a 4-bit clock with 16 positions, each carrying two labels: the unsigned value (outer) and the two’s-complement signed value (inner):

outer: unsigned (0–15)inner: signed (−8…+7)same bit pattern, two readings001+12+23+34+45+56+67+78−89−710−611−512−413−314−215−1

Notice how the signed labels split the circle into two halves: the right side holds non-negative values 0+7, the left side holds negative values −1−8 (with position 8 itself — bit pattern 1000 — holding the most-negative value −8).

A few things are worth calling out about how this clock works:

  • Each position holds one bit pattern — the unsigned and signed labels are just two different interpretations of the same 4 bits.
  • Adding 1 moves you clockwise by one position.
  • Past 15, you wrap back to 0. On this clock, 15 + 1 = 0, 15 + 2 = 1, 15 + 3 = 2, and so on — any value past 15 keeps walking around the circle, landing on position 0, then 1, then 2.

Mathematicians have a compact notation for this wrap-around behavior: a ≡ b (mod n) reads as “a lands at the same clock position as b on an n-position clock.” So our wrap-around examples from the bullet above can be written as 16 ≡ 0 (mod 16), 17 ≡ 1 (mod 16), 18 ≡ 2 (mod 16) — each just saying that the number on the left lands at the same position as the number on the right when you walk around a 16-position circle. So 18 ≡ 2 (mod 16) just says that 18 lands on the same position as 2 on the 16-position clock — one full lap (16 steps) plus 2 extra.

Note that a ≡ b (mod n) is a relation (a true-or-false statement comparing two numbers), not an operation — so it doesn’t directly map to %. Its code-level equivalent is the check a % n == b % n — both expressions are true exactly when a and b land at the same clock position. The % operator itself corresponds to a separate piece of notation, a mod n (written without the ), which is an operation: for example, 18 mod 16 = 2 gives the remainder after dividing, which is the exact clock position you land on. For any n, n-bit unsigned arithmetic works exactly like a clock with 2n2^n positions.

Why the flip-and-add-1 recipe works

Now we have enough machinery to see why the recipe from earlier actually produces the right encoding for negative numbers.

The central concept is the additive inverse. On a modular clock, “negative x” isn’t really a minus sign — it’s whichever clock position, when added to x, lands you back at position 0. That position is called the additive inverse of x, and storing it in the bits is exactly what two’s complement does. And the recipe we saw earlier — take the positive version, invert all bits, and add 1 — does just this: it’s a bit-level procedure for computing the additive inverse.

Let’s find the additive inverse of 5 on our 16-position clock. We want some position y such that 5 + y lands on 0. Starting at 5 and walking 11 steps clockwise lands on 16, which wraps to 0. So y = 11:

5+11=160(mod16)5 + 11 = 16 \equiv 0 \pmod{16}

So the additive inverse of 5 on a 16-position clock is 11. And 11 in binary is 1011 — exactly the bit pattern the flip-and-add-1 recipe produced for −5 earlier.

We can now generalize what we did with 5 and 11 to any number of bits. On a 2n2^n-position clock, the additive inverse of any value x is 2nx2^n - x, because

x+(2nx)=2n0(mod2n)x + (2^n - x) = 2^n \equiv 0 \pmod{2^n}

So two’s complement stores −x as the bit pattern for 2nx2^n - x read as unsigned. That’s what we observed on the clock, where the signed negatives −1, −2, …, −8 sit at the same clock positions as the unsigned numbers 15, 14, …, 8 — each one is the additive inverse of its positive counterpart.

Finally, flip-bits-then-add-1 is just a fast bit-level way to compute 2nx2^n - x without doing an actual subtraction:

  • Flipping every bit of xx gives you 2n1x2^n - 1 - x (this intermediate is called the one’s complement). For 4 bits with x=5=x = 5 = 0101: flipping gives 1010, which reads as unsigned 10, and indeed 1615=1016 - 1 - 5 = 10.
  • Adding 1 gives you 2nx2^n - x. Continuing the example: 10+1=1110 + 1 = 11, the additive inverse we computed above.

So the recipe isn’t a clever trick the designers invented — it’s the bit-level shortcut for computing the modular additive inverse.

The whole format in one sentence

Two’s complement is unsigned modular arithmetic with a different labeling of the upper half of the clock. That’s the entire scheme. Every property — the recipe, the single circuit, the wrap-around, the single-zero property — is a consequence of that one idea.

This also explains why a single adder circuit handles both signed and unsigned arithmetic. The hardware doesn’t know or care whether you label positions on the clock as “signed” or “unsigned.” It just adds modulo 2n2^n. Whether you interpret the pattern 1101 as unsigned 13 or signed -3, the CPU treats both operands as clock positions, walks forward by the second operand’s worth of steps, and lands on some position. The two interpretations emerge from how you read the result, not from how the CPU computes it.

Offset binary: the IEEE-754 scheme

Two’s complement is great at what it’s designed for: making signed integer arithmetic the same circuit as unsigned arithmetic. But when you step away from “I want to add and subtract signed integers” and ask different questions of a signed representation, it starts to feel awkward. Questions like:

  • How do I compare two signed values bit-by-bit, the same way a CPU compares unsigned values?
  • Where do the special bit patterns (smallest, largest, zero) naturally fall in the bit-pattern space?
  • Does the encoding cleanly separate “small” from “large” without sign-flag gymnastics?

The MSB-as-sign convention in two’s complement means 1000...0000 (the smallest negative) and 0111...1111 (the largest positive) are at opposite ends of the bit-pattern space, scattered rather than lined up at the boundaries. That’s fine for general-purpose integers, but it gets in the way of formats that need predictable, monotonic signed values — like the exponent field of a floating-point number, where comparing two floats by their exponents is a hot path the hardware needs to make cheap.

So computer arithmetic uses a different signed representation when those properties matter: offset binary (also called biased binary or excess-K). It’s the scheme IEEE-754 uses for the exponent of every floating-point number, and we’ll see exactly why later. For now, let’s look at how it actually works.

The encoding procedure for offset binary is straightforward: calculate an offset (bias), add it to the number you’re trying to store, and the resulting value is what actually gets stored (converted to binary if necessary). To demonstrate the steps let’s see how the number 3 can be stored in 4 bits.

First, we find the offset using the formula mentioned in IEEE-754 standard:

K=2n11K = 2^{n-1} - 1

where n is the number of bits. So the offset for 4 bits is 7. Then, we add offset to the original number: 3 + 7 = 10.

The resulting number 10 is how the number 3 is stored under the offset binary scheme. Since we used decimal system to get resulting number 10, we need to convert it to binary:

10:2=5+05:2=2+12:2=1+01:2=0+11010=10102\begin{aligned} 10 : 2 &= 5 + 0 \\ 5 : 2 &= 2 + 1 \\ 2 : 2 &= 1 + 0 \\ 1 : 2 &= 0 + 1 \\ \\ 10_{10} &= 1010_2 \end{aligned}

If you don’t know the conversion algorithm, or want to know why it works, check out my article on decimal-binary conversion algorithms.

Defining offset

Suppose we have only 4 bits to store numbers. Using the formula for permutations with repetition gives us 24=162^4 = 16 different bit patterns. The question is what those 16 numbers represent. Suppose we’re interested in storing only non-negative integers. Then, the range is:

[0;15]10[0000;1111]2[0; 15]_{10} \qquad [0000; 1111]_2

But if we include negative integers, then the range can vary:

[1;14]10[0000;1111]2[7;8]10[0000;1111]2[8;7]10[0000;1111]2\begin{aligned} [-1; 14]_{10} &\qquad [0000; 1111]_2 \\ [-7; 8]_{10} &\qquad [0000; 1111]_2 \\ [-8; 7]_{10} &\qquad [0000; 1111]_2 \end{aligned}

What’s interesting here is that although we change the range in decimal, in binary it remains the same, only now the lowest number 0000 represents a negative number. It’s as if this lowest number is offset down from zero by 1 in the first case, 7 in the second and 8 in the third.

In other words, the offset K is simply the choice of where to split the fixed range of bit patterns between negatives and non-negatives — it decides how many slots go on each side of zero. There’s no single mathematical standard for how to pick it, just convention. Two choices are common:

K=2n1(equal split, range [2n1; 2n11])K=2n11(IEEE-754, range [(2n11); 2n1])\begin{aligned} K &= 2^{n-1} \quad &\text{(equal split, range } [-2^{n-1};\ 2^{n-1}-1]) \\ K &= 2^{n-1} - 1 \quad &\text{(IEEE-754, range } [-(2^{n-1}-1);\ 2^{n-1}]) \end{aligned}

For 4 bits, K = 8 gives the equal-split range [-8; 7], while K = 7 (IEEE-754’s choice) gives the range [-7; 8] with one extra positive value.

Suppose we need to store number 3 in 4 bits. We’ll use the IEEE-754 bias formula K=2n11K = 2^{n-1} - 1, which for n=4n = 4 gives K=7K = 7. This offset splits the 16 bit patterns into the range [-7; 8], so bit pattern 0000 represents -7 and 1111 represents 8. Now, if 0000 is -7, what number do we need to add to get to 3? It’s 10.

Let’s see what this gives us:

00002+10102=10102710+1010=310\begin{aligned} 0000_2 + 1010_2 &= 1010_2 \\ -7_{10} + 10_{10} &= 3_{10} \end{aligned}

This shows that number 3 is stored as 1010 in binary with offset of 7. Also, it should be easy to spot where the operation of adding the offset to get the number representation in offset binary comes from:

7+10=33+7=10-7 + 10 = 3 \rightarrow 3 + 7 = 10

Finally, as a consequence, if we added the offset to get number representation in offset binary, we should subtract it to convert number back to the original.

Advantages over two’s complement

The biggest advantage of offset binary over two’s complement is that it allows comparing numbers as is using lexicographical order, without the need of additional operations. For example, let’s compare two numbers 3 and -3 represented with 4 bits. Under offset binary they have the following representation:

310=10102310=01002\begin{aligned} 3_{10} &= 1010_2 \\ -3_{10} &= 0100_2 \end{aligned}

Comparing bit by bit, it’s immediately clear for computer from the first bit that the first number is bigger. Lexicographical order can’t be applied to numbers stored as two’s complement:

310=00112310=11012\begin{aligned} 3_{10} &= 0011_2 \\ -3_{10} &= 1101_2 \end{aligned}

To compare them computer has to perform additional operations.

Monotonic ordering is the headline advantage, and a few important consequences fall out of it — and together they explain why IEEE-754 specifically chose offset binary for the floating-point exponent.

The biggest payoff is that an entire positive float — sign, exponent, and mantissa all read as one big unsigned integer — becomes monotonic in the value it represents. That means hardware float-compare circuits can literally reuse the plain integer comparator. Under two’s complement this property would break, forcing dedicated comparison logic for floats.

The scheme also places reserved bit patterns neatly at the boundaries. The all-zero and all-one exponent fields sit at the two extremes of the range, and IEEE-754 uses them as the slots for special values — ±0 and subnormals at the bottom, ±∞ and NaN at the top. The bias (e.g. 1023 for fp64) is picked so the usable exponents fall between those reserved endpoints, and subnormals / gradual underflow work smoothly because they live right next to exact zero at the low end. With two’s complement, the smallest and largest bit patterns would land in the middle of the pattern space instead, making special-value detection clumsy.

So the choice isn’t arbitrary — offset binary is the representation that makes all these properties fall into place at once.

The range is always asymmetric

One property all three schemes share: every binary representation that includes zero has an asymmetric range, off by exactly one value. It’s a counting constraint — with n bits you have 2n2^n codes (an even number), but a symmetric range around zero would need 2k+12k + 1 codes (an odd number). So one side always carries one extra value.

The three schemes handle it differently. Two’s complement puts the extra value on the negative side — for 8 bits, the range is -128 to +127, with -128 (10000000) the lone negative without a positive counterpart. Offset binary lets the bias choose which side gets the extra slot — IEEE-754’s K = 2^{n-1} - 1 gives one extra positive, while K = 2^{n-1} mirrors two’s complement. Sign-magnitude avoids the asymmetric range only by having two zeros (+0 and -0), which breaks arithmetic — so in practice, any scheme that gets arithmetic right has to accept the asymmetry.

You can see this directly in IEEE-754 constants: for fp64, the usable exponent range (after reservations) is -1022 to +1023, still off by one. That’s why the largest finite double is about 1.8×103081.8 \times 10^{308} while the smallest normal positive double is about 2.2×103082.2 \times 10^{-308} — the magnitudes are close but not equal.

For a walkthrough of how the four basic operations (addition, subtraction, multiplication, division) actually work on two’s complement integers — and what happens at the bit level when they overflow or otherwise break — see How binary arithmetic works: two’s complement integers and IEEE-754 floats.