Chapter 7
Three ways to run code
You have installed three languages. They look like three separate subjects. They are not. They are three answers to one question: when does your code get turned into instructions the processor can carry out?
The same program, three times
Here is the program from chapter 6, written three ways. Read them side by side. They do exactly the same thing.
#include <stdio.h>
int main()
{
int num1;
int num2;
printf("To add two numbers.\n");
printf("Enter the first number: ");
scanf("%i", &num1);
printf("Enter the second number: ");
scanf("%i", &num2);
num1 = num1 + num2;
printf("The sum of two numbers is %i.\n", num1);
return 0;
}
print("To add two numbers.")
num1 = int(input("Enter the first number: "))
num2 = int(input("Enter the second number: "))
num1 = num1 + num2
print(f"The sum of two numbers is {num1}.")
import java.util.Scanner;
public class Sum {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("To add two numbers.");
System.out.print("Enter the first number: ");
int num1 = in.nextInt();
System.out.print("Enter the second number: ");
int num2 = in.nextInt();
num1 = num1 + num2;
System.out.printf("The sum of two numbers is %d.%n", num1);
}
}
All three print the same thing:
To add two numbers.
Enter the first number: 3
Enter the second number: 4
The sum of two numbers is 7.
But you start them differently
C needs two commands. One to build, one to run:
$ clang sum.c -o sum
$ ./sum
Python needs one. There is nothing to build:
$ python3 sum.py
Java needs two, but the second one is not your program — it is a machine that runs your program:
$ javac Sum.java
$ java Sum
Those small differences are the whole chapter. Here they are in one picture.
The three roads
Watch it explained
This four-minute video covers the same idea with animations. It opens in a new tab:
The video opens with about fifteen seconds where the author talks about his own books, and it ends with an advertisement for a newsletter. The four minutes in between are what matter.
It says C++ where we say C. For everything in this chapter, the two behave the same way: both are compiled all the way to machine code before you run them.
The video comes from ByteByteGo: youtube.com/@ByteByteGo
It explains how real software is built, and how large systems work across many machines at once. The drawings are clear and the videos are short. For anyone learning to build software, it is one of the best places to watch.
A habit worth starting now, and keeping:
- Watch one video every week.
- Write short notes in your own words while you watch.
- Then try the idea out yourself, even in a very small way.
Watching teaches you a little. Writing it down teaches you more. Trying it is what makes it stay.
C — translated once, before
clang turns your whole file into machine code and saves it. That
finished file is the program. When you run it, no translating happens at all
— the processor is handed instructions it already understands.
Two things follow from that, and you can see both.
The source is no longer needed. Copy just the program somewhere else and run it:
$ mkdir ~/ship && cp sum ~/ship/
$ cd ~/ship && ls
sum
$ ./sum
To add two numbers.
No sum.c in that folder, and it still runs. This is how most
software is delivered: you get the machine code, not the source.
Change the source and nothing changes until you build again.
The program is a photograph of your code, taken when you ran
clang.
Python — translated as it goes
There is no build step, and no second file. python3 is a program
that reads your file and does what each line says, one line at a time, every
single time you run it.
$ ls
sum.py
One file in, nothing new out. That is why Python feels quick to work with: edit, run, edit, run, with nothing in between.
It also explains something from
step 12. pdb can stop
your program in the middle of a line and show you the value of a variable,
because at that moment something is still reading your program. The
source is right there, being followed.
A loop that runs a million times has its lines worked out a million times. C worked them out once, before the program started.
That is where the speed difference below comes from.
Java — a bit of both
Java splits the difference, and this is why it needs two commands.
javac translates your source into bytecode.
Bytecode is instructions, but not for your processor — for an imaginary
machine that does not exist in hardware.
$ javac Sum.java
$ file Sum.class
Sum.class: compiled Java class data, version 65.0
You met that file already. In step 5 you looked at its first bytes:
$ xxd -l 8 Sum.class
00000000: cafe babe 0000 0041 .......A
cafe babe is the marker that says "this is Java bytecode", the
same way 7f 45 4c 46 says "this is a Linux program".
Then java starts the JVM — the Java Virtual
Machine. It is a program that pretends to be that imaginary processor. It
reads your bytecode and carries it out.
And it does one more thing. When it notices a piece of bytecode being used over and over, it translates that piece into real machine code, right then, and uses the fast version from that point on. This is called JIT, short for just-in-time compilation.
Because the same Sum.class runs anywhere a JVM exists — a
laptop, a phone, a server — with no rebuilding. Your sum from
C runs only on the kind of processor and system it was built for.
That portability is why Java is used for large systems that must run on many different machines.
What it costs
Same task in all three: add up every whole number from 1 to 10,000,000, and print the total. Measured on one machine, on the same day.
Python took about 150 times as long as C. Java landed between them, and a good part of its time was starting the JVM at all — on a longer job, JIT closes much of the gap.
This loop is the worst possible case for Python: ten million tiny steps and no real work in any of them. Most real programs spend their time waiting for a disk, a network or a person, where the language makes no difference at all.
Six seconds of computer time can easily save an hour of your time. That is often a good trade.
What each one leaves on the disk
| Language | You write | Build makes | To run it you need |
|---|---|---|---|
| C | sum.c — 326 bytes |
sum — 16064 bytes |
nothing else |
| Java | Sum.java — 471 bytes |
Sum.class — 899 bytes |
a JVM |
| Python | sum.py — 188 bytes |
nothing | python3, and the source |
Notice the C program is fifty times bigger than its source. That is the C library joined in by the linker, as chapter 6 described.
The whole chapter in one table
| C | Java | Python | |
|---|---|---|---|
| Translated when | before you run | partly before, partly during | while it runs |
| Called | compiled | bytecode + JIT | interpreted |
| Build step | yes | yes | no |
| Speed | fastest | close behind | slowest |
| Runs on another machine | only the same kind | anywhere with a JVM | anywhere with Python |
| Good for | speed, small hardware | large systems, many machines | getting it working quickly |
Other languages fit the same three shapes. Go and Rust are compiled like C. Ruby and JavaScript are interpreted like Python. C# is bytecode like Java. Learn the three shapes and a new language is far less new than it looks.
Try it
Build the C program, then remove the source and prove it still runs:
$ clang sum.c -o sum && mkdir -p ~/ship && cp sum ~/ship/
$ cd ~/ship && ls && ./sum
Now try the same trick with Python. Copy only the "program" — except there isn't one:
$ python3 /does/not/exist/sum.py
Look at what each build step produced:
$ file sum sum.c sum.py Sum.java Sum.class
And find the marker at the front of the Java bytecode:
$ xxd -l 8 Sum.class
You can now explain
- The one question that separates the three languages
- Why C needs two commands and Python needs one
- Why a C program still runs after you delete its source
- What bytecode is, and why an imaginary processor is useful
- What the JVM does, and what JIT adds
- Why
pdbcan stop Python in the middle of a line - Why Python is slower, and when that does not matter
- Which of the three shapes a new language is likely to fit
