Negative integer representation on computers is obvious

Negative integer representation on computers is obvious

The common explanation of how computers represent negative integers is needlessly complicated, it's actually quite simple when you understand modular arithmetics.

How computers represent positive integers

Let's start with positive integers first. I am going to go rather quickly over how binary representation works, because I assume that anyone that clicked this piece is somewhat interested in tech and maths related stuff, such that they don't struggle with it. But if you do, no worries, it's not actually that difficult and I can explain it here.
Let's first distinguish between the value of a number, which doesn't depend on the base we are working in (base 10, base 2 i.e. binary, or base 16 i.e. hexadecimal to list the main ones) and its representation, the sequence of symbols we use to represent a value, and which does depend on the base we choose.

The way we usually represent numbers is in base 10, the number of fingers we have. That is to say, when we write down '135', the value behind that representation is:
(1 x 100) + (3 x 10) + (5 x 1)
where 100, 10 and 1 are powers of 10 respectively. The value associated with each digit, the symbols from 0, 1, 2 all up to 9, depends on its position. This is why this way of representing numbers is called positional notation, as opposed to how roman numerals work for instance which have to use a different symbol to represent 50, as opposed to 5.

Digits in base 10 go from 0 to 9, such that we have 10 different digits. But this choice of a base is not universal, and for computers for instance it is convenient to represent numbers in base 2, because it means that each digit has only 2 values: 0 (off) or 1 (on), which is very convenient for electrical circuits.
The idea is the same in base 2, we have the powers of 2, which are 1, 2, 4, 8, 16, 32, 64, 128, 256, and so on. Taking the following sequence of digits to be a binary number, 10101, I get that its value is (1 x 16) + (1 x 4) + (1 x 1) = 21.
Because we represent numbers in base 10 by default, and I need a representation to talk about a number's value, let me denote with a suffix (10) for numbers represented in base 10, and the suffix (2) for binary numbers (adapt for whatever base you want). Using this notation, I can say that 10101(2) = 21(10).

This is how we represent positive integers in binary. What might be not so obvious is that every integer value has a unique representation in any integer base we want, such as base 10 or base 2. It is not that difficult to show, but because it's not the main point of this piece, let's just assume it is true, knowing that the justification lies in a proof by contradiction. 1

Positive integers already use modular arithmetics

The way we add two binary numbers is by doing long addition, like what you learn in primary school, except it is done in base 2, with the same idea of the "carry-on" digit if a sum of digits is equal to or exceeds 2, the base we are working in. So let's say we want to add 23 and 12, we first represent them in binary and then add them. So, we get:
23(10) = 16 + 4 + 2 + 1 = 10111(2)
12(10) = 8 + 4 = 1100(2)
10111
+ 01100

100011 = 35(10)

This works perfectly fine, because again value is independent of representation. Computers have an added restriction however, which is that they have a finite number of bytes to work with, since we are dealing with a finite machine, which has finite memory. All my computations so far could be carried on 8 digits, or 8 bits, which is one byte, which can represent all integers from 0 to 2^8 - 1 = 255.
But what happens if the sum of two numbers on a byte exceeds 255? In practice, only the smallest 8 bits are kept. So for instance, 160 + 130 = 290 in base 10, but in base 2, on a single byte, we get:
10100000
+ 10000010

100100010
Truncated down a single byte, which gives us:
00100010

Therefore, on a single byte, 160 + 130 gives us 34(10), as opposed to 290(10). In practice, because we only keep the smallest 8 bits, this means that we are working with a modular arithmetic, and in fact, 34 = 290 (mod 256), which is to say that the two sides of the equality are equal up to a multiple of 256, or equivalently, that their positive difference is a multiple of 256.

Everyone is implicitly familiar with modular arithmetics by the way that clocks display time. We are not shocked to see that the time goes from 23:58 to 00:03 after a few minutes, because we know that a new day has come and the clock has "rolled over" so to speak.
It's worth acknowledging that in general, with modular arithmetics, there is no privileged representative. What I mean by that is that 7 = 17 (mod 10), but then again, we also have 7 = 27 (mod 10), or 7 = 57 (mod 10), or yet again 7 = 117 (mod 10). All these numbers, 7, 17, 27, 57, 117 and infinitely more, are integers which are equivalent to one another, under the "modulo 10" relationship, which is to say that they are all equal to one another up to a multiple of 10, but there is no meaningful reason to priviledge any of them in our representation.
In practice however, people like to use the numbers 0, 1, 2, ..., n-1 when working in (mod n), but it's just a convention, there is no meaningful mathematical reason for that. It's totally valid to say that 7 + 4 = 11 (mod 10), just as we can say that 7 + 4 = 1 (mod 10), which is usually the form that people priviledge, because the latter expresses everything in "reduced" representatives.

