 Assembly languages are often called low-level languages, because they are metaphorically low in the sense that programmers using them are left down in all the details of the actual machine instructions. When you write a program in assembly, you're piecing it together, machine instruction by machine instruction. The advantage is precise control. You can have the CPU execute the precise series of instructions that you want. In contrast to low-level languages, a high-level language takes away precise control from the programmer in exchange for convenience. In a high-level language, each line of code typically corresponds to more than just one machine instruction, thereby accomplishing more work per line of code. By abandoning one-to-one correspondence between source code and machine instructions, high-level languages also allow us to write code that is portable across different processors. The programmer can write a program once and have it run on many target platforms. In the first 30 or so years of computing, most programming was done in low-level assembly languages. Also, the vast majority of programming is done in high-level languages and assembly is only used in a few niche cases, such as in writing certain portions of operating systems. The conversion of high-level language source code into machine code is not called assembly, but rather compilation, and the programs which do it are called compilers. The reason for the different term is that to assemble connotes a simple process of one-to-one correspondence. Each line becomes one instruction resulting program. To compile, in contrast, connotes a more sophisticated process of translation in which lines of source code and machine instructions do not strictly correspond one-to-one. Each line of source can translate into many machine instructions. Still, the general idea of assembly and compilation is basically the same. One form of code gets translated into another form of code. So, for example, say you're programming in the high-level language C. We've written our C code in two different source files, one called foo.c and the other called bar.c, and we run each one independently through the compiler, spitting out two separate object files foo.o and bar.o. Then just like in assembly, we run the object files together through the linker to produce a single executable. It's possible in some cases to link object files generated from different languages. For example, it's quite common to link object files generated from assembly code with object files generated from C code. This can be useful because the precise control of assembly may allow me to optimize code in a way that I just can't in C. So, I might write most of my program in C for the relative convenience, but for the portion of code which I need to optimize, I might write just that part in assembly. Another reason to mix assembly code with C code is that in C, and in fact in all high-level languages really, there is no way to directly invoke a system call. Remember that system calls are invoked using a particular instruction. Well, nothing in the semantics of the C language will cause the compiler to output that machine instruction. So in fact, C code is ultimately reliant upon assembly code to invoke any system calls, and the same is true of any other high-level language. Without native machine code, without assembly, we just couldn't invoke system calls. An alternative to compilation is a scheme called interpretation, performed by programs called interpreters, whereas a compiler reads your source code and translates it into another form of code, usually machine code, an interpreter reads your source code and translates it into action. That is to say, an interpreter reads the source code, and as it reads the source code, it does what the source code says to do. Admittedly, the choice of terms here is not apt. An interpreter in normal life refers to someone who translates one language into another, but that's more analogous to what a compiler does. An interpreter in programming refers to a program which reads and acts upon code. So whereas a compiler is more like C3PO, an interpreter is more like an action hero, like John McClain. To run a program with an interpreter, we don't produce any executable file, we just run the code through an interpreter, and the interpreter's actions as it reads the code is the running of our code. To run source code split into multiple files, we just run them all through the interpreter together. The interpreter then handles any business of linkage between the files as it runs the code. To imagine an interpreter at work, start off imagining a program which reads a list of mathematical expressions. As the program reads each line, it evaluates the expressions. So when reading these three lines, the program would get the values 93, then negative 7, then 16. Now imagine that we introduce variables, such that the value produced on one line is remembered by the program as a symbolic name, such that we can then use the name on successive lines. So here the value of 1 plus 2 is retained as the name foo, so in the next line the expression 1 plus foo would evaluate to 4. Next imagine that we introduce control flow with if and while, such that some lines may get evaluated only conditionally, and some lines may repeat. Lastly, imagine we introduce functions, such that a group of lines can be invoked by name. Well, once our program can process expressions, variables, control flow, and functions, we basically have an interpreter for a programming language. Another way to run code is to use compilation and interpretation together. For instance, in the Java language we first run the source code through a compiler, but rather than spit out machine code, this compiler generates an intermediate form of code which Java calls bytecode. This bytecode is then run and executed by an interpreter, which in Java we call a virtual machine. Java bytecode actually looks very much like machine code. The instructions of bytecode denote basically the same kinds of operations of a processor instruction set. However, no processor directly understands these bytecode instructions, instead a virtual machine interprets them, meaning it does what the instructions say to do as it reads them. The thinking behind this hybrid approach is that it gives us the advantages of compilation and interpretation at the same time. On the one hand, compiled code tends to run faster than interpreted code because it needn't parse the source code when we run the program, and a compiler isn't under time pressure to actually run the program, so it has all the time it wants to optimize the code it generates. On the other hand, interpreted code allows us to run the same code on multiple platforms without first compiling for each platform. In the hybrid approach, we only have to compile our source code once in the bytecode, and then the same bytecode can run on any system with a Java virtual machine. So in short, we compile for efficiency, but interpret for portability. Now, interpretation, even of pre-compiled bytecode, always introduces performance overhead compared to running straight machine code. Whereas machine instructions are directly executed by the CPU, interpretation involves the extra work of reading the code, deciding what work that code denotes, and then actually doing the work. So for greater efficiency, many virtual machines have adopted a scheme called JIT, standing for just-in-time compilation. With JIT, the virtual machine may actually compile some or all of the bytecode into machine code and then run that machine code rather than interpret those parts of the code. Even though this of course introduces more work upfront when a program is loaded, the idea is that a smart VM can decide when and where the performance gain will outweigh the cost. In fact, by delaying compilation until a program is actually run, the VM can make intelligent decisions about how to optimize the generated machine code in a way that a normal compiler cannot, because the VM has the advantage of observing the code actually run and thus knows more about its real performance. For example, in some cases, there's a trade-off between optimizing for space, meaning for minimizing memory usage and optimizing for execution time, for minimizing processor usage. By measuring actual performance, the VM can make an informed choice between the two. So the argument is sometimes made that it's possible for code running in a VM with a just-in-time compiler to actually exceed the performance of code that's compiled into machine code ahead of time. How often this actually comes true in real-world scenarios, if at all, that's a highly debated topic.