How Computers Work - Software

CS101

Machine Code

"Software" is the general category of code which runs on the hardware. The common case is a "program" like Firefox -- software you run on your computer to solve a particular problem. A computer can run multiple programs at the same time, keeping their drawing into windows etc. separated so they hopefully do not interfere with each other. If the hardware is like a player piano, then a program is like the music.

A CPU understands a low level "machine code" language (also known as "native code"). The language of the machine code is burned into the design of the CPU hardware; it is not something that can be changed at will. Each family of compatible CPUs (e.g. the very popular Intel x86 family) has its own, idiosyncratic machine code which is not compatible with the machine code of other CPU families. What does it mean for the CPU to "run" the machine code? The machine code defines a set of individual instructions. Each machine code instruction is extremely primitive, such as adding two numbers or testing if a number is less than zero. When stored, each instruction takes up just a few bytes. A program that does something useful like display an image or read email is just a large sequence of these very simple machine code instructions.

What is a Program

A program, such as Firefox, is basically a file that contains the bytes of the machine code instructions of the program -- a ".exe" file on Windows or an "app" file on Mac OS X. Each machine code instruction takes up about 4 bytes, and whole sequence of instructions is essentially just a big block of bytes. This is a first definition of "program", and we'll have an alternate definition below.

What is "Running"

When the user double clicks a program file to run it, essentially the instruction bytes stored in the program file are copied into RAM, and then the CPU is directed to begin running at the first instruction in that area of RAM.

The CPU runs instructions using a fetch-execute cycle: the CPU gets the first instruction in the sequence, executes it (adding two numbers or whatever), then fetches the next instruction and executes it, and so on. Some of the instructions affect the order that the CPU takes through the instruction sequence .. for example an instruction might direct the CPU to jump back to an earlier point in the instruction stream (loops are implemented this way), or to skip the next instruction if a particular condition is true (if-statements are implemented this way).

Operating System

The "operating system" of a computer is like a first, supervisory program that begins running when the computer first starts up ("boots up"). The operating system plays an invisible administrative and bookkeeping role behind the scenes. When a desktop or laptop starts up, the operating system typically gets things organized and then launches a "file explorer" program which displays windows and menus etc. that show the user what file systems are available, allowing the user to navigate and operate on the files. The operating system keeps things organized in the background so that multiple programs can run at the same time, which is known as "multitasking". The operating system gives each program its own area of memory, so each program only accesses its own resources .. attempting to limit what an erroneous or malicious program can do. Keeping the programs separate is sometimes known as "sandboxing" .. mediating the access of each program so it operates independently, without interfering with other programs or the system as a whole. Similarly, each program has some access to the screen through a window, but this output area is separated from the output of other programs.

Recall that a .exe file or whatever is essentially just a file of native code instructions. When you double-click the program, it is the operating system "launches" the program, doing the housekeeping steps of allocating an area of memory within RAM for the program, loading the first section of the program's native code into that memory, and finally directing the CPU to start running that native code.

A digital camera is also a little computer. When it boots up, it does not run a file manager program. Instead, after the basic housekeeping is set up, the camera may just run a single program that draws the menus etc. on the camera's screen and responds to clicks on the camera's buttons and so on.

Where Do Programs Come From?

It is very rare to write native code by hand. There are two major ways that programs are built.

1. Old School: Compiled Native Languages

One approach is that the programmer writes their code in a compiled-native language. The computer languages C and its derivative C++ are by far the most popular such languages, although they are relatively primitive compared to more modern languages (below).

As an example of how compiled native languages work, the C++ language has a notion of a "string", a linear collection of characters (Javascript code has this notion of strings too). Lower level machine code does not operate at the level as complex as complex as a string. The machine code works at the level of just adding or comparing individual numbers and that's it. In the C++ language, a programmer can write a line like..

  // C++ code
  a = "hi";
  b = a + "!";

This code appends the string "!" on to the end of "hi", resulting in the string "hi!" stored into the variable b. The native code of the CPU does not have something as complex as a "string append" instruction. However, the string append operation can be accomplished by a long sequence of the primitive native instructions.

The Compiler for the C++ language, reads that C++ code and translates and expands it to a larger sequence of the primitive native code instructions to implement the sequence of actions specified by the C++ code. The output of the compiler is, essentially, a program file (.exe or whatever) made of many native instructions that implements the actions specified in the C++ code. The compile step produces the .exe file from the C++ code, and it is finished. Running the .exe can happen later, and is a separate step.

