My Computer cs600 699. videos untitled

My Computer

NameSizeTypeDate
23 object(s)
 

602. programming

Programming


The gap between human intent and machine execution is bridged by programming languages, which vary in how much abstraction (aka. context) they provide over the underlying hardware. The sequence of instructions written in a language constitutes a program, and the tools that translate it into something the machine can execute determine how that program runs.

I


1.1. Programming Language

A program is a sequence of instructions which directs a computer to perform a task, often classified as system programs that provide OS utilities and programming environments (e.g. compilers, shells, daemons) or application programs that meet end-users’ needs (e.g. web browsers, text editors), both running in user space. A shell (§603#2.1) is itself a system program that invokes compiled binaries such as the Unix commands under /usr/bin/ (e.g. cat, ls), whereas terminal emulators (e.g. Ghostty) and file browsers (e.g. Finder) are application programs providing interfaces to the same underlying tools. Note that either kind reach kernel services exclusively through system calls.

A programming language (PL) is a formal system of syntax and semantics for expressing instructions. A computer cannot infer precisely from its context, thus every construct resolves to exactly one interpretation, a rigour mathematical notation only approximates. The two diverge in what they primarily express. The notation states what is true (e.g. $x^2 + y^2 = r^2$: a circle, $\pi r^2$: an area), but a program specifies what to do (e.g. draw that circle on pixels) under finite memory, discrete representation, execution order, and so on. Reals, for instance, are stored as finite binary approximations, hence some decimals are left inexact (e.g. under IEEE 754, $0.1 + 0.2 = 0.3 + 5.6 \times 10^{-17}$).

PLs form a spectrum ordered by how much control and overhead the programmer bears, from assembly (i.e. a near bijection with ISA) through systems languages (e.g. C, Rust) that attend closely to data types and memory, to high-level languages (e.g. Python, JavaScript) which abstract both along with many hardware details. Formula translating system (Fortran, 1957) was the first compiled high-level PL, and the tools that translate source code into executables have co-evolved with PLs ever since. High-level abstraction let programs scale to millions of lines, yet the software crisis (1968) followed, where undisciplined goto and global state made them hard to reason about.

Programming paradigms imposed discipline on program structures. Structured programming (Dijkstra, 1968) restricted control flow to sequence, selection, and iteration, which the Böhm-Jacopini theorem (1966) had proved sufficient for any flowchart program. Object-oriented programming (Simula, 1967; Smalltalk, 1980) organised programs around objects hiding state behind interfaces. Functional programming (lambda calculus, 1936; Lisp, 1958) favoured pure functions and immutable data which became useful for concurrency. Apparently, PL designers shape syntax, type systems, and std. libraries around a preferred paradigm, though most features serve several.

For instance, every language must fix its scoping rule, which determines where a binding is visible. Dynamic scoping resolves a variable to the most recent binding on the call stack, whereas lexical scoping (ALGOL 60, 1960; Scheme, 1975) resolves by position in the source text, so a function sees the environment in which it was defined. In turn, nearly all modern languages adopted the latter for predictable reasoning, each with its own lookup order (e.g. Python’s LEGB: Local, Enclosing, Global, Built-in). Combined with first-class functions, lexical scoping yields closures, which then underpin callbacks, decorators, and also stateful higher-order functions (PEP 227, PEP 318).

1.2. Type System

A bit pattern means nothing until given a type. The byte 01100001 is the integer 97 under arithmetic (e.g. $+, \times$) or the character ‘a’ under concatenation (per ASCII/Unicode). A data type is the tag that supplies missing context, a domain of values and the operations permitted on them, and the type system comprises a PL’s rules for assigning and checking these types. Concretely, primitive types (e.g. int, float, char, bool) have fixed representations, and data structures (e.g. stack, list, tree, hash map) carry invariant-bound internal state that any C code can access (e.g. s.data[50]). Such access silently broke dependents as codebases grew in the 1960s-70s.

As a remedy, Barbara Liskov introduced the abstract data type (ADT, 1974), a specification of operations and invariants (e.g. peek, push, pop preserving LIFO order) independent of implementation and language. Her own CLU (Turing Award, 2008) was the first to enforce it, for its compiler rejects access to a type’s hidden representation. Encapsulation, as it came to be called, further provided i) correctness; ii) modularity; and iii) substitutability (the Liskov Substitution Principle, 1987). Modern languages inherit that compiler-enforced access control (e.g. Java, Rust), dynamic languages enforce only at runtime (e.g. Ruby), while Python’s _ prefix is convention, not enforcement.

