The CPU, Memory, and Binary

Code along video

Learning objectives

In this code along, we begin our descent from Python toward the machine. We will meet the two main components of a computer, the CPU and RAM, see where Python falls on the spectrum of programming languages, and learn to represent numbers in binary (base 2) and hexadecimal (base 16), which are the languages that the computer itself speaks.

Code along summary

This lesson is conceptual, and so we will not need to write any code to follow along.

A computer is an electrical machine

Our livestream opened with a picture of a huge bank of memory cards. This kind of memory is called RAM (random access memory), and the more of it that a computer has, the more applications it can run at once. Every application needs to store data as it runs. An email app stores incoming messages and contacts; a game stores its dragons and wizards and their attributes. If we have a machine with multiple cores, then we also need plenty of memory available to the programs running on those cores, so that cores and memory go hand in hand.

Underneath it all, a computer is just an electrical machine. If we zoom far enough into the RAM, or into the computer’s central processing unit (CPU), we find many millions or billions of transistors, which are tiny electrical switches (or amplifiers). It is helpful to think of each transistor as being on or off, although it is more accurate to think of it as being in a high or low state. A chip, which is called an integrated circuit in old-school language, consists of a huge number of these transistors linked together.

Because every transistor is in a high or a low state, everything in a computer is ultimately represented using two values, which we write as 1 and 0. You have probably heard the trope that everything in a computer is ones and zeros, but it may not be clear what that has to do with the Python code that we have been writing.

Today’s question

Consider the following mystery function. It takes two integers as input, returns nothing, and prints something at the end. All it does is set some variables, run a while loop, and perform a little bit of arithmetic.

def mystery(r2: int, r3: int) -> None:
    r1 = 1
    r6 = 0
    while r3 != 0:
        r6 += r2
        r3 -= r1
    print(r6)

This function looks like it was written in a very bizarre way, with variable names like r1 and r6. We will hang on to that observation for now, since there is a reason for these names that we will discover in this chapter. Our big question for the chapter is: how do we translate code like this into the electrical instructions of ones and zeros that a computer can understand?

STOP: What do you think that mystery() does? We will not spoil the answer just yet; by the end of this chapter, we will understand this function on the level of ones and zeros.

Frequent words in three languages

Way back at the beginning of this course, we solved the following problem, in which a k-mer is a substring of length k.

Frequent Words Problem
Input: A string text and an integer k.
Output: All most frequent k-mers in text.

This is not a hard problem if you have made it this far in the course. We slide a window of length k along the text, use a dictionary to keep track of how often each k-mer occurs, find the maximum value in the dictionary, and then collect all the k-mers achieving that maximum (there may be a tie).

There are a lot of programming languages, and they all have different strengths and weaknesses. Let us see how a solution to this problem looks in three of them. First, here is a solution in a language called C0 (“C naught”), a version of the C language with a little extra functionality that is used in a course taken by first-year undergraduates in Carnegie Mellon’s School of Computer Science. That course does not really teach the language; students are expected to become comfortable programming in it on their own. C in general is a very fast language with a lot of strengths, but you might look at this code and think that you would not be the most comfortable taking a course in it.

int frequent_words(string text, int k, string[] result) {
  int n = string_length(text);
  if (k <= 0 || k > n) return 0;

  string[] kmers = alloc_array(string, n - k + 1);
  int[] counts = alloc_array(int, n - k + 1);
  int m = 0;

  for (int i = 0; i <= n - k; i++) {
    string kmer = string_sub(text, i, k);
    int idx = -1;
    for (int j = 0; j < m; j++) {
      if (string_equal(kmers[j], kmer)) { idx = j; break; }
    }
    if (idx == -1) { kmers[m] = kmer; counts[m] = 1; m++; }
    else counts[idx]++;
  }

  int maxc = 0;
  for (int i = 0; i < m; i++) if (counts[i] > maxc) maxc = counts[i];

  int r = 0;
  for (int i = 0; i < m; i++) {
    if (counts[i] == maxc) {
      result[r] = kmers[i];
      r++;
    }
  }
  return r;}