Returning to base 2, the crucial part to understand is that because we only work with a finite number of bits, in this case 8 for the sake of demonstration, we are implicitly working with modular arithmetics. The number 25(10) is represented as 00011001(2) on 8 bits, but this is also the representation of 281(10).
Now what happens of course with computers is that they need to assign a single value to each representation, which is why they map the sequence of bits 00011001(2) to 25(10) and not 281(10). And in the case of positive integers, the privileged representatives we take are 0, 1, 2, ..., 2^n - 1, in the case of n bits.

Because of the truncation mechanism that necessarily arises from having a finite number of bits, if I were to count up from 00011001(2) in binary, I would reach 11111111(2), the maximum value on one byte, and then overflow all the way back to 00000000(2). In terms of the value we assign, there is a discontinuity, from 255(10) to 0(10), but in terms of modular arithmetics, we can say that we simply keep adding 1, and that 256 = 0 (mod 256). The problem lies in how discontinously our representation changes, which is a real problem of course, but fundamentally what the binary system over a finite number of bits is is a system for adding numbers in a modular arithmetic, and it's up to us to make sure that the result is coherent with the finite representations we have.

You now understand negative integers

Once you understand that computers represent modular numbers and modular addition with positive integers, you in fact already understand how they represent negative numbers too.
All you have to do is shift your representation window: instead of representing the numbers from 0 to 255, in 8 bits, you represent the ones from -128 to 127, because we want as many negative numbers as positive ones.
So in practice, when you count upwards in negative number representation, the numbers that are represented go as such:
00000000 : 0
00000001 : 1
00000010 : 2
(...)
01111111 : 127
10000000 : -128 = 128 (mod 256)
10000001 : -127 = 129 (mod 256)
(...)
11111111 : -1 = 255 (mod 256)
00000000 : 0 = 256 (mod 256)

The "discontinuity" we see from 127 to -128 is simply a discontinuity in representation, but if we interpret the sequence of bits as an integer modulo 256, there is no discontinuity, it's simply addition.

See how elegant and intuitive the system is? It's just modular arithmetics in base 2, which is what we had in the case of positive integers, but now we simply shifted the window of representation, from 0...255 to -128...127.
Addition works the exact same with negative integers, because we are dealing with modular arithmetic, except that the privileged representative has changed: instead of assigning 128 to 10000000(2), we assign -128, but the two are equal modulo 256, so the result is still correct up to a multiple of 256, we just need to interpret it in a coherent manner.

Just like positive integers have overflow, you have something similar with negative integers, as we have seen. If you add 1 to 127(2) in negative numbers, you get -128, an overflow to a negative number, and if you remove 1 from -128, you get 127, an underflow to a positive number. Again what changes discontinuously is our representation.

The infamous twos complement formula

If you wanted a formula that translates the sequence of bits in binary, to the negative integer we assign to it, then you would get the formula of the two's complement, which is a bit discontinuous as we might expect given the example of counting up I gave.
The problem with the usual explanation of how we represent negative integers is that it sounds completely arbitrary. The one I received went as such:

  1. The intuitive way to represent negative numbers is to use one bit to represent the minus sign, let's say the first bit as a convention. This makes sense so far
  2. But it doesn't work because we have two sequences of bits that map to 0. This is what is told to students, but there is no explanation given as to why it wouldn't work
  3. So instead, when you have a negative sign (largest bit = 1), then the underlying value is obtained by flipping all the other bits, reading it as a positive number, and adding one. Okay sure, that allows the value 0 to only have a single representation, but why are we doing such a convoluted process
  4. We can verify that long addition in binary works with this arcane system of representation. Wait what? How could that be? How could addition work well with such a strange way of assigning a value to a sequence of bits?

Point 4) is probably the hardest part to explain about the two's complement formula. Of course we can assign values to a sequence of bits however we want, but we would like it to be coherent with long addition because that makes it very easy to manipulate, and also means we only need one algorithm to add positive or negative integers.
The usual "proof" that addition still works that way is incredibly tedious, having to treat positive and negative numbers separately, which gives you a lot of cases to treat in isolation and is incredibly inelegant.

When you understand the underlying modular arithmetic, what is going on is actually incredibly simple, and you are no longer surprised that 01111111(2) = 127(10), whereas 10000000(2) = -128(10).
There is also no need to memorize any formula, because you can just assign the value you would get from parsing the sequence of bits as a positive integer, and then go to whatever representative in (mod 256) you need. For instance, if you have 10010110(2), you can decode it as a positive integer as 2 + 4 + 16 + 128 = 150(10), and since 150 = -106 (mod 256), you know that if you are representing negative integers, it would map to -106.
This is the power of understanding maths. Things become coherent as opposed to arbitrary, which means you need to memorize less and can focus more in general principles.

Footnotes

1 See proofs here for instance, the argument with the euclidean division for instance.


Links and tags

Go back to the list of blog posts

Maths

2025-10-31