2. New School: Dynamic Languages

There is a broad category of more modern languages such as Java (the world's most popular language, used in CS106A), Javascript, and Python, which do not use the compile-native structure. Rather than work within the compile-to-native format, these languages can be implemented by an "interpreter".

An interpreter is a program which reads in code as its input, and "runs" the input code. The interpreter proceeds through the code given to it, line by line. For each line, the interpreter deconstructs what the line says and performs those actions, piece by piece. For example, Javascript which we have been using, is implemented by a Javascript interpreter which is built into Firefox.

So in Javascript when we have code lines like:

  // Javascript codee
  a = 1;
  b = a + 2;

The interpreter runs this code, by taking the lines one at a time, and for each, interpreting its actions. For "a = 1;" the interpreter reserves a few bytes to store the value of a, then stores the value 1 into those bytes. Then for "b = a + 2;" the interpreter evaluates (a + 2) getting the value 3, reserves some bytes for the b variable, then stores the 3 into the b bytes. The compiler translates the code into native code. The interpreter looks at each line of code, and performs it in the moment.

Compiler vs. Interpreter

The compile to native strategy does everything ahead of time. For example, looking at all the C++ code, and producing a .exe block of native code that completely implements what is specified in the C++ code. At run time time, we just have the .exe; we do not need the original C++ code. It is not possible to reverse the compile process, starting with the .exe and recovering the original C++ code. The reverse can only be done in a very limited way, not producing the full, useful C++ code (all the comments are missing, as a first example).

In contrast, the interpreter deals with each line in the moment, just as it runs. As an analogy, you can think of the interpreter as a person with their own memory and capabilities who looks a the code to be run, and pantomimes or simulates each step that is described in the code.

The native compiled code runs fast because many questions -- is there an = sign here, does this line deal with strings or numbers -- all those questions are sorted out at compile-time, not when the program is running. In contrast, the dynamic language deals with all those issues as the program is running, making the dynamic code, in effect, slower.

Javascript code that is "run" by an interpreter is another form of program that can run on a computer. Behind the curtain, the interpreter is itself a program, for example written in C++. So we are building one layer on top of another. This works!

Dynamic Language Strengths/Weaknesses

Dynamic languages have two main features. (a) they run much slower, say 10x as very rough rule of thumb, than compiled-native code. (b) languages implemented by interpreters can have significant programmer-friendly features that are not possible in compiled-native code. The best example is automatic memory management, which is a feature in Dynamic languages, but in C and C++, the programmer must solve that problem manually. (Aside: many crashes in C and C++ programs are due to errors in the programmer's memory management scheme. It is a difficult problem to solve manually.) Because dynamic languages like Java and Python have more features, a programmer can often write the code to solve a problem more quickly in a dynamic language than they can in C++. The time and attention of programmers is generally quite scarce (translation: programmers are scarce and expensive, which is why you want to be a CS major, or at least a minor!). Therefore, dynamic languages which allow the programmer to produce a correct program more quickly and reliably are pretty attractive, even if the resulting program uses uses more CPU and more RAM. Aside: Moore's law in effect, keeps making the programmer relatively more expensive compared to the CPU.

Overall, different computer languages have different strengths and weaknesses, and best language for a particular problem depends on the situation. As above, dynamic languages like Java and Python can run slower and generally operate with higher overhead than C++ code, so for some problems, writing in C or C++ is the best strategy. Also, Java and Python lack certain "low level access" features which are needed in rare cases.

JIT Just In Time Compiler

The most modern form of dynamic language is implemented with an interpreter paired with a Just In Time compiler (JIT) trying to get the best of both worlds. The JIT looks a sections of dynamic code that are being run very frequently, and for those, does a compile to native code for that section on the fly. So the interpreter is used for simple cases, but for important sections of dynamic code (like the inside of a loop), the JIT creates a block of native code for that section. The native code is run for that section of dynamic code, giving similar performance to C++, and is discarded when the program exits. Java and Javascript both use JIT technology extensively. The great speedup of browsers in the last few years has been largely due to the implemented of JIT technology for Javascript. The JIT erases most but not all of the "10x" penalty. Even with JITs, dynamic languages still have higher overhead use of resources compared to C and C++.

Common Scenarios To Understand