Let's Talk Compilers

Let's Talk Compilers

Introduction

In modern software development, creating efficient, optimised code is crucial for building fast, scalable applications. To achieve this, developers rely on a variety of tools that help transform and optimise code before it reaches the browser or server. Among these tools, compilers play a vital role in improving performance by converting high-level code into a form that a computer can understand and execute.

In this article, we’ll dive deep into compilers—exploring how they work, why they’re significant in development, and how they transform source code into machine-readable instructions that run faster and more efficiently.

Let’s dive in!

What is a Compiler?

A compiler is a tool that transforms high-level programming code (like JavaScript, TypeScript, or C++) into a lower-level language that a computer can understand and execute. This lower-level language can either be machine code (which the computer's CPU directly executes) or bytecode (which is executed by a virtual machine).

Think of it like this:
Your Code
(Source Code) → Compiler (Processing & Optimisation) → Machine Code (Executable Instructions)

This entire process helps improve performance by making the code run faster and more efficiently on a computer.

Importance of Compilers

  1. Improving Performance

    By optimising the code before execution (through processes like removing redundant calculations), compilers ensure that the program runs efficiently. This reduces lag and increases speed.

  2. Cross-Platform Compatibility

    Compilers enable software written in one programming language to be executed on different platforms (Windows, macOS, Linux). By converting code into machine code or bytecode specific to a platform, developers can ensure their applications work across various environments.

  3. Enabling New Technologies

    Compilers make it possible to run languages like WebAssembly in the browser. WebAssembly allows high-performance execution of code written in languages like C, C++, and Rust, expanding the types of applications that can run directly in a web browser.

Examples of Compilers

CompilerLanguage InputCompilation Output
V8 EngineJavaScriptMachine Code
EmscriptenC++WebAssembly (Wasm)
Dart CompilerDartMachine Code, JavaScript, Wasm
Rust CompilerRustWebAssembly (Wasm)
AssemblyScript CompilerAssemblyScriptWebAssembly (Wasm)

How Do Compilers Work?

Compilers typically go through the following stages:

  1. Lexical Analysis

    What Happens Here?

    • The compiler breaks the code into small, meaningful pieces called tokens.

    • These tokens are words, symbols, or numbers that represent the smallest units of the program.

    • For example, in the line let a = 5;, tokens would include let, a, =, 5, and ;.

Why is it important?

  • Lexical analysis simplifies the program and prepares it for further processing.
  1. Syntax Analysis

    What Happens Here?

    • The compiler checks whether the code follows the correct syntax or grammar of the programming language.

    • It ensures that statements are structured correctly (e.g., no missing semicolons or mismatched parentheses).

Why is it important?

  • This step catches basic errors in the program, such as a missing closing bracket or incorrect ordering of statements.
  1. Semantic Analysis

    What Happens Here?

    • The compiler checks if the code makes logical sense.

    • For example, it ensures that variables are used correctly (e.g. using undeclared variables).

Why is it important?

  • This step helps ensure program correctness by catching issues that syntax analysis might miss, such as invalid variable references.
  1. Optimisation

    What Happens Here?

    • The compiler tries to improve the performance of the code by eliminating inefficiencies.

    • It may remove redundant calculations, combine operations, or rearrange the code for faster execution.

Why is it important?

  • Optimisation ensures that the final output is as efficient as possible, helping the program run faster and use fewer resources.
  1. Code Generation

    What Happens Here?

    • Finally, the compiler converts the optimised code into machine-readable instructions, or machine code.

    • These instructions can be executed directly by the CPU.

Why is it important?

  • This step produces the final executable code that the computer can run.

Compilation Strategies

There are two main strategies that is used to translate a program written in a high-level language into a format that a computer can understand. These are;

  1. Just-In-Time (JIT) Compilation

  2. Ahead-Of-Time (AOT) Compilation

Before we explore these strategies in detail, let’s define some key terms that will help us understand how they work.

  • Machine Code: The binary instructions that a computer’s Central Processing Unit (CPU) directly understands and executes.

  • Bytecode: A lower-level code representation that is not directly executed by a CPU but runs inside a virtual machine (e.g. Java bytecode runs inside the JVM, WebAssembly bytecode runs inside browsers).

  • Execution: The fundamental process of running a program on a computer. Once the computer understands the code (through compilation or interpretation), it begins executing instructions to perform the program’s tasks.

  • Runtime: The period during which a program is actively running. It is the phase when the compiled or interpreted code is being executed by the system.

  • Compilation: A method of preparing code for execution by translating high-level code (e.g. JavaScript or Dart) into a lower-level language (such as machine code or bytecode) that the computer can efficiently execute.

  • Interpretation: An alternative to compilation, where the code is translated and executed line-by-line instead of converting it into machine code beforehand.

  • Optimisation: The process of improving code performance, such as making it run faster or use less memory.

With these terms in mind, let’s explore JIT and AOT compilation and how they impact web development.

Just-In-Time (JIT) Compilation

Just-In-Time (JIT) compilation happens while the program is running. Instead of compiling the entire code before execution, a JIT compiler translates code into machine code on demand, only when it's needed.

At first glance, JIT compilation might seem similar to interpretation, but they work differently. Let’s explore how interpretation functions to better understand the differences.

How Interpretation Works

  1. The program starts running immediately (there’s no compilation step).

  2. The interpreter reads one line (or statement) of code at a time.

  3. It translates and executes that line on the fly.

  4. This process repeats for every line of the program.

Since no compiled machine code is stored, the interpreter repeats this process every time the program runs, leading to slower execution speeds.

How JIT Compilation Works

  1. The program starts running, just like with an interpreter.

  2. The JIT compiler analyses which parts of the code are used most frequently.

  3. These frequently used parts are compiled into machine code and stored.

  4. The next time those parts are needed, the compiled version is executed instead of interpreting them again.

This optimisation allows JIT compilers to reuse machine code, making execution much faster than pure interpretation.

As a result, JIT compilers are well-suited for dynamic applications that require real-time user interactions or handle constantly changing data, such as web applications, games, and real-time data processing systems. By compiling frequently used code during runtime, JIT allows applications to adapt to their execution patterns, enhancing performance over time and providing faster, more responsive user experiences.

Application of JIT Compilation: JavaScript’s V8 Engine

JavaScript was originally designed as an interpreted language, meaning its code was executed line by line without prior compilation. However, modern browsers (like Google Chrome) and runtimes (like Node.js) use JIT compilation to significantly improve performance. This is made possible by Google’s V8 engine, which powers JavaScript execution.

How JIT Works in the V8 Engine

  1. Initial Execution: The V8 engine starts by interpreting JavaScript code line by line.

  2. Hot Code Detection: If a function is executed multiple times, V8 identifies it as frequently used (hot code).

  3. Optimisation & Caching: The JIT compiler compiles the function into optimised machine code and stores it for future executions. This eliminates the need for repeated interpretation.

  4. Further Optimisations: Over time, V8 continuously refines and optimises frequently used code, making JavaScript execution even more efficient.

Example: JIT Compilation in Action

Let’s look at a simple JavaScript function:

function add(a, b) {
  return a + b;
}

console.log(add(8, 10)); // Outputs: 18

What Happens Behind the Scenes?

  1. First Run: V8 interprets the add() function and executes it.

  2. Repeated Execution: If add() is called multiple times, V8 detects it as hot code.

  3. JIT Compilation: V8 compiles add() into optimised machine code and stores it in memory.

  4. Subsequent Executions: The next time add() is called, V8 runs the compiled version directly, skipping interpretation.

This approach makes JavaScript much faster than traditional interpreted languages.

Ahead-Of-Time (AOT) Compilation

AOT compilation happens before runtime, meaning the code is fully compiled into machine code before the program starts running. This results in faster execution because the program doesn’t need to compile anything while running.

How AOT Compilation Works

  1. The entire program is compiled before execution, producing an optimised binary file.

  2. The compiled file is then executed directly without additional compilation steps.

This approach makes AOT perfect for applications that need fast and predictable performance, such as mobile apps, production web frameworks, and environments where startup speed is critical. By pre-compiling code ahead of time, AOT reduces runtime work, making it especially useful for apps with stable, well-defined processes that don’t require frequent changes while running.

Application of AOT Compilation: Angular AOT Compiler

Angular uses Ahead-of-Time (AOT) compilation to improve web app performance by compiling templates and TypeScript before deployment. Instead of letting the browser compile templates at runtime, Angular does this work during the build phase, ensuring the app loads faster when a user accesses it.

How Angular AOT Compilation Works

  1. Build Phase (Before Deployment):

    • When you run ng build --aot, Angular compiles the HTML templates and TypeScript into optimised JavaScript.

    • This means the browser receives only precompiled JavaScript, not templates or TypeScript.

  2. Production Deployment:

    • Since templates are already compiled, the browser doesn’t need to process them at runtime, leading to faster execution.

Example: Angular Component Before AOT Compilation

@Component({
  selector: 'app-hello',
  template: `<h1>Hello, {{name}}!</h1>`
})
export class HelloComponent {
  name = "World";
}

What Happens in AOT?

  1. Build Time Compilation (Before Deployment)

    • Angular compiles this before it reaches the browser, replacing {{name}} with a direct JavaScript expression.
  2. Optimised Output

    • Instead of shipping templates and compiling them in the browser, Angular generates efficient JavaScript code in advance.
  3. Faster Execution in the Browser

    • Since templates are precompiled, the browser skips extra processing and directly runs the optimised JavaScript.

JIT vs AOT: Key Differences

FeatureJIT CompilationAOT Compilation
Compilation TimeHappens at runtimeHappens before execution
PerformanceStarts slower but optimises over timeFaster startup, optimised beforehand
Use CasesDynamic apps, browsers, JavaScriptMobile apps, WebAssembly, production frameworks
ExamplesV8 (JavaScript), WebAssembly JITAngular, Flutter (Dart), Rust to WebAssembly

In Conclusion,

Compilers are essential tools that convert high-level code into machine-readable instructions, enhancing performance and ensuring efficient execution.

In this article, we explored two primary compilation strategies: Just-In-Time (JIT) and Ahead-Of-Time (AOT). JIT compiles code during runtime, optimising performance for dynamic applications, while AOT compiles code ahead of time, leading to faster startup times and more efficient execution for performance-critical apps.

By understanding these strategies, developers can optimise their code for faster, more scalable software. Choosing the right compilation approach can significantly boost your application's speed and efficiency.