Next, here is an implementation in Go, a language with great support for parallel and distributed programming. Like being at the eye doctor, if we asked you whether you prefer lens number one (C0) or lens number two (Go), you probably would not need to have ever programmed before to pick number two. There is something about it that looks a little more natural.

func FrequentWords(text string, k int) ([]string, int) {
	n := len(text)
	if k <= 0 || k > n {
		return []string{}, 0
	}
	counts := make(map[string]int, n-k+1)
	for i := 0; i <= n-k; i++ {
		kmer := text[i : i+k]
		counts[kmer]++
	}
	maxCount := 0
	for _, c := range counts {
		if c > maxCount {
			maxCount = c
		}
	}
	var result []string
	for kmer, c := range counts {
		if c == maxCount {
			result = append(result, kmer)
		}
	}
	sort.Strings(result) // deterministic order
	return result, maxCount
}

Finally, here is the Python implementation. If we flip the lenses again and ask whether you prefer Go or Python, you would probably again pick number two.

def frequent_words(text: str, k: int) -> list[str]:
    if k <= 0 or k > len(text):
        return []

    counts: dict[str, int] = {}
    for i in range(len(text) - k + 1):
        kmer = text[i:i + k]
        counts[kmer] = counts.get(kmer, 0) + 1

    max_count = max(counts.values())
    result: list[str] = []
    for kmer, c in counts.items():
        if c == max_count:
            result.append(kmer)
    return sorted(result)
STOP: What do you notice about these three implementations of the same algorithm?

The Python code uses fewer lines, and it also looks quite a bit closer to human language than the other two. C0 looks alien, whereas Python may look familiar even if you have never written a line of code in your life. This is why very few programming courses for beginners are taught in C. Some introductory courses use a language like Go (in fact, Programming for Lovers used Go as its model language for a long time), but many people want to come to programming through a language like Python. Looking familiar is frankly not the best reason to pick a language, but it is a big reason why so many people have chosen Python as their first one.

Programming languages fall on a spectrum

As we have seen in this course, the downside of Python is that even though it resembles human language, it can be slow. In general, programming languages fall on a spectrum.

A spectrum of programming languages from low-level (machine code, assembly, C, C++) through Java and Scala to high-level (Perl, Python, JavaScript, Ruby)
Figure: Programming languages fall on a spectrum from low-level languages that are closer to the machine to high-level languages that are closer to human language. Source: codecommit.com.

At the far left of this spectrum are low-level languages, which are closer to the machine and give us more control over what happens inside it. As we move to the right, we encounter high-level languages, which are (or appear to be) closer to human language and come with a lot of bells and whistles. Python is a great example: it often gives us six different ways of doing the same thing, whereas a language like C might give us zero ways and assume that we will implement the idea on our own. The downside of having so many one-size-fits-all solutions built into a language is that things can slow down.

In the middle of the spectrum are mid-level languages. Go falls in the middle, near Java, providing a little more control over the machine than Python while being more comfortable to work with than C.

A conceptual architecture of a computer

We have been mentioning the CPU and RAM without saying precisely what we mean by them. The following figure shows a classic conceptual architecture of a computer.

Slide titled Conceptual Architecture of a Computer: a CPU holding a program counter and registers 0 through n, connected by arrows to RAM, a row of words 0 through m. Notes explain that registers hold small amounts of data for processing, the CPU performs operations on data in registers, and RAM stores more data as binary numbers.
Figure: A conceptual architecture of a computer. The CPU holds a small number of registers and performs operations on them, while RAM stores many more data as binary numbers.

Think of RAM as a very large storage facility, in which each bay holds a finite amount of storage. Each of these bays, which we call a word and identify by its address, corresponds to a collection of transistors, each of which is high or low. We represent these two states with the binary digits (or bits) 1 and 0, so that RAM stores our data as strings of binary numbers. The more RAM we have, the more data our applications can access, and the more they can do.

The CPU contains a much smaller collection of storage locations that are internal to it, called registers. Whereas a computer may have billions of addresses of RAM, it may have only 16, 32, or 64 registers. The CPU pulls data from RAM into registers, performs fast operations on those registers (like addition and subtraction), and then writes results back to RAM. For complicated electrical engineering reasons, this design is the most efficient: we have a huge storage facility, but at any moment we keep a small number of things close at hand, and if we run out of space, we push data back to RAM.