A violation is an operation outside a type’s domain, and PLs differ in when their type system detects one (i.e. type discipline). Statically-typed languages catch them before the program runs, whereas dynamically-typed ones check values as execution reaches them. Even Java, statically typed, defers array bounds to runtime. On the other hand, strong typing rejects implicit coercion (e.g. 2 + ‘2’ raises TypeError in Python), while weak typing permits it (e.g. 2 + ‘2’ yields ‘22’ in JavaScript, and C silently truncates int x = 3.14 to 3 despite its static checks). Both axes decide where a PL’s bugs surface, at compile time, at runtime, or never.

Static typing was safe but verbose. Robin Milner (Turing Award, 1991) dissolved the tradeoff using Hindley-Milner (HM) type inference in Meta Language (ML), where let f(x) = x + 1 compiles to $f : \text{int} \to \text{int}$ without annotations. HM works because each use of a value constrains its type variable, and unification solves for the principal type (Damas and Milner, 1982). It enabled Haskell and influenced Rust, Swift, and Kotlin. Gradual typing (Siek and Taha, 2006) took the opposite path, retrofitting optional annotations as implemented in Python’s type hints (PEP 484), checked by external tools (e.g. mypy, pyright) but ignored at CPython runtime, e.g. def f(x: int) -> int.

In the terms of abstract algebra, an ADT is an algebraic structure $(S, {o_i}, \mathcal{A})$. Its domain, operations, and invariants are respectively the carrier set $S$, family ${o_i}$, and axioms $\mathcal{A}$. While the ADJ group at IBM made this reading exact via initial-algebra semantics (Goguen et al., 1977), the field studies what is deducible from the axioms alone, so a theorem proved once holds in every structure satisfying them. One such theorem, the uniqueness of inverses, therefore applies to any group, from $(\mathbb{Z}, +)$ to $(S_n, \circ)$ to $(GL_n(\mathbb{R}), \times)$. Both group and stack thus specify what must hold, silent on what carries it out. The correspondence buys programming equational reasoning and generic code.

The parallel extends even to what an operation excludes. $\sqrt{-1}$ is undefined in $\mathbb{R}$, .append() is undefined on an integer, and each system rejects rather than guesses. Yet the parallel breaks at extension and membership. Unlike mathematics, which chains its number systems $\mathbb{N} \subset \mathbb{Z} \subset \mathbb{Q} \subset \mathbb{R} \subset \mathbb{C}$ to admit new solutions (e.g. $x + 3 = 1$ in $\mathbb{Z}$, $x^2 = -1$ in $\mathbb{C}$), types form a lattice rather than a chain (e.g. numbers, lists, trees). In membership, $3 \in \mathbb{Z}$ and $3 \in \mathbb{R}$ are the same object, while int 3 (0x00000003) and float 3.0 (0x40400000) are in distinct hexadecimal. Therefore, the embedding $\mathbb{Z} \hookrightarrow \mathbb{R}$ costs maths a single act of identification but a program pays a conversion instruction.

