What Seven Real Languages Actually Do
C++, Python, JavaScript, TypeScript, Java, Rust and Go, each traced through the same three questions — with the version numbers attached, because every one of these answers has changed at least once.
For the languages I actually use, what happens at build time and what happens at run time?
Seven different answers to the same question — what the program is at each of the four moments from [[source-to-behaviour]]. The comparison is only meaningful because the axis is fixed: for each implementation, name the artifact that exists at start-up, name the moment it was produced, and name what is still being translated while the program runs.
Each implementation may assume exactly the guarantees its language definition provides, which is why the same strategy pays off differently across them. A Go compiler may assume no signed-overflow undefinedness and must therefore prove loop bounds another way; a C++ compiler may assume it and gets induction-variable analysis cheaply; a JavaScript engine may assume almost nothing statically and must therefore observe first and guard afterwards.
Key points
- The same three questions describe all seven implementations, and every answer carries a version number that has changed recently.
- TypeScript does a full type check and then erases everything; nothing at run time consults a TypeScript type.
javacdeliberately optimizes almost nothing, because the JIT can do it better with knowledge of what actually loaded.- Rust monomorphizes generics and pays in build time and binary size; Java erases them and pays in what it cannot recover at run time.
- Every one of these languages has at least one alternative implementation with a different strategy in production use.
Seven implementations, one axis
The table below is the useful form of the question "is X compiled". Every row names an implementation and a version, because in every one of these rows the answer has changed inside the last decade and will change again.
Read the last column as the source of every warmup effect, every deoptimization and every performance mystery that has no corresponding code change. Where it says "nothing", performance is a property of the build; where it does not, performance is a property of the run.
| Implementation | At start-up the program is | Produced when | Still translating at run time |
|---|---|---|---|
| C++ / Clang 18, releaseimplementation | Native machine code in an executable or shared object | Build time, per translation unit, then linked | Nothing. Templates were instantiated at build time; virtual dispatch is a table lookup, not a translation |
| Python / CPython 3.12implementation | Bytecode for a stack machine, cached in __pycache__ | First import of each module | Bytecode specialisation: the adaptive interpreter rewrites instructions in place once operand types stabilise |
| JavaScript / V8 12implementation | Source text. Nothing was produced ahead of time | Nothing at build time; lazily parsed as functions are first called | Parsing, bytecode generation, and optimizing compilation of hot functions, with deoptimization when a guard fails |
| TypeScript / tsc 5implementation | JavaScript with every type annotation deleted | Build time, for the checking; the emit is a syntactic transform | Whatever the JavaScript engine underneath does. The types are gone and no runtime consults them |
| Java / HotSpot 21implementation | Class files containing JVM bytecode | Build time by javac, which does very little optimization | Interpretation, then C1, then C2 per hot method, with on-stack replacement for hot loops and deoptimization on failed speculation |
| Rust / rustc 1.7x, releaseimplementation | Native machine code, generics already monomorphized | Build time; borrow checking on MIR, optimization by LLVM | Nothing. There is a small runtime, but it does not translate anything |
| Go / gc toolchain 1.22implementation | A statically linked native executable including the scheduler and collector | Build time, by a toolchain that owns its whole pipeline | Nothing translational; the scheduler and garbage collector run alongside |
The rows that surprise people
Three of these deserve unpacking, because each contradicts a widely held summary.
TypeScript emits nothing that helps at run time. tsc performs a full semantic analysis — resolution, inference, structural type checking, exhaustiveness — and then deletes all of it, emitting JavaScript that is often nearly character-identical to the input minus the annotations. There is no TypeScript runtime, no type information available to reflection, and no way to check a value against an interface at run time. That is a deliberate design choice with a specific payoff and a specific bill; see [[typescript-pipeline]] and [[type-erasure]].
`javac` is not where Java performance comes from. It compiles to bytecode with almost no optimization, deliberately: inlining and escape analysis in javac would be premature, because the JIT can do both with knowledge of the actual class hierarchy that got loaded. The consequence is that reading Java bytecode tells you very little about what will execute — which is the opposite of the relationship between C++ source and its assembly.
Rust generics are compiled twice, or ten times. Monomorphization emits a separate specialised copy of a generic function for every concrete type it is used with, which is why Rust generics have no dispatch cost and why Rust build times and binary sizes are what they are. Java takes the opposite route and erases, which is why Java generics cost nothing at build time and cannot recover the type parameter at run time. Neither is a mistake; they are the two ends of [[monomorphization]] versus [[type-erasure]].
Strategy is not fixed per language
Every row above has at least one alternative implementation that answers the questions differently, and in several cases the alternative is in production use somewhere.
None of these are exotic. They are the ordinary consequence of the point from [[compiler-vs-interpreter]]: the language definition does not specify the strategy, so anyone with a reason can implement a different one.
- Python: PyPy is a tracing JIT that compiles hot loops to machine code; Cython compiles annotated Python ahead of time through C; MicroPython targets microcontrollers with a smaller bytecode and no cache directory at all.
- Java: GraalVM native-image compiles the whole program ahead of time into a native executable with a fixed closed world, trading reflection and dynamic class loading for start-up in milliseconds instead of seconds.
- JavaScript: engines can snapshot a warmed heap and ship it, and bytecode caches persist across page loads — both are ways to move work backwards across the run-time boundary.
- C++: Cling interprets it; Emscripten compiles it to WebAssembly, which is then compiled again by the browser engine; a
constexprfunction is evaluated by the compiler itself at build time, which is a third translation strategy inside the same build. See[[compile-time-evaluation]]. - Rust: the Cranelift backend exists to make debug builds fast, producing worse code much more quickly, which is a different point on the same trade as
[[optimization-levels]].
What this changes about your decisions
The strategy determines which questions about performance are answerable and where. In a build-time strategy, the artifact is the truth: read the assembly, and what you see is what runs. In a run-time strategy, the artifact is a starting point and the truth is a profile of a warmed process — reading the bytecode tells you almost nothing.
It also determines what a deployment costs. Native artifacts are per-target, which means a build matrix; bytecode artifacts are portable, which means one build and a runtime dependency on every host. Container image size, cold-start latency and the ability to reproduce a build all fall out of this single choice.
| Decision | Build-time strategy | Run-time strategy |
|---|---|---|
| Where you measure performance | The artifact: disassembly and static inspection are meaningful | A warmed process: the artifact is not predictive |
| Start-up latencytypical | Loader time only, typically milliseconds | Parse, interpret and warm up before steady-state performance appears |
| Artifacts per release | One per target triple | One, plus a runtime on every host |
| What a crash report needs | Symbols kept separately, and a symbolication step | A stack trace the runtime produces itself, with names intact |
| What dynamic features cost | eval and reflection need a translator shipped with the program, or are unavailable | Nearly free — the machinery is already resident |
How it works
The steps, in the order the compiler takes them.
- Identify the artifact present at process start: source text, bytecode, or native code.
- Identify when it was produced: build time on another machine, first use on this one, or continuously.
- Identify what still translates during the run: nothing, bytecode specialisation, or full optimizing compilation with deoptimization.
- Read the consequences off those three answers: where performance is measurable, what start-up costs, what deployment ships, and whether dynamic features are affordable.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A microbenchmark of a JVM or JavaScript workload reports numbers that never reproduce in production, because it measured before the optimizing tier ran at all.
- A TypeScript service accepts malformed JSON from an external API and fails deep inside business logic, because the developer believed an interface annotation was checked at run time. Nothing checks it.
- A Rust build takes twenty minutes and produces a fifty-megabyte binary, and the cause is a generic function instantiated over dozens of concrete types rather than anything about the algorithm.
- A Java service moved to native-image starts in twelve milliseconds and then throws
ClassNotFoundExceptionon a code path that uses reflection, months after the migration was declared finished. - A Python service is profiled, the hot function is rewritten in idiomatic Python and gets slower, because the original version was hitting a specialised bytecode path that the rewrite made polymorphic.
When it helps
- Choosing a language for a workload with a hard start-up constraint, where the answer follows directly from the third column rather than from any benchmark.
- Explaining a performance report that makes no sense, which in run-time strategies is usually warmup and in build-time strategies is usually the optimization level.
- Estimating the deployment cost of a language before committing to it: artifacts per target, runtime dependency, image size and crash-report machinery all fall out of the strategy.
When it hurts
- Comparing languages by these rows alone. The rows describe translation strategy, and most production performance is decided by allocation behavior, I/O and data layout, which the strategy influences but does not determine.
- Assuming the row is stable. These answers change between versions often enough that a two-year-old summary is a liability, and several teams have made migration decisions on obsolete ones.
What it costs
Every one of these is paid by something.
- Build-time strategies buy predictability and start-up latency, and pay a build matrix per target, a symbolication pipeline for crash reports, and the inability to use facts only the run knows.
- Run-time strategies buy portability, dynamic features and specialisation on real data, and pay warmup latency, memory for the compiler and its output, and performance that is a property of a process rather than of an artifact.
- Monomorphization buys zero-cost generics and pays in compile time and code size — measurably, on real Rust and C++ builds. Erasure buys compile time and code size and pays by making the type parameter unrecoverable at run time, which forces reflection-based workarounds that then have their own cost.
What else you could do
What a different compiler or language does instead, and when that is better.
- For a Python service that is compute-bound: PyPy, or Cython for the hot module, or moving the hot loop into a native extension — three different points on the same trade.
- For a Java service with a hard cold-start budget: GraalVM native-image or a class-data-sharing archive, both of which move work earlier at the cost of dynamic capability.
- For a JavaScript workload with a large start-up cost: bytecode caching and heap snapshotting, which do not change the strategy but move work backwards inside it.
- For any of them: measure first.
[[profile-guided-optimization]]moves run-time knowledge into a build-time strategy, which is the only way to have some of both.
See it for yourself
The flag, dump or tool that shows you this directly.
- Python:
python -m dis module.pyfor the bytecode,python -X importtime script.pyfor what compilation costs at start-up. - Java:
javap -c Class.classfor the bytecode,java -XX:+PrintCompilationfor the tiering,-XX:+UnlockDiagnosticVMOptions -XX:+PrintInliningfor what the JIT actually inlined. - JavaScript:
node --trace-opt --trace-deopt script.js. A repeated deopt on one function is almost always the cause of a mystery regression. - TypeScript:
tsc --noEmittype-checks without producing anything, and diffingtscoutput against the input shows exactly how little the emit does. - Rust and C++:
cargo build --release && cargo bloatornm --size-sorton the binary shows what monomorphization actually produced.
Plausible wrong readings
Stated the way a confident engineer states them.
- "TypeScript adds type safety at run time." It adds nothing at run time. Every guarantee it provides is a build-time guarantee about code that was checked, and data crossing a boundary from outside was not checked by anything.
- "Java is slow because it runs on a virtual machine." Steady-state HotSpot performance on scalar code is competitive with ahead-of-time compilation on many workloads; the costs that are real are start-up, memory footprint and allocation behavior, none of which is the dispatch loop.
- "Go compiles to native code, so it has no runtime." It has a substantial runtime — a scheduler, a garbage collector, growable stacks — statically linked into every binary. Native compilation and having a runtime are independent.
- "Rust and C++ are the same strategy." Both compile ahead of time, and their generic instantiation, their overflow semantics and their build-time evaluation models differ enough that performance advice does not transfer unexamined.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
Seven languages, three questions each: what exists when the program starts, when was it made, and what is still being translated while it runs. C++, Rust and Go answer "native code, at build time, nothing". Python and Java answer "bytecode, at build time, and yes". JavaScript answers "source text, never, everything". TypeScript answers "JavaScript with the types deleted".
practical
Use the third column to decide where to measure. If nothing translates at run time, disassemble the artifact and trust it. If something does, benchmark a warmed process, discard the first seconds, and check the deoptimization log before believing any regression is in your code. Use the second column to decide what deployment looks like: build-time strategies need a target matrix and a symbol server, run-time strategies need a runtime on every host.
advanced
The rows converge under pressure and it is worth watching how. Java grew ahead-of-time compilation because cold start became a billing line item; Python grew a specialising interpreter and then a JIT because its ceiling became a constraint; JavaScript engines grew bytecode caches and snapshots because parsing megabytes of source on every page load was the dominant cost. Every one of those moves is the same move — shifting work across the build/run boundary — and every one pays the same price, which is that whatever is decided earlier is decided with less information and must therefore be guarded, configured, or given up.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
If you were asked this in an interview
- Take Java and Rust. Where does each spend its optimization budget, and what does that mean for how you measure them?
- Why does TypeScript not check types at run time, and what does a team have to do about it?
- A Python function gets slower after a refactor that removed a branch. What is your hypothesis?
Connections
- Programming Languages & Runtime Internals — The runtimes themselves — Go's scheduler, HotSpot's collectors, V8's object modelThese rows describe what each toolchain translates and when. What the resident runtime then does with the result is the other domain's subject, and most of the performance differences between these seven languages in production are decided there rather than here.