There is a little more to it, because some special memory addresses are reserved for other purposes. Reading from or writing to these addresses may cause a peripheral device like the display or the hard drive to perform some task.

In both the registers and RAM, the data stored at each location is a string of ones and zeros. So if we want to do something as simple as adding two numbers, those numbers are represented in binary, which means that we need to become comfortable with numbers in base 2.

Representing numbers in binary

This is an area of computer science in which a strong mathematical foundation is necessary. You are very comfortable thinking of a certain number as 4256, but the number itself is a more abstract concept than this representation. When we write 4256 in base 10 (also called decimal), we mean that we have four thousands, two hundreds, five tens, and six ones. Each position of the number corresponds to a power of 10.

Power of 10Place valueDigit of 4256Contribution
103100044000
1021002200
10110550
100166
Table: In base 10, 4256 = 4 · 103 + 2 · 102 + 5 · 101 + 6 · 100.

We do not have to split the number into powers of 10, however. In base 2 (binary), the only digits allowed are 0 and 1, and each position corresponds to a power of 2. The largest power of 2 that fits into 4256 is 212 = 4096 (recall that 210 = 1024). This leaves 4256 − 4096 = 160, and the largest power of 2 fitting into 160 is 27 = 128. That leaves 32, which is 25. Since 4096 + 128 + 32 = 4256, we need a 1 in the positions for 212, 27, and 25, and a 0 everywhere else.

Power of 2Place valueBinary digitContribution
212409614096
211204800
210102400
2951200
2825600
271281128
266400
2532132
241600
23800
22400
21200
20100
Table: Writing 4256 in binary. The rightmost digit is the ones place, the next is the twos place, then the fours place, and so on up to the 4096s place.

Reading the binary digits from the top of the table to the bottom, we obtain the binary representation of 4256.

4256 = 1000010100000 (base 2)
     = 1 * 2^12 + 1 * 2^7 + 1 * 2^5
     = 4096 + 128 + 32

The point is that we can represent a number in base 10, or we can represent the same number in base 2. For any number that we choose, we can always find its base 2 representation, which is how the number is stored inside the computer.

Hexadecimal notation

Even though everything inside a computer is a sequence of ones and zeros, writing everything in binary would be tedious. So when we talk about stored data, we often use base 16, which is called hexadecimal (or hex for short). We will soon see why hexadecimal is such a natural choice.

Base 10 has ten digits, 0 through 9. Base 16 needs sixteen digits, so after 0 through 9 we use A to represent 10, B for 11, C for 12, D for 13, E for 14, and F for 15.

Hexadecimal digit0123456789ABCDEF
Base 10 value0123456789101112131415
Table: The sixteen hexadecimal digits, 0 through F, and their values in base 10.

In base 16, each position of a number corresponds to a power of 16. The rightmost digit is still the ones place, but the next digit is the 16s place, the next is the 162 = 256s place, and the next is the 163 = 4096s place. Our number 4256 is equal to one 4096 plus ten 16s, and so in hexadecimal it is written 10A0.

Power of 16Place valueHex digit of 10A0Contribution
163409614096
16225600
16116A (10)160
160100
Table: In base 16, 10A0 = 1 · 163 + 0 · 162 + A · 161 + 0 · 160 = 4096 + 160 = 4256.

We can pick any base we like, and we can represent any number we like in that base.

Exercise: Convert the hexadecimal number A7D to base 10, and then convert it to binary. Remember that D (which is 13) occupies the ones place, 7 occupies the 16s place, and A (which is 10) occupies the 256s place.
STOP: As you work on the previous exercise, ask yourself: which of the two conversions of A7D was easier, converting it to base 10 or converting it to binary?

Looking ahead

At the end of this video, the chat converted A7D to 2685 in base 10, and nearly everyone voted that converting to base 10 was easier than converting to binary. In the next lesson, we will start by working through both conversions and see why the answer to the poll is the opposite: converting from hexadecimal to binary is far easier than converting to base 10, which is exactly why hexadecimal is such a convenient shorthand for binary.

Scroll to Top
Programming for Lovers banner no background
programming for lovers logo cropped

Join our community!