Implementations realise these specifications under finite hardware, exactly or approximately. int32 realises the quotient ring $\mathbb{Z}/2^{32}\mathbb{Z}$ exactly, for wrapping arithmetic is reduction mod $2^{32}$, and approximates the infinite $(\mathbb{Z}, +, \times)$ using the representatives $[-2^{31}, 2^{31}-1]$. str approximates a free monoid $(\Sigma^\ast, \cdot, \varepsilon)$ as a byte array bounded by memory. Concatenation is its operation, the empty string $\varepsilon$ its identity, and the monoid is free, i.e. nothing holds beyond the axioms. list approximates the same free monoid over a type $T$, $(T^\ast, \cdot, [])$, and list[str] is then $\Sigma^{\ast\ast}$ , the construction applied twice. Whatever storage and algorithms realise them remain details the ADT hides.

Types also compose in the terms of set theory. A struct or tuple is a Cartesian product ($\text{String} \times \text{Int} = \Sigma^\ast \times \mathbb{Z}$), a tagged union or enum is a disjoint union (Rust’s $\text{Result}\langle T, E \rangle = T \sqcup E$), and a function type $A \to B$ has $\vert B\vert ^{\vert A\vert }$ inhabitants for finite $A$, $B$. These are known as algebraic data types because their cardinalities follow arithmetic ($\vert A \times B\vert = \vert A\vert \cdot \vert B\vert $, $\vert A \sqcup B\vert = \vert A\vert + \vert B\vert $), and the arithmetic predicts real counts. $\text{Bool} \to \text{Bool}$ has exactly $2^2 = 4$ inhabitants (identity, negation, constant true, constant false), and $\text{Option}\langle T \rangle = T \sqcup 1$ has $\vert T\vert + 1$ values, the extra one being None. Type constructors like List go further as functors $\textbf{Type} \to \textbf{Type}$, lifting a function $f : A \to B$ to $\text{map}(f) : A^\ast \to B^\ast$ while preserving composition.

Under the Curry-Howard correspondence (Curry, 1934; Howard, 1969), the same constructions read in the terms of mathematical logic. A type is a proposition and its inhabitant is a proof, hence $A \times B$ is conjunction (evidence of both), $A \sqcup B$ is disjunction, and $A \to B$ is implication, where a function claims to construct evidence of $B$ from evidence of $A$ and its body is the proof. The empty type admits no inhabitants and so no proofs, which makes it falsehood. Type-checking thereby becomes proof-checking, and Lean and Coq exploit it as theorem provers. A theorem is stated as a type, its proof is a program of that type, and the compiler checks the given proof mechanically.

1.3. Memory Management

Every object a program allocates occupies memory for its lifetime, and memory being finite, a program that never reclaims memory for reuse will exhaust it. Static allocation, as in Fortran, left nothing to reclaim, sizes fixed at compile time and slots held for the whole run. Dynamic data arrived with Lisp’s heap allocation for objects of unknown size/lifetime (§603#3.1). While it reclaimed the heap automatically via garbage collection (GC), C (1972) left it to the programmer through malloc/free from its std. library. In practice, manual discipline failed at scale, as Microsoft (MSRC, 2019) and Chromium (2020) each attribute ~70% of their C/C++ vulnerabilities to memory bugs.

How a collector finds garbage splits GC into three forms: i) reference counting (e.g. CPython): each object carries a count and is freed the moment it drops to zero, though cycles keep counts positive; ii) mark-and-sweep: the collector traces reachable objects from roots (e.g. globals, stack variables) and frees the rest, which classically stops the world, though incremental and concurrent variants run tri-colour marking alongside the program to keep each pause short; and iii) generational collection (e.g. Java, .NET): most objects die young, thus the youngest generation is collected most often. All three conveniently automate reclamation at the cost of non-deterministic pauses.

Rust’s ownership system takes the third path, where the compiler itself schedules reclamation. Each value belongs to exactly one owner (e.g. the variable holding it) and is freed the moment that owner leaves scope, with a borrow checker rejecting at compile time any reference that would outlive it. Assignment itself moves ownership, hence using the moved-from name is a compile error, and where one owner proves too strict, Rc/Arc opt individual values back into reference counting. Reclamation thus becomes a compile-time proof, trading GC’s pauses for stricter rules on the programmer, rules that also exclude data races by construction.

