The Bridge from Language Models to Compiler Theory
Imported@odus · Mar 1, 10:03 PM
Conversation summary
The Chomsky hierarchy divides languages into four levels. Modern programming languages are considered context-sensitive because they depend on external context, such as type declarations; writing code in a completely self-contained form would make it context-free, but practical programming still requires context-sensitive features.
Conversation
"Please explain the 'Chomsky hierarchy' in layman's terms, and explain why modern programming languages are generally considered 'context-sensitive'? If I write code in a completely self-contained, non-referencing form, am I 'downgrading' the language to context-free?"
This is a very profound question that cuts to the heart of programming language essence. Let's break down these three parts with accessible analogies.
Part 1: Explaining the Chomsky Hierarchy in Layman's Terms
Noam Chomsky is not only a linguist but also a founding figure of computer science. He classified all "languages" (whether human or computer) into four levels based on "how complex the grammar rules are".
You can think of these four levels as "Russian nesting dolls", each level containing the one below it. The higher the level, the fewer restrictions, the stronger the expressive power, but the harder it is for a computer to process.
- Type 0: Turing Machine (Unrestricted)
-
Layman's understanding: "The realm of gods." Anything that can be logically computed can be expressed here.
-
Example: Extremely complex natural language logic, or an infinite loop program.
- Type 1: Context-Sensitive
-
Layman's understanding: "Treating people according to their circumstances."
-
What a word means depends on its surroundings. For example, the Chinese word "意思" (yìsi):
-
In "这人真有意思" (This person is really interesting), it means "interesting";
-
In "小小心意,意思一下" (A small token of appreciation, just a gesture), it means "gift/intention".
-
Rule characteristic: The current state depends on the previous state (strong memory).
- Type 2: Context-Free
-
Layman's understanding: "Lego bricks" (nested structure).
-
No matter where you place this red brick, it is always a red brick. The structure of a sentence is like a branching and nesting tree, but how each branch grows depends only on the current rule, not on the neighboring branch.
-
Rule characteristic: Typical "bracket matching."
(( ))is valid, but whether it matches doesn't require knowing whether the brackets contain Shakespeare or code, as long as the left and right brackets correspond. -
Computer application: The "grammatical skeleton" of the vast majority of programming languages is at this level.
- Type 3: Regular
-
Layman's understanding: "Assembly line scanning."
-
Like a keyword search. It can only handle simple linear arrangements, not infinite nesting (e.g., it can't count how many layers of brackets there are).
-
Computer application: Your "Find/Replace" function, or checking if an email format is correct.
Part 2: Why Are Modern Programming Languages Considered "Context-Sensitive"?
Many textbooks say programming languages are "context-free," but this is actually a white lie, or only half the truth.
The truth is: the syntactic structure is 'context-free', but the semantic checks are 'context-sensitive'.
1. Why is the "structure" context-free?
In the first step (parsing the code structure), the compiler indeed treats the code as Type 2 (Context-Free). It only cares if the shape of your statement is correct:
-
if (条件) { 语句 }-> This structure is valid. -
As for what variable is written in the "condition," the compiler doesn't care at this point.
2. Why is the "essence" context-sensitive?
Once you enter the semantic analysis phase, you must look at the context. The most classic example is "variable declaration."
Look at this line of C code:
C
x = x + 5;
If you only look at this line (ignoring context), the statement structure is perfect. But, is this statement valid?
-
Without looking at the preceding context, you simply don't know!
-
If line 1 had
int x;, this statement is valid. -
If line 1 didn't have it, or had
string x;, this statement is invalid (type error).
This is 'context-sensitivity': Whether x on line 10 can be used depends on whether it was declared on line 1. This ability for "cross-line dependency" exceeds the capability of Context-Free Grammar (CFG).
Conclusion: For practicality, modern programming languages require "variables must be declared before use" and "types must match." These rules forcibly push the language into the realm of context-sensitivity.
Part 3: If I Write Code as "Completely Self-Contained," Am I Downgrading the Language?
"If I write code in a completely self-contained form that doesn't reference external variables, am I 'downgrading' the language to context-free?"
The answer is: your code becomes simpler, but the language itself is not downgraded.
1. The Compiler's Perspective Doesn't Change
Even if you write a piece of "self-contained" code like this:
C
void main() {
int a = 1;
int b = 2;
int c = a + b; // 自给自足,不引用外部
}
Although you have closed the loop logically, the compiler will still activate the 'context-sensitive' checking mechanism. It will still look up the Symbol Table:
-
It will check: "Was
adeclared earlier?" (Even though it's on the previous line, this is still context). -
It will check: "Are
aandbnumbers? Can they be added?"
As long as the language allows checking types and scopes, the compiler's mechanism is Type 1 (Context-Sensitive), no matter how simple the code you write is.
2. When Would It Truly Be "Downgraded"?
Only when the language rules themselves abandon these checks would it be downgraded to context-free.
For example, designing a "pure arithmetic expression language":
-
No variable declarations (everything is an immediate value).
-
No function calls (no need for cross-line lookups).
-
Only nested addition, subtraction, multiplication, division:
3 * (4 + 5).
This language would have no need to build a symbol table or look back at what was written on the previous line. This kind of language is truly a "context-free" language.
3. Your Approach Does Reduce "Cognitive Load"
Although you haven't changed the mathematical property of the language (the Chomsky hierarchy), what you are doing is extremely valuable in engineering:
-
You reduce "cognitive context."
-
This is exactly what Functional Programming advocates: Pure Functions, which don't depend on external state, where a given input guarantees a given output.
Although the compiler still checks by the old rules, for humans reading the code, understanding your code no longer requires the brain to act as a "context-sensitive analyzer," making the code easier to maintain.
Summary
-
Chomsky Hierarchy: It's a classification of grammar complexity and capability (from simple regular expressions to the all-powerful Turing machine).
-
Modern Languages: Although they look "context-free" (using brackets and semicolons for nesting), to ensure logical correctness (like type checking, variable declaration), their core is full of "context-sensitive" checks.
-
Your Action: Writing "self-contained" code does not change the compiler's checking mechanism (the language isn't downgraded), but you reduce the difficulty for humans to read the code (logical decoupling), which is an excellent programming habit.
Would you like me to explain how a compiler specifically implements this "context-sensitive" check using a Symbol Table?
How was the Chomsky hierarchy originally proposed? How was it conceived? Explain the first few layers in detail.
This is a fascinating piece of historical tracing. To understand why Noam Chomsky proposed this hierarchy, we need to travel back to the 1950s.
It was an era when "Behaviorist" psychology dominated everything, and computer science was just budding. Chomsky proposed this hierarchy not to design programming languages (there wasn't even a shadow of C language then), but to counter the psychological authorities of the time, attempting to use mathematics to unlock the secrets of the human brain.
Here is the origin story of the Chomsky hierarchy and an in-depth analysis of its first few layers.
I. Origin: A Debate on "How Humans Speak"
1. Background: The Reign of Behaviorism
In the 1950s, the big name in psychology was B.F. Skinner. He believed that humans learn language like training a pigeon to peck—a kind of "stimulus-response" conditioned reflex.
- Viewpoint: You learn to say "apple" because you were rewarded after saying the word. Language is just a linear chain of habits.
2. Chomsky's Counterattack: Finite Means, Infinite Sentences
The young Chomsky scoffed at this. He proposed a core insight:
Humans can understand and create sentences they have never heard in their lives.
For example: "A purple dinosaur tap-dancing on Mars." You've never heard this sentence before, nor received any related "stimulus," but you understand it.
Chomsky argued:
-
Language is not a linear accumulation of habits (like threading beads).
-
The brain must contain a set of "Generative Rules" (Generative Grammar).
-
This set of rules can use finite vocabulary and logic to generate infinite sentences.
3. Mathematization: From Linguistics to Mathematics
To prove the existence of these "internal rules," Chomsky needed a mathematical tool to describe grammatical structure. In 1956, he published the groundbreaking paper "Three Models for the Description of Language."
In this paper, he didn't initially draw out four levels. Instead, through a process of "trial and error," he gradually deduced the boundaries of grammar. He formalized the "rules for generating sentences" into mathematical formulas and, based on the degree of restriction on the rules, divided them into four levels. This is the Chomsky hierarchy.
II. Core Idea: Rewriting Rules
Chomsky viewed language generation as a process of "rewriting." Imagine you have a stack of substitution cards. The rules typically take the form:
α→β (meaning replace α with β)
-
Left side α: The original symbol.
-
Right side β: The symbol after replacement.
The difference between the levels in the hierarchy lies solely in: What restrictions do we place on the left side α and the right side β?
III. Detailed Explanation: The Evolutionary Logic of the First Few Layers
Chomsky started thinking from the simplest model, found it insufficient, and then added complexity level by level. Let's look at it from simple to complex (i.e., from Type 3 to Type 0), which best matches the human thought process.
1. Type 3: Regular Grammar — The Simplest Linear Thinking
-
Starting Point: Chomsky first examined the then-popular "Markov chain" model (similar to modern smartphone text prediction).
-
Rule Restrictions:
-
Rules must be very simple, like a chain.
-
Form:
A -> aorA -> aB. -
Explanation: State
Acan only generate a terminal symbola, or generate aaand then jump to the next stateB. -
Corresponding Machine: Finite State Automaton. It has no memory, only a "current state."
-
Chomsky's Discovery: He proved that this model cannot describe English.
-
Example: English has nested structures, like
If [ ... ] then [ ... ]. -
A regular grammar is like a goldfish with a 7-second memory (actually 0 seconds; it doesn't remember history). It cannot handle structures like
(((( ))))that require counting brackets, because it doesn't know how many brackets were opened before. -
Conclusion: This is a model that is too weak, only capable of handling simple linear arrangements.
2. Type 2: Context-Free Grammar — The Birth of Tree Structures
-
Breakthrough: To handle nested structures, Chomsky relaxed the rules.
-
Rule Restrictions:
-
The left side must have only one non-terminal symbol.
-
Form:
A -> γ(γ can be any string of symbols). -
Key Point: The left side can only be
A, notaAb. This means: No matter whereAappears, its replacement rule is the same, unaffected by the surrounding environment. -
Layman's Understanding: This is the "Lego brick" or "syntax tree" I mentioned in the previous answer.
-
Sentence
S -> 名词短语 + 动词短语. -
Whether this sentence is in a poem or an insult, the internal structural rules of
名词短语never change. -
Corresponding Machine: Pushdown Automaton.
-
Compared to Type 3, it adds a "Stack." This gives it memory, allowing it to push left brackets in and pop them out when encountering right brackets, thus enabling the handling of infinite nesting.
-
Historical Significance: Chomsky believed this level could roughly describe the syntactic structure (Phrase Structure) of natural language. Later, this level directly became the theoretical cornerstone of programming language compilers.
3. Type 1: Context-Sensitive Grammar — Considering the Environment
-
Deepening Thought: Chomsky found that while Type 2 could handle structure, in some natural language phenomena, the same word must change differently in different contexts (e.g., inflection, number agreement).
-
Rule Restrictions:
-
The thing on the left can have "bodyguards" (context).
-
Form:
αAβ -> αPβ. -
Explanation: Only when
Ais sandwiched betweenαandβcanAbecomeP. -
Hard Rule: The length of the generated string cannot decrease (the length of the right side ≥ the length of the left side). This is to ensure that a computer (Linear Bounded Automaton) can process it within finite memory without infinite looping.
-
Corresponding Machine: Linear Bounded Automaton.
-
Its tape length is limited (proportional to the length of the input content).
4. Type 0: Unrestricted Grammar — The Limit of Mathematics
-
End of Thought: If we want to describe any process that can be computed, what should the rules look like?
-
Rule Restrictions: No restrictions at all.
-
Form:
α -> β. -
The left side must have at least one non-terminal symbol, but beyond that, anything goes. You can make long things short, short things long, or even delete entire lines of code.
-
Corresponding Machine: Turing Machine.
-
Significance: This is the boundary of logic and computability. If Type 0 cannot describe it, then the problem is mathematically "uncomputable."
IV. Summary: Chomsky's Greatness
Chomsky did not invent the computer (that was Turing and von Neumann), nor did he directly invent C or Java.
His greatness lies in:
- Unifying the Perspective: He discovered that "the grammar of language" and "the logic of computing machines" are mathematically the same thing.
-
Type 3 = Finite State Machine
-
Type 2 = Stack Machine
-
Type 0 = Turing Machine
- Pointing the Direction: Computer scientists at the time were struggling with how to make machines understand instructions. Chomsky's paper was like a "heavenly book" that suddenly told them: "Hey, stop guessing. If you want computers to handle nested structures (like mathematical formulas), you at least need to design a Type 2 system."
Thus, the Chomsky hierarchy is a bridge between linguistics and computer science. It was born to explore the essence of human thought, yet unexpectedly became the cornerstone of modern programming languages.
How did Chomsky's ideas enter computer theory?
This is a fascinating piece of scientific history, often called "one of the greatest coincidences in history."
Simply put, Chomsky's ideas were not "invited" into the field of computer science; rather, two groups of people, unaware of each other, were digging tunnels on different mountains and miraculously met in the center of the mountain.
Here is the detailed story of this "convergence" process:
Phase 1: Their Respective Predicaments (Mid-1950s)
1. The Linguist's Predicament (Chomsky) As mentioned earlier, Chomsky was studying natural language at MIT. He used mathematical formulas to write "rewriting rules" like A→α, attempting to define the grammatical structure of human language. His goal was to reveal the mysteries of the brain.
2. The Programmer's Predicament (John Backus) At the same time, at IBM, programming legend John Backus (father of FORTRAN) was facing a huge engineering challenge. There was no universal "programming language standard" yet. People described a language (like the newly born ALGOL 58) entirely by writing essays:
"Um... if there's an 'if' in front, then there must be parentheses after it, and inside the parentheses must be an expression..."
This kind of natural language description was extremely vague, causing great pain for compiler writers, who often argued over "whether a semicolon can be added here." Backus desperately needed a rigorous mathematical notation to define programming language syntax.
Phase 2: The Astonishing Coincidence (1959-1960)
1. Backus's Invention In 1959, to define the ALGOL 58 language, Backus invented a new notation system. Later improved by Peter Naur, it became known as BNF (Backus-Naur Form).
BNF looks like this:
Plaintext
<数字> ::= <数位> | <数位> <数字>
Meaning: "A number" is defined as "a digit," or "a digit followed by another number."
2. The Fateful Convergence Shortly after the ALGOL 60 report was published, computer scientists were horrified (or delighted) to discover:
- Chomsky's Type 2 Grammar (Context-Free):
A→a A
- Backus's BNF:
<A>::=a<A>
These two things, aside from looking different symbolically, were mathematically completely identical!
Although Backus was not inspired by Chomsky (he later admitted he hadn't read Chomsky's paper at the time), they arrived at the same destination by different paths: the best mathematical model for handling 'nested structures' and 'recursive logic' is Context-Free Grammar (CFG).
Phase 3: Theoretical Foundation (1960s)
Once it was discovered that the two were the same, Chomsky's profound mathematical theorems, originally used to analyze English and Hebrew, were immediately "borrowed" by computer scientists.
A key figure here is Donald Knuth.
1. The Reversal from 'Generation' to 'Parsing' Chomsky was concerned with "Generation": how to generate sentences from rules. But compilers are concerned with "Parsing": given a pile of code, how to reverse-engineer the rules to see if it's written correctly.
2. The LR Parsing Algorithm In 1965, based on Chomsky's theory, Knuth invented the LR (Left-to-right, Rightmost derivation) parsing algorithm. He proved that as long as your programming language grammar conforms to a subset of Chomsky's "context-free" properties, he could write a program that, while reading the code, performs syntax analysis at extremely high speed (linear time).
This directly laid the foundation for modern compilers. Without this theoretical breakthrough, compiling large software could have taken days or weeks.
Phase 4: Tooling and Popularization (1970s)
By the 1970s, the UNIX system was born. Stephen Johnson at Bell Labs found writing parsers by hand too tedious, so he used Chomsky's and Knuth's theories to create a tool called Yacc (Yet Another Compiler Compiler).
-
Before: Programmers had to write code themselves to analyze how brackets matched.
-
With Yacc: Programmers only needed to write Chomsky-style grammar rules into a file, and Yacc would automatically generate C code to parse it.
From then on, the "Context-Free Grammar" from the Chomsky hierarchy became a required course for undergraduate computer science students and the standard starting point for designing new programming languages.
Summary: Why Did They Converge?
Chomsky's ideas entered computer theory because he touched upon the essential structure of information.
Whether it's God-given human language or human-created programming language, as long as it involves "infinite expression" and "hierarchical logic" (like brackets within brackets), it will ultimately converge on the mathematical model described by Chomsky.
This was a discovery, not an invention.
What is a compiler? Explain compiler theory to me.
A compiler is one of the crown jewels of computer science. If programming languages are the bridge between humans and machines, then the compiler is the builder of that bridge.
In simple terms, a compiler is a super translator.
-
Input: High-level language code you write (C++, Java, Rust, etc.), human-readable but not understandable by computers.
-
Output: Machine language (instructions made of 0s and 1s), which computers can run blazingly fast but looks like gibberish to humans.
-
Task: Convert the former into the latter while preserving the logic exactly, and also find ways to make the code run faster and use less memory.
Modern compiler theory is extremely mature and typically adopts the classic "three-stage" architecture: Front End, Middle End, Back End.
Let me break down each step in this pipeline for you.
Stage 1: Front End — "Understanding Intent"
Task: Check if your code is correct and convert it into a structure that computers can easily process. This stage is most closely related to the "Chomsky hierarchy" we discussed earlier.
1. Lexical Analysis / Scanning
-
Action: Like reading an article, break your code into individual tokens.
-
Tool: Regular grammar (Type 3).
-
Example:
-
You write:
total = price + 5; -
The compiler sees a stream:
[ID:total]``[ASSIGN:=]``[ID:price]``[PLUS:+]``[INT:5]``[SEMICOLON:;] -
It doesn't care about syntax, only whether the spelling of each token is correct (e.g., if you wrote
ifasiff, it will report an error here).
2. Syntax Analysis / Parsing
-
Action: Assemble the sequence of tokens into a hierarchical tree called an Abstract Syntax Tree (AST).
-
Tool: Context-free grammar (Type 2).
-
Example: It will recognize that
total = price + 5is an "assignment statement". -
The left side is
total. -
The right side is an "addition expression".
-
The left side of the addition is
price, and the right side is5. -
If your parentheses don't match or you miss a semicolon, this step will fail (Syntax Error).
3. Semantic Analysis
-
Action: Context-sensitive checks.
-
Tools: Symbol Table + Type System.
-
Example:
-
The AST is built and structurally fine. But the compiler asks: "Has the variable
pricebeen declared before?" -
"Is
pricea string? If so, it cannot be added to the number5!" -
Once this step passes, the compiler confirms: your code is valid.
Stage 2: Middle End — "Optimization Master"
This is the most exciting part of modern compilers. Task: It doesn't care what language you wrote in (C or Go), nor what machine you'll run on (Intel or ARM); it only cares about the logic itself.
To achieve this, it converts the AST into a universal code called Intermediate Representation (IR).
4. Optimization
The compiler performs a series of "magical" operations on the IR to make the code stronger:
-
Dead Code Elimination: You wrote
if (0) { do_something(); }, the compiler sees it will never execute, so it deletes it directly, saving space. -
Constant Folding: You wrote
x = 2 * 3 + 4;, the compiler directly changes it tox = 10;, so no calculation is needed at runtime. -
Loop Unrolling: Reduces the overhead of loop jumps by directly replicating the loop body several times.
LLVM is currently the most famous compiler architecture, and its core advantage lies in having an extremely powerful and universal IR.
Stage 3: Back End — "Adapt to Local Conditions"
Task: Translate the optimized IR into instructions specific to the target machine.
5. Code Generation
-
If you're on an Intel CPU, it translates to x86 assembly.
-
If you're on a mobile chip, it translates to ARM assembly.
-
This step is very tedious because each CPU's instruction set is different.
6. Register Allocation
-
The fastest place in a CPU is the registers (maybe only a few dozen).
-
The compiler has to solve a very difficult math problem (graph coloring): how to arrange hundreds of variables into these few registers, minimizing the number of times data must be moved to memory (RAM)?
Summary: The Big Picture of a Compiler
If we think of a compiler as a factory:
-
Front End (Linguist): Responsible for translating the "customer order" (source code) into "factory blueprints" (AST), confirming there are no mistakes.
-
Middle End (Engineer): Takes the "blueprints" and optimizes them (IR), removing unnecessary steps and streamlining the process for maximum manufacturing efficiency.
-
Back End (Worker): Based on the optimized blueprints, cuts the final parts (machine code) for different "machine tools" (CPU architectures).
Why is this architecture so powerful?
In the old days, writing a compiler was M * N work (M languages, N CPUs). With the Middle End (IR), it becomes M + N:
-
Want to invent a new language (like Rust)? Just write a new front end that targets the common LLVM IR.
-
Want to invent a new CPU (like RISC-V)? Just write a new back end that consumes LLVM IR.
This is why new programming languages are emerging endlessly — the barrier to creating a language has been greatly lowered by compiler theory (especially IR architecture).
Would you like me to show a simple example of how a specific line of code (like a loop) transforms from Source Code to IR to Assembly?
What does compiler theory cover? Do all programming languages have a compiler?
That's a great question. Let's separate "compiler theory" from "how programming languages run."
Part 1: What Does Compiler Theory Actually Cover?
If computer science is a building, compiler theory is its "structural mechanics." It's not just about how to write a compiler; it studies how to convert one symbolic logic into another without loss and efficiently.
It mainly consists of three core theories:
- Formal Languages & Automata:
-
This is the "Chomsky hierarchy" we just discussed.
-
Core question: What strings of characters are valid code? How does the machine recognize them?
-
Applications: Regular expressions, lexical analysis, syntax parsing.
- Program Analysis & Optimization:
-
This is a major application of mathematical graph theory.
-
Core question: Even if the code is correct, is it optimal? Is there any redundancy? How does data flow?
-
Applications: It can deduce that
x = 5; y = x + 2is equivalent toy = 7. This requires extremely rigorous logical proof; you cannot change the logic incorrectly.
- Type Theory:
-
This is a branch of logic.
-
Core question: Can the "apples" here and the "pears" there be put in the same basket?
-
Applications: Checking data safety, preventing memory errors.
In a nutshell: Compiler theory is the science of how to make computers "understand" human logic and "rewrite" it into the most efficient machine logic.
Part 2: Do All Programming Languages Have Compilers?
Short answer: No.
Although all languages must eventually become machine code for the CPU to run, the way they "become" differs. They are usually divided into two camps: Compiled and Interpreted.
We can use a "book translation" analogy:
1. Compiled Language
-
Examples: C, C++, Go, Rust
-
Mode: "Translate the whole book, publish and distribute"
-
Process:
-
You write the code (original English book).
-
The compiler (translator) goes away, spends time translating the entire book into machine code (Chinese translation), and generates a
.exefile. -
Running: Later, users only look at this
.exe(Chinese translation). At this point, the translator is no longer needed.
-
Advantages: Extremely fast execution (because it's already native machine code), good user privacy (source code not visible).
-
Disadvantages: Every time you change even a single punctuation mark, you have to recompile the entire program (reprint the whole book).
2. Interpreted Language
-
Examples: Python, JavaScript (early), PHP, Ruby
-
Mode: "Simultaneous interpretation"
-
Process:
-
You write the code (original English book).
-
No need to pre-translate and generate a
.exe. -
Running: When the user runs the code, a program called an interpreter starts. It reads one line of code, translates it into machine instructions, and has the CPU execute that line.
-
Advantages: Flexible, changes take effect immediately, easy cross-platform (carry the source code).
-
Disadvantages: Slow! Because every run requires re-translation; also, users need to install the interpreter environment to run it.
Part 3: Modern "Hybrids" — Blurred Boundaries
Now things are more complex because pure "interpretation" is too slow. Modern languages often use hybrid strategies, which is the latest battlefield of compiler theory.
1. Bytecode & Virtual Machine (VM)
-
Examples: Java, C#
-
Strategy: "First translate into a simplified version, then execute on the spot"
-
The compiler first translates the code into an intermediate format called bytecode (e.g., Java's
.class). This is like translating complex "Shakespearean Old English" into "modern vernacular." -
Then, the Virtual Machine (JVM) runs this vernacular. This is faster than reading source code directly, but slower than C.
2. Just-In-Time Compilation (JIT)
-
Examples: Modern Java, Modern JavaScript (Chrome V8 engine), PyPy, Julia
-
Strategy: "Tailor to the situation"
-
When the program starts, it runs interpreted (for fast startup).
-
However, the JIT compiler secretly watches in the background. If it finds that a piece of code (e.g., a loop) is executed thousands of times (called "hot code"), it will immediately compile that code into machine code in memory.
-
The next time that code runs, it executes machine code directly, and speed takes off.
-
Result: Modern JavaScript (V8) runs very close to C++ speed because it actually has a powerful compiler built in.
Summary
-
Compiler theory is the mathematical science of formal languages, logic transformation, and optimization.
-
Not all languages have a traditional compiler.
-
C/C++ are ahead-of-time (AOT) compiled, like a printed book.
-
Python is mainly interpreted, like simultaneous interpretation.
-
Java/JS are hybrids (JIT), like "first simultaneous interpretation, then if a part is important, print it as a handout."
This is also why C++ feels slow to write (compilation is slow) but runs fast, while Python feels fast to write but runs slow.
Would you like to know more about how "Just In Time" (JIT) compilers work, specifically how they make JavaScript run so fast in your browser?
What is the difference between compilation and interpretation?
“Compilation” and “interpretation” are two different strategies for getting a computer to execute code.
If we compare source code to an “original English book” and CPU execution to “reading Chinese”, then the difference between these two strategies can be vividly explained in terms of translation methods:
1. Plain-language analogy: translating an entire book vs. simultaneous interpretation
Compilation — like “publishing a translated book”
-
Process: You hand the entire English book to a translator (the compiler). The translator works in seclusion, translates the whole book into Chinese, and prints it as a new book (
.exeexecutable file). -
When reading: The reader (the user) can simply pick up that Chinese book and read it. The translator does not need to be present, nor does the original author.
-
Characteristics:
-
Fast to read: Because it has already been translated, you can pick it up and read it immediately.
-
Difficult to correct mistakes: If the original author changes one sentence, the entire book must be translated and printed again.
Interpretation — like “simultaneous interpretation”
-
Process: You do not translate the entire book. Instead, you find a translator (the interpreter) to sit beside the reader.
-
When reading: The original author reads one sentence in English, the translator translates one sentence into Chinese on the spot, and the reader listens to that one sentence.
-
Characteristics:
-
Slow to read: Because you have to listen, think, translate, and read at the same time, it is inevitably less efficient than reading a book directly.
-
Quick to correct mistakes: If the original author wants to change a sentence, they can simply read it again, and the translator can immediately follow the change.
2. Comparison table of the core differences
For a more intuitive view, let us compare them across several key dimensions:
DimensionCompiled****Interpreted
Representative languagesC, C++, Rust, Go Python, JavaScript, PHP, Ruby
When translation occurs****Before run-time. Translated all at once.At run-time. Run one line, translate one line.
OutputProduces an independent executable file (such as .exe).No independent executable file; it must run together with the source code.
Execution speed****Extremely fast. The CPU runs machine code directly, with no extra overhead.Relatively slow. The CPU must not only execute the code's logic, but also spend time running the interpreter itself.
Cross-platform compatibility****Poor. An exe compiled on Windows cannot run on Mac; it must be compiled again on Mac.Good. As long as an interpreter is installed on the Mac, the source code can be taken there and run directly.
When errors are detected****Strict. Even if there is a spelling error on line 1000, an error is reported during compilation, and it will not let you run at all.Lenient. If the first 999 lines are error-free, they can run first; it will not report an error and crash until it reaches line 1000.
3. A little deeper: Why are interpreted languages “slow”?
Imagine a loop that needs to execute 100 times:
Python
for i in range(100):
print("Hello")
-
Compiled: When the compiler sees the loop, it directly generates machine instructions for printing 100 times. At run-time, the CPU executes them like a machine gun going “rat-a-tat-tat,” with no unnecessary talk.
-
Interpreted: The interpreter is also a program, and it works inside the loop:
-
Read the first line of code -> analyze its meaning -> call the printing function.
-
Go back to the beginning.
-
Re-read the first line of code -> re-analyze its meaning (it has forgotten that it analyzed it just now) -> call the printing function.
-
...Repeat 100 times. This is* the “interpretation overhead”, and a great deal of computing power is wasted on “repeatedly analyzing the meaning of the code” rather than on the actual computation.*
4. Modern evolution: the boundary has blurred
Although they can be distinguished in theory, modern technology is making the two increasingly similar:
-
The middle ground (bytecode): Java and Python both actually undergo “pre-compilation” first, converting the code into an intermediate format (Bytecode) that is easier to read than pure text, but is not yet machine code. This is a compromise.
-
JIT (Just-In-Time Compilation): The V8 engine in the Chrome browser (which runs JavaScript) and modern JVMs (which run Java) are “interpreters with compilers built in.”
-
It first executes by interpretation.
-
Once it discovers that a section of code is running very frequently (a hot spot), it quietly compiles it into machine code in the background.
-
In this way, it achieves speeds close to those of compiled languages while retaining the flexibility of interpreted languages.
Summary
-
Compilation = translate the whole thing first, then execute (fast, but troublesome).
-
Interpretation = translate while executing (slow, but flexible).
Would you like to know more about "Bytecode" and why languages like Java and Python use this middle step?
So interpreted languages only have a front end and a back end, and lack a middle end?
That is a very perceptive deduction! Following the logic from earlier, it is indeed easy to arrive at that conclusion.
But the answer is: in “pure” old-school interpreters, you are right; in “modern” interpreted languages, the middle end has actually always been there—it is simply hidden.
We can divide the situation into two types: the “primitive school” and the “modern school.”
1. The “primitive school”: Pure Interpreter
Your deduction is completely valid here.
In early BASIC or simple Shell scripts, the process really was very short:
-
Front end: Read one line of code and analyze its syntax (AST).
-
Execute directly: Take this AST tree and immediately get to work.
The missing middle end: It does almost no optimization.
-
It does not optimize
x = 2 + 3intox = 5. -
Every time it reads this line, it has to calculate it all over again.
-
Result: There is only a front end (understanding) and a back end (performing actions), with no middle end (thinking about how to do things better).
2. The “modern school”: Interpreter with VM
This is the mainstream situation today (Python, Java, Ruby, PHP). To solve the problem that “pure interpretation” is too slow, they introduce a “hidden compilation stage.” This stage plays the role of the middle end.
Let us use Python as an example:
The hidden middle end: Bytecode
When you run python hello.py, Python does not simply read one line and run one line. Behind the scenes, it secretly performs a “compilation” once:
-
Front end: Parse the source code into an AST.
-
Middle end: Convert the AST into a kind of intermediate code called Bytecode.
-
This is the middle end! * You may have seen automatically generated
.pycfiles or__pycache__folders in a directory; they contain bytecode that has undergone initial processing and optimization. -
At this stage, the compiler performs some simple optimizations (such as constant folding).
- Back end (Virtual Machine): The Python virtual machine (PVM) reads this bytecode and only then executes it.
So the process for modern interpreted languages is:
Source code -> [ Front end + Middle end ] -> Bytecode -> [ Virtual Machine (Back end) ] -> CPU
Although this “middle end” is not as powerful as the compiler for C++ (it does not perform extremely complex mathematical-derivation optimizations), it does indeed exist and undertakes the tasks of “standardization” and “initial simplification.”
3. The “radical school”: JIT engine (Just-In-Time)
Examples: Chrome V8 (JavaScript), JVM (Java), PyPy
Here, your deduction is completely overturned. These interpreters not only have a middle end; their middle end (optimizer) is absurdly powerful.
When Chrome runs your JavaScript code:
-
At first, it does indeed start up like an interpreter (so it can show you the web page quickly).
-
Monitoring (Profiler): It secretly observes in the background and discovers which section of code runs most frequently (the hot spot).
-
Start the compiler: It extracts this hot code and sends it to a complete built-in compiler with extremely powerful optimization capabilities (such as TurboFan in V8).
-
Heavy optimization: This compiler performs dead-code elimination, inline caching, loop unrolling... all of these are tasks of the middle end.
-
Replacement: It replaces the optimized machine code.
Conclusion: Modern high-performance interpreters (JS engines) actually conceal a complete C++-level compiler architecture internally; their middle ends are extremely substantial!
Summary comparison
Type Has middle end (IR/optimization)Typical examples Notes
Pure interpreter****NoShell scripts, old-style BASIC Read one statement, execute one statement; no thinking, no optimization.
Bytecode interpreter****Yes (lightweight)Python, traditional PHP Convert to bytecode (IR), perform simple optimizations, and then hand it to the virtual machine to run.
JIT interpreter****Yes (heavyweight)V8 (JS), HotSpot (Java) Perform extremely complex optimizations at run-time; the middle end is very powerful.
Therefore, apart from the simplest scripting languages, modern programming languages almost all have a “middle end,” because everyone has realized that if you do not optimize (think) before executing, the efficiency is simply too low.
Would you like me to explain what "Bytecode" actually looks like? I can show you the difference between Python source code and its bytecode.
So compiled languages just have one additional compilation step, and when they actually run, are they now not very different from interpreted languages? I am a little confused.
That's a fantastic point to think about! You're not confused at all; you've touched upon the very frontier of "Performance vs. Flexibility" in modern computer science.
Simply put: The difference is still significant. Although modern technology (JIT) has made interpreted languages faster, the "baggage" they carry is completely different from that of compiled languages.
We can use a "race car" analogy to help you thoroughly understand this difference.
1. Different Baggage: Running Empty-Handed vs. Running with a Backpack
This is the biggest difference between the two, and also why C/C++ remains the king of performance.
Compiled Languages (C/C++, Rust) —— "Running Empty-Handed"
-
At Compile Time: Before the race starts (before the software is released), the compiler has already done all the preparation it can.
-
At Runtime: The generated
.exefile contains only streamlined machine instructions, with nothing extra. -
Status: The CPU takes the instructions and runs directly, traveling light.
Interpreted/JIT Languages (Java, Python, JS) —— "Running with a Backpack"
Even with JIT (Just-In-Time compilation) turning the code into machine code, they must carry a heavy "backpack" to run. This backpack is called the Runtime (Runtime Environment).
What's inside this "backpack"?
- Garbage Collector:
-
C++ Program: The programmer manages memory themselves, throwing it away when done.
-
Java/JS Program: There's an automatic sweeper following behind, running and asking: "Is this variable still needed? If not, I'll throw it away." This consumes a lot of CPU and memory.
- Type Checking:
- Even if JIT compiles it into machine code, to prevent errors, it often has to insert some "roadblocks" in the code to check types: "Hey, are you sure this variable is a number?"
- The JIT Compiler Itself:
- JIT compiles while running. The act of compiling itself consumes CPU! When a program first starts, the CPU has to both run the business logic and busy itself with compiling code, which divides its attention.
Conclusion: Even if the quality of machine code generated by JIT were as good as C++ (which is usually not the case in practice), because it carries the two heavy burdens of "Garbage Collection" and "Just-In-Time Compilation," it will always struggle to outrun the "empty-handed" C++.
2. Optimization Time: Deliberate vs. Improvisational
The optimization capability of a compiler depends on how much time it has to "think."
Compiled (AOT - Ahead Of Time) —— "Deliberate"
-
Scenario: Before you release a game, you compile it on a server.
-
Time: The compiler has unlimited time.
-
It can spend an hour analyzing your code, trying 100 optimization schemes, and finally selecting the most perfect instruction arrangement. It can see the entire program's logic and perform extremely aggressive global optimizations.
Interpreted (JIT - Just In Time) —— "Improvisational"
-
Scenario: A user opens a webpage.
-
Time: Only a few milliseconds.
-
The JIT compiler must complete compilation in an instant without the user noticing any lag. It cannot afford to spend time deriving complex mathematical formulas to optimize your code. It can only do some "short and quick" simple optimizations.
Conclusion: The C++ compiler is a chess grandmaster (thinks for a long time for each move); JIT is a blitz chess player (must move immediately, cannot think too long). The quality of a grandmaster's moves is usually higher than that of a blitz player.
3. Startup Speed and Stability
-
Compiled:
-
Startup: Extremely fast. The OS loads the
.exeand starts executing instructions directly. -
Performance Curve: A straight line, stable from start to finish.
-
Interpreted/JIT:
-
Startup: Slow. First, the virtual machine must start, the garbage collector must be initialized, bytecode must be loaded...
-
Performance Curve: A "warm-up" curve. Initially, it's interpreted execution (slow), then after a while JIT kicks in (faster), and then garbage collection triggers (sudden stutter). This is one reason why Android phones sometimes inexplicably "drop frames," while iOS (which primarily uses AOT languages like Objective-C/Swift) is relatively smoother.
4. Why Do You Feel There's "No Difference"?
The reason you get the illusion that "the difference is small" is that hardware is too powerful.
For 90% of application scenarios (writing a web backend, writing a script, making a simple app):
-
CPU performance is abundant.
-
You simply don't care whether it finishes in 10 milliseconds or 1 millisecond; humans perceive both as "an instant."
-
In these areas, development efficiency (Python is fast to write) is more important than runtime efficiency (C++ is fast to run).
However, once you get into the following areas, the difference becomes starkly apparent:
-
AAA Game Engines (Unreal Engine): Must use C++. Each frame has only 16 milliseconds; a 1-millisecond stutter from the garbage collector is unacceptable.
-
Operating System Kernels (Windows/Linux): Must use C/Rust. Cannot carry a "backpack."
-
High-Frequency Trading: Must use C++. Competition is at the nanosecond level; interpreter overhead is fatal.
Summary: The Current Landscape
-
Compiled (C++/Rust) = F1 Race Car.
-
Extremely fast, extremely lightweight, hard to drive (hard to write code).
-
No "middle layer".
-
Modern Interpreted (Java/JS/Go) = High-Performance Coupe.
-
Very fast, has automatic transmission (garbage collection), easy to drive (easy to write code).
-
Even with a turbocharger (JIT), it is still a civilian car and will struggle to outrun an F1 car.
So, it's not that they've been "reduced to the same thing"; rather, interpreted languages are striving to catch up to the taillights of compiled languages, but that "backpack" (Runtime) determines that it's very difficult for them to truly catch up.
Would you like me to explain what "Garbage Collection" (GC) is in detail? It is the heaviest item in the "backpack" of modern languages.
What is JIT?
JIT stands for Just-In-Time Compilation.
It is a "hybrid" technology invented by modern programming languages (like Java, JavaScript, C#, Python's PyPy) to balance "the flexibility of interpreted languages" with "the high performance of compiled languages."
To summarize in one sentence: JIT is an interpreter "with a memory."
1. Easy Analogy: A Smart Translator
To help you understand instantly, let's continue with the translation analogy:
- Normal Interpreter (without JIT): Is a rigid translator. No matter how many times he has translated a sentence, every time he encounters it, he has to look it up in the dictionary again, re-analyze the grammar, and re-translate.
Scenario: That is, if your code has a loop
for (1 to 1000), he will clumsily translate the sentence inside the loop 1000 times.
-
JIT Compiler: Is a smart translator. He also translates sentence by sentence at first (interpreted execution). But he has a little notebook.
-
Observing Secretly: He notices that the 5th sentence on page 10 has been read 100 times by the reader (this is called Hot Spot Code).
-
Just-In-Time Compilation: He thinks: "This part is used too often. I shouldn't translate it every time." So, while the reader isn't looking, he translates this part directly into perfect Chinese (machine code), writes it on a sticky note, and pastes it in the book.
-
Direct Use: The 101st time the reader gets here, he directly points to the sticky note for you to read (runs the machine code directly), and the speed instantly increases 50 times!
2. How JIT Works? (Standard Process)
JIT doesn't start working right away; it's quite cunning and usually follows these steps:
Step 1: Interpretation
When the program first starts, JIT does not work. The interpreter faithfully executes line by line.
- Why? Because compilation consumes CPU and time. If code runs only once (like initialization code), spending time compiling it is a loss. It's faster to just interpret it.
Step 2: Profiling
While the program is running, the JIT engine starts a Profiler in the background. It installs a "counter" on each function or loop.
-
"This function has been called 10 times... no problem."
-
"This function has been called 10,000 times! Warning! This is a Hot Spot!"
Step 3: Compilation
Once a hot spot is detected, the JIT compiler steps in. It grabs this "hot spot code" and compiles it in the background into highly optimized machine code (Native Code).
- At this stage, it performs optimizations as complex as a C++ compiler (like dead code elimination, inlining, etc.).
Step 4: Replacement and Execution (On-Stack Replacement)
The next time the program reaches this point, the JIT engine pushes the "interpreter" aside and lets the CPU directly run the generated machine code.
- At this point, the speed goes from a "bicycle" to a "Ferrari."
3. JIT's "Special Move": De-optimization
This is the most magical and also the most difficult part of JIT.
In dynamic languages like JavaScript or Python, variable types are not fixed.
JavaScript
function add(a, b) {
return a + b;
}
-
JIT observes that in the past 1000 calls to
add, you always passed integers (1 + 2). -
Bold Assumption: JIT guesses "you'll definitely pass integers in the future too," so it compiles the code into "integer addition machine instructions" (very fast).
-
Unexpected Event: On the 1001st call, you suddenly pass a string ("Hello" + "World").
-
De-optimization: The previously generated integer addition instructions can't run! JIT will immediately "Bailout", discard the optimized machine code, degrade back to the original interpreter mode, and execute slowly again.
This is why it is the ultimate embodiment of "context-dependence": it doesn't just look at the code, but also at the data you pass at runtime.
4. Summary: Pros and Cons of JIT
Advantages:
-
Fast: For long-running programs (like Web servers, large games), performance can approach C++.
-
Fast Startup: Unlike C++, which needs to be fully compiled before running, JIT programs can compile while running, making startup speed close to interpreted languages.
-
Dynamic Optimization: It can generate code best suited for the current CPU model (Intel or AMD), whereas statically compiled C++ often has to choose a generic one.
Disadvantages:
-
High Memory Usage: It needs memory to store source code, bytecode, and also memory to store the compiled machine code (Code Cache).
-
Runtime Overhead: The act of compiling code itself consumes CPU. When a program first starts, there might be a brief stutter (Warm-up phase) because JIT is working hard.
-
Unpredictability: Sometimes, due to triggering "de-optimization," performance can suddenly drop.
Representative JIT Engines Today:
-
V8: Used by Chrome browser and Node.js, making JavaScript run very fast.
-
HotSpot: Default engine for the Java Virtual Machine (JVM).
-
PyPy: Python's JIT implementation (5-10 times faster than standard Python).
Would you like me to explain why, despite JIT, languages like Python (standard version) are still considered "slow" compared to C++?
So compilers also need to be constantly optimized?
That's a very profound question. The answer is: Yes, compilers not only need to be "optimized," but this optimization war will never end.
Compiler Engineers are among the highest-paid and most scarce talents in the computer field, precisely because their work is never done.
Why can't a compiler be "written once and done"? Why does it need constant updates and optimization? There are mainly three core reasons:
1. Hardware Changes: CPUs are "Constantly Evolving"
The compiler is the translator between software and hardware. Although the C language code hasn't changed, the underlying CPU is always changing.
-
New Instruction Sets: Intel releases a new CPU that supports AVX-512 instructions (can process more data at once).
-
Old Compiler: Doesn't know about this new instruction, can only use old methods to calculate one by one.
-
New Compiler: Must be upgraded to learn to use this new instruction. Once learned, recompiling the old code immediately improves performance by 20%.
-
Architecture Changes: For example, Apple switched from Intel chips to M1/M2/M3 (ARM architecture).
-
For the compiler backend, this is like "changing the species." The compiler must be specifically redesigned and tuned for the characteristics of the M1 chip (like its huge instruction cache, unified memory architecture) to squeeze out its performance.
2. The Mathematical Bottomless Pit: Perfect Optimization is "Impossible"
You might think: "Can't we write a perfect compiler that always generates the world's fastest machine code?"
Mathematics tells us: It's impossible.
In computer science, code optimization has been proven to be an "NP-Complete Problem" or "Undecidable Problem."
-
Simple Explanation: For any slightly complex piece of code, finding the "absolutely fastest" arrangement of instructions would require a computation time that could be the lifespan of the universe.
-
Practical Approach: Compiler engineers can only design various "Heuristics" — which are "rules of thumb" or "smart guesses."
-
For example: "I think unrolling this loop 4 times might be better than unrolling it 8 times, but I'm not sure."
-
Engineers in each generation are looking for more accurate and better "guesses," trying to infinitely approach that theoretical "perfection," but they will never reach it.
3. The Contradiction in User Needs: Compile Time vs. Runtime
Optimization is not free. The longer the compiler "thinks," the faster the generated code usually is, but the longer you wait for compilation.
This is why compilers offer "Optimization Levels" for users to choose:
-
-O0 (No Optimization):
-
Compiler Status: Doesn't think at all, literal translation.
-
Characteristics: Extremely fast compilation (click and run), convenient for debugging (code line numbers match), but runs slowly.
-
-O2 (Standard Optimization):
-
Compiler Status: Thinks normally, performs safe and reliable optimizations.
-
Characteristics: The default standard in the industry, a balance between speed and size.
-
-O3 (Aggressive Optimization):
-
Compiler Status: Thinks hard, tries various aggressive methods (like splitting large functions, completely restructuring loops).
-
Characteristics: Slow compilation, the generated program might run extremely fast, but the program size increases (code bloat), and in rare cases, it might even introduce logical errors (Bugs).
The Frontier of Compilers: AI is Entering the Arena
Since human engineers can't find the "perfect solution," people are now starting to use AI for compiler optimization.
-
Before: Human engineers wrote fixed rules — "If a loop has fewer than 10 iterations, unroll it."
-
Now (Machine Learning in Compilers): Google and Facebook are training AI models. They let the AI look at billions of lines of code and then tell the compiler: "Hey, based on my experience, when you encounter code shaped like this, moving these three lines after that line will make it run 5% faster."
This is why compiler projects like LLVM and GCC are still as active as if they were just born, with thousands of lines of code updates every day.
Summary: A compiler is not a static tool; it is a living, evolving "super-brain," and its level of intelligence directly determines the operational efficiency of all software in the world.
Would you like me to explain what "Loop Unrolling" (a classic optimization technique) looks like with a simple code example?
Is JIT development harder than developing a normal compiler?
There is an unequivocal answer: yes, developing a JIT compiler is far more difficult than developing an ordinary AOT (Ahead-of-Time) compiler.
If writing an ordinary C compiler is “building a house,” then developing a high-performance JIT (such as V8 or the JVM’s HotSpot) is like “changing the engine while flying a plane.”
JIT development is difficult because it not only requires understanding all compiler theory, but also faces three hell-level constraints:
1. An Extremely Strict “Time Budget” (Time Budget)
This is the most direct difference.
-
Ordinary Compiler (AOT):
-
Mindset: If compiling a large C++ project takes 10 minutes, programmers may complain, but they can accept it.
-
Algorithm Choice: You can use extremely complex algorithms (with, for example, O(n 2) or even O(n 3) complexity) to calculate the optimal solution. You can treat the entire program as a whole (whole-program optimization, LTO) and repeatedly refine it.
-
JIT Compiler:
-
Mindset: The user is clicking around a webpage. If your compiler dares to cause a stall of more than 50 milliseconds, the user will think, “This webpage is so laggy,” and close it.
-
Algorithmic Limitation: You cannot use overly sophisticated optimization algorithms, because they are too slow!
-
Contradiction: You must generate high-quality machine code (so it runs fast), but you must also generate it at extremely high speed (so it does not cause lag). This requires an extremely subtle algorithmic balance (Trade-off).
2. The Nightmare-Like “On-Stack Replacement” (OSR - On-Stack Replacement)
This is the Boss in the beginner village that drives the most newcomers away from JIT development.
Imagine this:
-
A loop belonging to the user is running in the interpreter and has already reached its 5,000th iteration (in the middle of a
whileloop). -
The JIT thinks: No, this is too slow. I’m going to compile it into machine code.
-
Here comes the difficult part: The code is currently running! You must seamlessly switch the current “interpreter state” to the “machine-code state” without stopping the program or resetting variables, and then let the CPU continue running from the 5,001st iteration.
This means you need to:
-
Precisely move the variables in the interpreter’s memory into the CPU’s physical registers.
-
Reconstruct the interpreter’s virtual stack frame into the machine code’s physical stack frame.
-
If you get even one byte wrong, the program crashes immediately (SegFault).
This is like performing a heart transplant while the heart is still beating; the difficulty is easy to imagine.
3. “Gambling” and “Rollback” (Speculation & De-optimization)
An ordinary compiler only needs to guarantee “correctness,” whereas a JIT must learn to “gamble.”
-
AOT: On seeing
a + b, it must consider every possibility (What if there is an overflow? What if the types do not match?). The generated code is very conservative and bloated. -
JIT:
-
Observe: I see that this has been integer addition the last 1,000 times.
-
Speculate (Speculate): I’ll bet it will be an integer next time too! I’ll generate an extremely concise integer-addition instruction and remove all those complex checks.
-
Set a Trap: But what if the bet is wrong? (For example, a floating-point number appears on the 1,001st iteration.) The JIT must plant a “trap” in the machine code.
-
Rollback (De-optimization): Once the trap is triggered, the program must instantly “cross over” from the world of machine code back into the world of the interpreter, restore its previous state, and continue running in slow mode.
Difficulty: Safely implementing the process of “de-optimizing” from optimized machine code back to the interpreter, while also ensuring that the data remains completely consistent, requires an enormous amount of engineering work and logical complexity.
4. Debugging Difficulty: Crashes Caused by “Uncertainty”
-
AOT: If the code is compiled incorrectly, it fails in the same place every time. You can debug it slowly.
-
JIT:
-
The bug appears because: the user clicks A first and then B, causing the JIT to trigger level-3 optimization; just then, insufficient memory triggers GC; GC moves an object’s address, causing the machine code generated by the JIT to access the wrong address…
-
Heisenbug: As soon as you try to debug it (by adding a breakpoint), the program runs more slowly, the JIT’s triggering timing changes, and the bug disappears!
-
JIT engineers routinely face unreproducible crashes and need exceptionally strong low-level expertise.
Summary: Why Is JIT So Difficult?
Dimension Ordinary Compiler (AOT)JIT Compiler
Visible scopeSource code only Source code + runtime data (Runtime Profile)
Compilation timingWhile the developer is asleep While the user is using it
Memory constraintsUse as much as needed; release it after compilation Must be extremely frugal and compete with the application for memory
Core difficultiesComplex mathematical algorithm optimizationState migration (OSR), de-optimization (De-opt), concurrent compilation
Illustrative analogy****Architect (draws the plans and builds slowly)Special forces soldier (responds in real time according to circumstances on the battlefield)
For precisely this reason, mature commercial JIT engines are few and far between worldwide (V8, SpiderMonkey, JavaScriptCore, HotSpot, .NET CLR). Engineers capable of hand-writing JIT engines are usually top-level architects fiercely sought after by major technology giants.
Who actually develops compilers?
Developing compilers (especially modern, high-performance ones) is an extremely capital-intensive, intellectually demanding "heavy industry."
Today, the development of mainstream compilers is no longer an "individual effort" but a vast network woven together by tech giants, chip manufacturers, academia, and open-source communities.
We can categorize them into the following tiers:
Tier 1: The Tech Giants
Motivation: Control the ecosystem. If I control the language and compiler, I control the developers, and thus the future of the software ecosystem.
-
V8 Engine (JavaScript): To make Chrome fast, Google assembled a world-class compiler team (located in Denmark and Munich).
-
Go Language Compiler: To solve Google's internal large-scale concurrency programming problems.
-
LLVM Contributions: Google is one of the largest contributors to LLVM (the cornerstone of modern compilers), for Android and data centers.
- Apple
-
LLVM & Clang: Originally a university project, Steve Jobs saw its potential and hired its author, Chris Lattner. Apple fully funded the project to break free from GCC. All software running on your iPhone and Mac today is essentially compiled with this toolchain.
-
Swift: A self-developed language built on LLVM to solidify the iOS ecosystem.
- Microsoft
-
Roslyn (C#): Microsoft completely rewrote the C# compiler, making it open-source and modular.
-
TypeScript: Developed by programming legend Anders Hejlsberg (also the father of C# and Delphi).
-
MSVC: The C++ compiler bundled with Visual Studio, with a long history and the foundation of Windows software.
- Meta (Facebook)
- They developed HHVM (PHP JIT) and later Hermes (a JS engine specifically for React Native).
Tier 2: Hardware Vendors
Motivation: Sell chips. If software doesn't run fast on my chip, no one will buy it. So they must develop extremely strong compiler backends.
- Intel
- ICC (Intel C++ Compiler): Although now transitioning to the LLVM-based ICX, Intel has a massive software team dedicated to "microarchitecture-level" optimizations for Intel CPUs (e.g., automatically using AVX-512 instructions).
- NVIDIA
- NVCC (CUDA Compiler): This is NVIDIA's lifeline. Why does everyone use NVIDIA for AI? Because its CUDA compiler translates C++ code into GPU instructions with extreme efficiency. Without this compiler, the H100 GPU is just a brick.
- ARM
- They maintain the LLVM backend for ARM architectures (phones, Apple M chips), ensuring code runs fast even on low-power chips.
Tier 3: Academia
Motivation: Explore theoretical boundaries. Many revolutionary compiler technologies originate in university labs.
-
UIUC (University of Illinois Urbana-Champaign): The birthplace of LLVM. Chris Lattner was just a master's/PhD student there; his advisor, Vikram Adve, is still a big name in compiler academia.
-
Stanford & Berkeley: Many studies on AI compilers and formal verification originate here.
-
Characteristic: Academia typically produces prototypes or papers, which are then industrialized by Tier 1 companies.
Tier 4: Open Source Communities and Foundations
Motivation: Freedom and collaboration.
-
GNU Community (GCC):
-
A group of hackers who believe in free software. Although many employees of large companies now contribute code to GCC, it still retains a unique "grassroots" and "anti-commercial monopoly" spirit.
-
The Linux kernel is primarily compiled with GCC.
-
Rust Foundation:
-
Although the Rust language originated at Mozilla, it is now managed by an independent foundation, with AWS, Google, Huawei, and Microsoft contributing money and people.
A Typical "God-Level" Profile: Who Writes the Code?
People who develop compilers are often called Compiler Engineers. They are the special forces of programmers.
-
Skill Tree:
-
Deep understanding of computer architecture (CPU pipelines, caches, instruction sets).
-
Deep understanding of discrete mathematics and graph theory (control flow graphs, graph coloring, lattice theory).
-
Deep understanding of assembly language (x86, ARM, RISC-V).
-
Salary:
-
Very high. In Silicon Valley or major domestic companies, the salary of a compiler expert is usually a tier higher than that of ordinary application-layer developers (writing web pages, writing apps).
-
Because talent is extremely scarce. There may only be a few hundred to a few thousand people globally who can hand-write a high-performance JIT engine.
Summary: A Concrete Example — LLVM
To understand the development model of modern compilers, look at LLVM:
-
Initiation: An academic project started by Chris Lattner during his PhD at UIUC.
-
Funding: Apple hired him, poured money into it, and turned it into an industrial-grade product to replace GCC.
-
Growth: Google, Sony, Intel found it useful and joined in.
-
Google contributed sanitizers (memory detection tools).
-
Sony used it to compile PS4/PS5 games.
-
Intel helped optimize CPU instruction generation.
- Current Status: It is now an open-source project belonging to all humanity, but its core maintainers are mostly employed by the aforementioned large companies.
So, "Who develops compilers?" The answer is: Top mathematicians and computer scientists earning high salaries at big tech companies, collaborating under the banner of open source.
Would you like me to tell the legendary story of Chris Lattner? He is basically the "Steve Jobs" of the modern compiler world.
Not many people in China are doing this, right?
This is a very common misconception.
To be factual: 10 years ago, you would have been right. Back then, China was mostly "using" compilers, with very few people "building" or "modifying" them.
But the situation now is: China is currently one of the most active and heavily invested regions in global compiler development.
Why? Because "they were forced to" and also because "a new track has emerged."
We can divide the forces doing compiler work in China into four major camps:
1. The "Last Stand" Faction: Huawei
Huawei currently has the most compiler experts and the most hardcore technology in China, bar none. They were forced into this by US sanctions.
-
Bisheng Compiler:
-
Huawei developed its own Kunpeng chips (ARM architecture) and Ascend chips (AI chips).
-
Without compiler support, these chips are scrap metal. Huawei had to deeply customize LLVM to develop a compiler that efficiently translates C/C++ code into Kunpeng instruction sets.
-
Ark Compiler / ArkTS:
-
For HarmonyOS. To squeeze maximum performance out of Java/JS for HarmonyOS, they had to heavily modify the compiler. They developed static compilation technology, allowing Java code to be directly compiled into machine code without a virtual machine dynamically interpreting it, solving Android's lag issues.
-
Scale: Huawei has thousands of people in its internal teams dedicated to tackling low-level software like compilers, operating systems, and databases.
2. The "Cost Reduction & Efficiency" Faction: Internet Giants (Alibaba, ByteDance, Tencent)
These companies have massive server farms. If a compiler can optimize performance by even 1%, for a company with millions of servers, the annual savings in electricity and hardware procurement costs can be hundreds of millions of RMB.
-
Alibaba:
-
Dragonwell: Alibaba is one of the world's largest Java users. They deeply customized OpenJDK to create their own JDK version. They made extremely deep optimizations to the JIT compiler inside the JVM to handle the terrifying traffic of Singles' Day.
-
RISC-V: The DAMO Academy is promoting the XuanTie chip, which requires a full set of compiler toolchain support.
-
ByteDance:
-
They invest heavily in the Go language compiler and the V8 engine (JS). Because Douyin/TikTok's backend heavily uses Go, and the frontend heavily uses JS. Optimizing the compiler directly saves money.
-
Tencent:
-
Konajdk (Tencent's JDK), and optimizations for C++ compilers in the gaming field.
3. The "Curve Overtaking" Faction: AI Chips and Autonomous Driving
This is currently the area with the biggest talent shortage. This is the so-called AI Compiler field.
-
Background: Modern AI models (like Llama, GPT) are written based on PyTorch/TensorFlow. But the underlying chips are diverse (Huawei Ascend, Cambricon, Horizon Robotics, Moore Threads, Biren Technology).
-
Pain Point: How do you translate PyTorch code for these domestic chips?
-
Current Status: Every domestic chip company must maintain a large team of compiler writers. If the compiler isn't good, no matter how powerful the chip is, its performance cannot be realized (this is called "low compute utilization").
-
Representative Figure/Project: Chen Tianqi (Tianqi Chen) (a super god in the field of machine learning compilation, author of TVM, although at CMU/OctoML, he has influenced a large number of domestic developers). Domestically, the Institute of Computing Technology of the Chinese Academy of Sciences and Tsinghua University are very strong in AI compilation.
4. The "Open Source Grind" Faction: CAS and the PLCT Lab
A name must be mentioned here: Wei Wu and his PLCT Lab (Intelligent Software Research Center, Institute of Software, Chinese Academy of Sciences).
-
Goal: They are one of the rare "national teams" in China that systematically grinds on open-source compiler toolchains.
-
RISC-V: China is betting on the RISC-V architecture as the future chip standard. The PLCT Lab is frantically contributing code to GCC and LLVM to ensure first-class compiler support for RISC-V. Their slogan is to train 10,000 core compiler developers.
Why Do You Feel "No One Is Doing It"?
-
High Threshold, Small Circle: The number of people in China who can hand-write a JIT engine is indeed small enough that they probably all know each other. This circle is not as large as "frontend development" or "Java development"; it's at the tip of the pyramid.
-
Not Directly User-Facing: You use HarmonyOS and find it smooth, but you don't know the Ark Compiler is working behind the scenes. You play Black Myth: Wukong and see the graphics, but you don't know the shader compiler is working behind the scenes.
-
Started Late, But Catching Up Fast: We are still relatively rare in "creating new languages" (like creating Python, Go, Rust). But in "optimizing compilers" and "adapting to domestic chips," China is already a world-class player.
Realistic Salary Situation
You can search job platforms for "Compiler Engineer" or "LLVM Developer":
-
Salary: Typically 1.5 to 2 times that of an ordinary programmer at the same level.
-
Hiring: Huawei, big tech companies, and chip unicorns are all frantically poaching talent.
So, it's not that no one in China is doing it; it's that they are frantically hiring people to do it. It's just that these people are usually busy reading papers and looking at assembly code, so they keep a low profile.