CPython sits at the opposite pole by staking reclamation on that reference counting, while the cyclic collector (the gc module) backstops cycles. The counts free memory promptly and deterministically, while C extensions must balance Py_INCREF/Py_DECREF by hand. Given that a simple read also mutates the count (i.e. ob_refcnt), the GIL (§604#1.3) guards these writes, and the free-threaded build makes the counts atomic and locks per object instead. In practice, del merely unbinds a name and decrements, and an object is freed only when its last reference goes (e.g. a NumPy view pins its base array). A forked worker loses its copy-on-write pages just by reading (§603#3.1).

How much type information a PL preserves at runtime shapes what it heap-allocates, what it automates, and what each value costs. C, a statically-typed language, erases types after compilation and places local variables on the stack as raw bits (e.g. int, 4 bytes), reclaimed manually. Java, also statically-typed, heap-allocates its objects (e.g. Integer, ~16 bytes; GC-managed) but stack-allocates primitives (e.g. int, 4 bytes). Python, dynamically-typed, binds type information to every value as a heap-allocated PyObject (e.g. int, ~28 bytes) carrying per-value metadata (e.g. reference count, type pointer, payload).

1.4. Error Handling

An error is a condition preventing a call from producing its desired result, and every such call must report back to its caller. In practice, the failures differ in where the report is lost, i) the caller: C, for instance, signals failure with a sentinel value (-1, NULL) and errno that one unchecked return silently discards; ii) the PL: Tony Hoare’s null reference (ALGOL W, 1965), a “billion-dollar mistake” in his words, inhabits every reference type and turns every dereference into a latent error; and iii) the context: Ariane 5 (1996, $370M) inherited code from Ariane 4, and a conversion that could never overflow on the old rocket did on the new one. All three fail far from the code at fault.

C’s successors traded between two goods, a clean happy path and visible failure. Exceptions (PL/I, 1964, ON-conditions; formalised in CLU, 1979) chose cleanliness, unwinding the call stack to a matching handler while hiding the set of possible failures from the signature. Go (2009) chose visibility, returning to explicit error codes that expose failure at every call site (if err != nil). Algebraic error types such as Haskell’s Either and Rust’s Result$\langle$T, E$\rangle$ reconcile the two by encoding success or failure in the type, hence the programmer must handle or propagate it and the compiler warns when a Result is discarded.

Yet not every error deserves handling. A bug is a defect in the program itself, whereas a recoverable condition (e.g. a missing file) is still an error but one arising from the outside world. The test is whether it could befall a correct program, hence a contract violation (e.g. an index out of bounds) signals a bug beyond local repair. Python, specifically, marks the divide in its exception hierarchy, where handlers catch Exception to recover, while BaseException subclasses (e.g. KeyboardInterrupt) terminate the run. Furthermore, Python’s EAFP idiom treats recoverable errors as ordinary control flow, trying the operation and catching what fails rather than checking beforehand.

II


2.1. Compiler

A compiler translates source code into a lower-level language as a batch process before execution (e.g. C/C++: GCC, Clang, MSVC). Grace Hopper’s A-0 system (1952), the first program called a compiler, matched mathematical notation to pre-written machine-code subroutines. Fortran generated machine code from arbitrary expressions, proving compiled output can match hand-written assembly. The Chomsky hierarchy (1956) provided the formal foundation for parsing by classifying grammars by expressive power, Backus-Naur form (BNF, 1959) gave a notation for these grammars, and Lex and Yacc (1975, Bell Labs) automated lexer and parser generation.

These formalisms ground the front-end of the standard compilation pipeline, which performs i) lexical analysis: breaking source code into tokens; ii) parsing: building an abstract syntax tree (AST) from the grammar; iii) semantic analysis: checking types and resolving scopes; iv) intermediate representation (IR) generation. The middle-end optimises the resulting IR (e.g. constant folding, dead-code elimination, inlining) using static single assignment (SSA) form, where each variable is assigned exactly once to simplify data-flow analysis. The back-end lowers the optimised IR to machine code for a target ISA, and a linker combines object files into an executable bound to a specific OS and ISA.

The partitioning of these stages has evolved through three generations. The GNU Compiler Collection (GCC, 1987), the first major free compiler, supported multiple languages and target ISAs. GCC is self-hosting, compiling its own source code through bootstrapping (an initial version written in another language is progressively recompiled by its own output). However, its architecture coupled front-ends to back-ends tightly (despite GIMPLE and RTL as intermediate representations), so supporting $N$ languages on $M$ targets required work proportional to $N \times M$. Its move to GPLv3 (2007) further discouraged adoption by proprietary toolchains.

Low-level virtual machine (LLVM, 2003) reduced this to $N + M$ by defining a stable, target-independent IR in SSA form (.ll text, .bc bitcode). This is a general pattern, since factoring a bipartite dependency through a shared hub replaces a product $N \times M$ of bespoke adapters with a sum $N + M$ of implementations, an argument that recurs for editor-language and agent-tool protocols (§605#4.4). It initially relied on GCC as its front-end (llvm-gcc), but Clang (2007) replaced it with a native C/C++/Objective-C front-end, emitting LLVM IR that the shared middle-end and back-end then process. In C, each stage is explicit: the preprocessor expands macros and #include directives (-E), the compiler emits assembly (-S), the assembler produces an object file (-c), and the linker combines object files into the executable. The back-end can compile the optimised IR ahead-of-time (AOT) via llc, or execute it directly via a just-in-time (JIT) compiler (lli).

LLVM standardised everything below its IR (optimisation and code generation), but the IR operates near the abstraction level of C, too low to represent domain-specific semantics (e.g. tensor operations, ownership constraints, or protocol conformance). Optimisations at this level are inherently limited, since the compiler cannot reason about structure it does not encode: fusing two tensor operations into a single kernel, for instance, requires semantic knowledge that LLVM IR has already discarded. Without a shared framework above LLVM IR, each project constructed its own intermediate layer (Swift-SIL, Rust-MIR, TorchScript), duplicating pass infrastructure, lowering logic, and verification.

The multi-level intermediate representation (MLIR, llvm-project/mlir) addresses it by providing an open-source compiler infrastructure between AST and LLVM IR. A developer defines a dialect (a domain-specific set of operations and types), and MLIR provides the shared infrastructure (passes, verification, rewriting). A PyTorch tensor addition, for instance, progressively lowers from torch.aten.add (Torch dialect) to element-wise loops (linalg.generic) to scalar loads and arithmetic (llvm.fadd). Each step is a systematic dialect-to-dialect transformation. Built-in dialects (arith, linalg, gpu) are maintained by the LLVM community, while external projects define their own (e.g. torch-mlir for PyTorch, IREE for ML inference).

The linker combines object files, potentially from different source languages, but the result is correct only if all were compiled against the same application binary interface (ABI), which specifies calling conventions, data type sizes, struct layout, and name mangling. Static linking embeds all dependencies into the executable, while dynamic linking defers resolution to shared libraries (e.g. .so, .dylib) at load time via a runtime linker (ld.so on Linux, dyld on macOS). ABI mismatches may silently corrupt data or crash at runtime, so a compiled binary targets a specific triple $(\text{ISA}, \text{vendor}, \text{OS})$. Translation layers such as Apple’s Rosetta 2 bridge ISA mismatches by translating x86_64 binaries to ARM (AOT at first launch, with JIT fallback for dynamically generated code).

On disk, the final executable takes an OS-specific format: Linux uses executable and linkable format (ELF), Windows uses portable executable (PE), and macOS uses Mach-O. On macOS, applications appear as single icons in Finder but are actually directories called bundles. A .app bundle (e.g. /Applications/Safari.app/) contains an Info.plist (metadata), a Resources/ directory (assets), and the actual Mach-O executable under Contents/MacOS/. The “program” in the strict sense is the Mach-O binary inside; the bundle is packaging that Finder renders as a clickable icon, and cd /Applications/Safari.app/Contents/MacOS/ reveals the executable directly.

2.2. Domain-Specific Compiler

The PLs discussed so far can refer to general-purpose languages (GPL). A domain-specific language (DSL) confines its abstractions to a specific domain and encodes domain knowledge as primitives that a GPL must reconstruct by loops, conditionals, and data structures. For instance, a structured query language (SQL) compiles a declarative query into a physical execution plan, while regular expression (regex) compiles a pattern description into a matcher, a finite automaton in engines like RE2 though a backtracking search in most (e.g. PCRE, Python’s re). Each DSL is backed by its own compiler or interpreter, but unlike a GPL compiler that emits standalone machine code, a DSL compiler targets its own executor (e.g. DB engine, regex engine, GPU runtime).

Compute unified device architecture (CUDA, §601#2.1) extended C/C++ with GPU-specific constructs that work as an embedded DSL for NVIDIA hardware. When a .cu source file mixes host code (i.e. CPU logic) and device code (i.e. GPU kernels marked with __global__ or __device__), NVIDIA CUDA compiler (NVCC) splits them at compile time, delegates host code to the system compiler, and either compiles device code into parallel thread execution assembly (.ptx) or a directly executable CUDA binary (.cubin) via NVIDIA’s LLVM derivative (NVVM). CUDA exposes the hardware model directly, so requires manual coalescing, synchronisation, and SM scheduling.

Triton (2021) by OpenAI is a Python-based DSL for GPU tensor operations, built on LLVM-MLIR, which automates what CUDA leaves manual by replacing the per-thread SIMT model with tile-based programming over multi-dimensional blocks. Its compiler captures the AST of a kernel (i.e. decorated with @triton.jit) and progressively lowers it through MLIR dialects: Triton IR (TTIR, hardware-agnostic) → TritonGPU IR (TTGIR, hardware-specific) → LLVM IR → executable. It occupies the middle ground between eager-mode PyTorch (i.e. no kernel control) and raw CUDA (i.e. full manual control), with access to shared memory and HBM (but w/o thread-level scheduling).

III


3.1. Interpreter (CPython)

Compilers and interpreters represent opposite trade-offs: the former compile to native machine code ahead of time for maximum runtime speed, while the latter execute source-level representations without producing a standalone binary, trading speed for immediacy and portability. Lisp pioneered the approach with its eval function which traversed S-expressions directly, and later formalised this into the read-eval-print loop (REPL) for interactive development. Modern interpreters typically compile to bytecode first, but the defining characteristic remains execution without ahead-of-time machine code generation. Interpreters also enable runtime dynamism (e.g. eval(“2+3”), exec(“x=10”), getattr(obj, “method”)) that a standalone AOT binary supports only by bundling an interpreter of its own.

Early interpreters were tree-walking and traversed the AST recursively with per-node pointer-chasing overhead. Bytecode compilation replaced tree traversal with sequential dispatch, reducing per-instruction overhead. Just as machine code is bound to a specific ISA, bytecode is bound to a specific virtual machine (VM), here a process VM that virtualises only an ISA (its bytecode instruction set) and delegates everything else (memory, I/O) to the host OS, unlike a system VM (§607#1.1) which virtualises full hardware. Unlike an ISA, however, a VM is software and can be ported to any platform. Java (1995) popularised the model with its Java Virtual Machine (JVM) that became a compilation target of other languages (e.g. Scala, 2004). Register-based VMs (e.g. Lua) encode operand locations in each instruction, and stack-based VMs (e.g. JVM, CPython) push and pop operands on an implicit stack.

In particular, CPython (1991) serialises its bytecode into .pyc files as a sequence of operation codes (opcodes) paired with operands. Each opcode is a low-level instruction analogous to an assembly mnemonic (e.g. LOAD_FAST, BINARY_ADD, RETURN_VALUE), and its operand indexes into the code object’s tables (constants, variable names, or nested code objects). Compiled .pyc files are cached in __pycache__/ (PEP 3147), and CPython skips recompilation when the source file has not changed. Runtime type resolution on every operation makes CPython roughly 10-100x slower than compiled C for CPU-bound logic, though the gap narrows for I/O-bound workloads where wait time dominates execution.

More explicitly, the compilation entry point (i.e. _PyAST_Compile in Python/compile.c) transforms the AST into a struct PyCodeObject that encapsulates the bytecode sequence (co_code), constants (co_consts), variable names, and closure references for a single code block (e.g. function, class). The Python virtual machine (PVM) executes bytecode in a dispatch loop (i.e. _PyEval_EvalFrameDefault in Python/ceval.c). Each iteration reads one opcode, dispatches via computed gotos to the corresponding handler (e.g. LOAD_FAST fetches a local variable and pushes it onto the operand stack), and advances the instruction pointer. Only one thread executes this loop at a time under the global interpreter lock (§604#1.3).

The PVM must also track runtime state while the dispatch loop executes instructions. For a function call, it creates a frame linked to its caller (f_back) and its bytecode (f_code), forming a linked list that mirrors the call stack. This is why CPython needs the reference-counting collector, and why closures (for capturing state), generators (PEP 255, for lazy iteration), and coroutines (PEP 492, for async I/O) can outlive the C stack. Each frame contains its own operand stack (the localsplus array), which is where the stack-based bytecode instructions push and pop values during execution.

The adaptive interpreter (PEP 659) narrows the dispatch overhead by specialising frequently executed opcodes (e.g. BINARY_OPBINARY_OP_ADD_INT), but does not close it for compute-heavy workloads. In practice, performance-critical Python bypasses the interpreter entirely through C extensions: NumPy delegates linear algebra to BLAS/LAPACK, PyTorch dispatches to CUDA kernels, and the standard library itself wraps C implementations (e.g. json, re, hashlib), with bindings via CPython’s C API, ctypes/cffi, Cython, or pybind11. Python handles orchestration while heavy computation runs at native speed, which is why it dominates ML and scientific computing despite its interpreter overhead.

3.2. JIT Compiler

Even with adaptive specialisation, bytecode dispatch remains the bottleneck for compute-heavy workloads. Just-in-time (JIT) compilation addresses this by compiling frequently executed code paths (“hot spots”) to native machine code at runtime, deferring the cost until the program’s actual behaviour reveals which paths are worth optimising. The Java Virtual Machine’s HotSpot compiler (1999) and the V8 engine for JavaScript (2008) brought to the mainstream the multi-tiered JIT strategies pioneered in Smalltalk and Self (Hölzle & Ungar, early 1990s), where code begins interpreted and is progressively compiled at higher optimisation levels as execution counts rise.

A JIT compiler instruments the interpreter to count how often each function or loop body executes. Once a threshold is crossed, the runtime compiles that region from bytecode (or an internal IR) into native machine code, patches the call site to jump directly to the compiled version, and subsequent invocations bypass the interpreter entirely. Because the JIT observes runtime types and values, it can apply speculative optimisations (e.g. inlining a method based on the observed receiver type) that an AOT compiler cannot. If an assumption is later violated, the JIT performs deoptimisation, discarding the compiled code and falling back to the interpreter for that path.

PyPy addressed CPython’s lack of JIT with a tracing JIT that records linear traces through hot loops rather than compiling entire functions, achieving 4-10x speedups but requiring a separate Python implementation. CPython 3.13 (2024) introduced an experimental copy-and-patch JIT (PEP 744) within CPython itself, choosing fast compilation and low implementation complexity over peak performance by copying precompiled templates and patching operand slots. GraalPy, built on the GraalVM framework, prioritises peak throughput via partial evaluation of the interpreter itself, at the cost of higher warm-up time and memory.




I gathered words solely for my own purposes without any intention to break the rigour of the subjects.
I also prefer eating corn in spiral .

Ready