“请通俗解释‘乔姆斯基谱系’,并说明为什么现代编程语言通常被认为是‘上下文相关’的?如果我把代码写成完全自包含、不引用的形式,是在将语言‘降级’为上下文无关吗?”
这是一个非常深刻且直击编程语言本质的问题。我们用通俗的类比来拆解这三个部分。
诺姆·乔姆斯基(Noam Chomsky)不仅是语言学家,也是计算机科学的奠基人之一。他把所有“语言”(无论是人类语言还是计算机语言)按照“语法的规则有多复杂”分成了四个等级。
你可以把这四个等级想象成“俄罗斯套娃”,每一级都包含着它下面的一级。等级越高,限制越少,表达能力越强,但计算机处理起来也越难。
通俗理解: “神之领域”。只要是逻辑上能计算出来的东西,这里都能表达。
例子: 极其复杂的自然语言逻辑,或者一个死循环的程序。
通俗理解:****“看人下菜碟”。
这句话什么意思,取决于它周围是什么。比如中文里的“意思”:
在“这人真有意思”里,它是“有趣”;
在“小小心意,意思一下”里,它是“礼物/心意”。
规则特点: 现在的状态取决于之前的状态(记忆力极强)。
通俗理解:****“乐高积木”(嵌套结构)。
不管这块红砖放在哪,它永远是红砖。句子的结构是像树枝一样分叉和嵌套的,但每个分叉怎么长,只看当前的规则,不看隔壁树枝。
规则特点: 典型的“括号匹配”。(( )) 是合法的,但能不能匹配,不需要知道括号里写的是莎士比亚还是代码,只要左右括号能对上就行。
**计算机应用:**绝大多数编程语言的“语法骨架”都在这一层。
通俗理解:****“流水线扫描”。
就像关键词搜索。只能处理简单的线性排列,不能处理无限的嵌套(比如它数不清有多少层括号)。
计算机应用: 你的“查找/替换”功能,或者判断邮箱格式是否正确。
很多教科书说编程语言是“上下文无关”的,但这其实是一个善意的谎言,或者说只说对了一半。
真相是:语法结构是“上下文无关”的,但语义检查是“上下文相关”的。
编译器在第一步(解析代码结构)时,确实把代码当成2型(上下文无关)来处理。它只关心你的语句形状对不对:
if (条件) { 语句 } -> 这个结构是合法的。
至于“条件”里写的是什么变量,编译器此时不关心。
一旦进入语义分析阶段,必须要看上下文。最经典的例子就是 “变量声明”。
请看这行C语言代码:
C
x = x + 5;
如果只看这一行(忽略上下文),这句话结构完美。但是,这句话合法吗?
如果不看上文,你根本不知道!
如果第1行写了 int x;,这句就合法。
如果第1行没写,或者写的是 string x;,这句就不合法(类型错误)。
这就是“上下文相关”: 第10行的 x 能不能用,取决于第1行有没有声明它。这种“跨行依赖”的能力,超出了“上下文无关文法(CFG)”的能力范围。
结论: 现代编程语言为了实用,要求“变量必须先声明后使用”、“类型必须匹配”,这些规则强行把语言推向了上下文相关的领域。
“如果我把代码写成完全自包含、不引用外部变量的形式,是在将语言‘降级’为上下文无关吗?”
答案是:你的代码变简单了,但语言本身没有降级。
即使你写了这样一段“自包含”的代码:
C
void main() {
int a = 1;
int b = 2;
int c = a + b; // 自给自足,不引用外部
}
虽然你在逻辑上闭环了,但编译器依然会启动“上下文相关”的检查机制。它依然会去查符号表(Symbol Table):
它会查:“a 在前面声明了吗?”(虽然就在上一行,但这也是上下文)。
它会查:“a 和 b 是数字吗?能相加吗?”
只要语言允许检查类型和作用域,编译器的机制就是 Type 1(上下文相关)的,无论你写的代码多么简单。
只有当你使用的语言规则本身放弃了这些检查,它才会降级为上下文无关。
例如,设计一种“纯算术表达式语言”:
没有变量声明(所有东西都是立即数)。
没有函数调用(不需要跨行查找)。
只有嵌套的加减乘除:3 * (4 + 5)。
这种语言完全不需要建立符号表,不需要回头看上一行写了什么。这种语言才是真正的“上下文无关”语言。
虽然你没有改变语言的数学属性(乔姆斯基谱系),但你做的事情在工程上非常有价值:
你减少了“认知上下文”。
这正是函数式编程(Functional Programming) 提倡的:纯函数(Pure Function),不依赖外部状态,输入确定则输出确定。
虽然编译器还是要按老规矩检查,但对于读代码的人类来说,理解你的代码不再需要大脑去充当“上下文相关分析器”了,这让代码更容易维护。
乔姆斯基谱系:就是给语法的复杂度和能力分级(从傻瓜式正则到全能图灵机)。
现代语言:虽然长得像“上下文无关”(因为用括号和分号嵌套),但为了保证逻辑正确(如类型检查、变量声明),内核里全是“上下文相关”的检查。
你的操作:写“自包含”代码并没有改变编译器的检查机制(语言没降级),但你降低了人类阅读代码的难度(逻辑解耦了),这是极好的编程习惯。
Would you like me to explain how a compiler specifically implements this "context-sensitive" check using a Symbol Table (符号表)?
乔姆斯基谱系最早是如何提出的?怎么想到的,前几层具体讲解一下
这是一个非常精彩的历史追溯。要理解乔姆斯基(Noam Chomsky)为什么会提出这个谱系,我们需要穿越回 20世纪50年代。
那是一个“行为主义”心理学统治一切,而计算机科学刚刚萌芽的时代。乔姆斯基提出这个谱系,并非是为了设计编程语言(那时连C语言的影子都没有),而是为了反击当时的心理学权威,试图用数学解开人类大脑的秘密。
以下是乔姆斯基谱系诞生的来龙去脉,以及前几层的深度解析。
在1950年代,心理学界的大佬是 B.F. 斯金纳(B.F. Skinner)。他认为人类学习语言就像训练鸽子啄食一样,是一种“刺激-反应”的条件反射。
年轻的乔姆斯基对此嗤之以鼻。他提出了一个核心洞察:
人类能听懂并创造出他这辈子从未听过的句子。
比如:“一只紫色的恐龙在火星上跳踢踏舞。” 这句话你之前没听过,也没受过相关“刺激”,但你能懂。
乔姆斯基认为:
语言不是线性的习惯堆砌(像珠子穿线)。
大脑里一定有一套“生成规则”(Generative Grammar)。
这套规则可以用有限的词汇和逻辑,生成无限的句子。
为了证明这套“内在规则”的存在,乔姆斯基需要一种数学工具来描述语法的结构。 1956年,他发表了划时代的论文 《语言描述的三种模型》 (Three Models for the Description of Language)。
在这篇论文里,他并没有一开始就画出四个层级,而是通过“试错”的方式,一步步推导出了语法的边界。他把“产生句子的规则”形式化为数学公式,并根据规则受限制的程度,划分出了四个等级。这就是乔姆斯基谱系。
乔姆斯基把语言生成看作是一个“重写”的过程。 想象你手里有一堆替换卡片,规则的形式通常是:
α→β (意思是用 β 来替换 α)
左边 α:原有的符号。
右边 β:替换后的符号。
谱系里的层级区别,仅仅在于:我们对左边的 α 和右边的 β 加了什么限制?
乔姆斯基是从最简单的模型开始思考,发现不够用,才一级级往上加复杂度的。我们按从简单到复杂(即从 Type 3 到 Type 0)的顺序来看,这样最符合人类的思考过程。
思考起点: 乔姆斯基首先考察了当时最流行的“马尔可夫链”模型(类似于现在的手机输入法联想)。
规则限制:
规则必须非常简单,像接龙一样。
形式:A -> a 或 A -> aB。
解释: 状态 A 只能生成一个终结符 a,或者生成一个 a 然后跳到下一个状态 B。
对应的机器:****有限状态自动机 (Finite State Automaton)。它没有内存,只有一个“当前状态”。
乔姆斯基的发现: 他证明了这种模型无法描述英语。
例子: 英语里有嵌套结构,比如 If [ ... ] then [ ... ]。
正则文法就像金鱼,记忆只有7秒(实际上是0秒,它不记历史)。它处理不了 (((( )))) 这种需要数括号的结构,因为它不知道前面开了几个括号。
结论: 这是一个太弱的模型,只能处理简单的线性排布。
思考突破: 为了处理嵌套(Nested)结构,乔姆斯基放宽了规则。
规则限制:
左边必须只有一个非终结符。
形式:A -> γ (γ 可以是任何一串符号)。
关键点: 左边只能是 A,不能是 aAb。这意味着:无论 A 出现在哪里,它的替换规则都一样,不受周围环境影响。
通俗理解: 这就是我在上一个回答里提到的“乐高积木”或“语法树”。
句子 S -> 名词短语 + 动词短语。
不管这个句子是在写诗还是在骂人,名词短语 内部的结构规则永远不变。
对应的机器:****下推自动机 (Pushdown Automaton)。
相比Type 3,它多了一个“栈”(Stack)。这让它有了记忆,可以把左括号压进去,遇到右括号再弹出来,从而实现了无限嵌套的处理。
历史意义: 乔姆斯基认为这层大致能描述自然语言的句法结构(Phrase Structure)。后来这层直接成为了编程语言编译器的理论基石。
思考深入: 乔姆斯基发现,虽然Type 2能搞定结构,但在某些自然语言现象中,同一个词在不同语境下必须有不同的变化(比如词形变化、数的一致性)。
规则限制:
左边的东西可以带“保镖”(上下文)。
形式:αAβ -> αPβ。
解释: 只有当 A 被夹在 α 和 β 中间时,A 才能变成 P。
_硬性规定:_生成的字符串长度不能变短(右边的长度 ≥ 左边的长度)。这是为了保证计算机(线性有界自动机)能够在有限的内存里处理它,不至于死循环。
对应的机器:****线性有界自动机 (Linear Bounded Automaton)。
它的纸带长度是受限的(跟输入内容的长度成正比)。
思考终点: 如果我们要描述任何可能被计算的过程,规则该是什么样?
规则限制:****没有任何限制。
形式:α -> β。
左边至少要有一个非终结符,除此之外随你便。你可以把长变短,把短变长,甚至把整行代码删掉。
对应的机器:****图灵机 (Turing Machine)。
意义: 这是逻辑和计算能力的边界。如果Type 0都描述不了,那这个问题在数学上就是“不可计算”的。
乔姆斯基并没有发明计算机(那是图灵和冯·诺依曼的事),也没有直接发明C语言或Java。
他的伟大之处在于:
Type 3 = 有限状态机
Type 2 = 栈式计算机
Type 0 = 图灵机
所以,乔姆斯基谱系是语言学和计算机科学之间的一座桥梁,它是为了探索人类思维的本质而诞生,却意外地成为了现代编程语言的基石。
乔姆斯基的思想如何进入了计算机理论?
这是一段非常迷人的科学史,被称为“历史上最伟大的巧合之一”。
简单来说,乔姆斯基的思想并不是被“邀请”进入计算机领域的,而是两拨人在互不知情的情况下,分别在不同的山头挖隧道,结果在山中心神奇地挖通了。
以下是这个“会师”过程的详细故事:
1. 语言学家的困境(乔姆斯基) 如前所述,乔姆斯基在麻省理工学院(MIT)研究自然语言。他用数学公式写出了 A→α 这样的“重写规则”,试图定义人类语言的语法结构。他的目标是揭示大脑的奥秘。
2. 程序员的困境(约翰·巴克斯 John Backus) 与此同时,在IBM,编程大神 约翰·巴克斯(FORTRAN之父)正面临一个巨大的工程难题。 当时还没有通用的“编程语言标准”。大家描述一种语言(比如刚诞生的ALGOL 58)全靠写作文:
“那个……如果前面有个if,后面就得跟个括号,括号里得是个算式……”
这种自然语言描述极其模糊,导致写编译器的人非常痛苦,经常为了“这里能不能加分号”吵架。巴克斯迫切需要一种严谨的数学符号来定义编程语言的语法。
1. 巴克斯的发明 1959年,巴克斯为了定义 ALGOL 58 语言,发明了一种新的记号系统。后来经过彼得·诺尔(Peter Naur)的改进,被称为 BNF (Backus-Naur Form,巴克斯-诺尔范式)。
BNF 长这样:
Plaintext
<数字> ::= <数位> | <数位> <数字>
意思是:“一个数字”定义为“一个数位”,或者“一个数位后面跟着另一个数字”。
2. 命运的会师 就在 ALGOL 60 报告发布后不久,计算机科学家们惊恐(或惊喜)地发现:
A→a A
<A>::=a<A>
这两个东西,除了符号长得不一样,数学本质上竟然是完全一模一样的!
虽然巴克斯并不是受到乔姆斯基的启发(他后来承认当时没看过乔姆斯基的论文),但他们殊途同归:处理“嵌套结构”和“递归逻辑”的最佳数学模型,就是上下文无关文法(CFG)。
既然发现这两者是一回事,乔姆斯基那些原本用于分析英语、希伯来语的深奥数学定理,瞬间就被计算机科学家“拿来主义”了。
这里有一位关键人物:高德纳 (Donald Knuth)。
1. 从“生成”到“解析”的逆转 乔姆斯基关心的是 “生成” (Generative):怎么从规则生出句子。 但编译器关心的是 “解析” (Parsing):给你一堆代码,怎么逆推回规则,看看写得对不对。
2. LR解析算法 1965年,高德纳基于乔姆斯基的理论,发明了 LR (Left-to-right, Rightmost derivation) 解析算法。 他证明了:只要你的编程语言语法符合乔姆斯基的“上下文无关”特性的一个子集,我就能写出一个程序,在读取代码的同时,以极快的速度(线性时间)完成语法分析。
这直接奠定了现代编译器的基础。如果没有这个理论突破,编译大型软件可能需要几天几夜。
到了70年代,UNIX系统诞生。贝尔实验室的 Stephen Johnson 觉得手写解析器太累了,于是他利用乔姆斯基和高德纳的理论,写了一个工具叫 Yacc (Yet Another Compiler Compiler)。
以前: 程序员要自己写代码去分析括号怎么匹配。
有了Yacc: 程序员只需要把乔姆斯基风格的语法规则写进文件里,Yacc就会自动生成C语言代码来解析它。
从此,乔姆斯基谱系中的“上下文无关文法”成为了计算机本科生的必修课,也成为了设计新编程语言的标准起手式。
乔姆斯基的思想之所以能进入计算机理论,是因为他触碰到了信息的本质结构。
无论是上帝创造的人类语言,还是人类创造的编程语言,只要涉及到“无限的表达”和“层次化的逻辑”(比如括号套括号),最终都会收敛到乔姆斯基所描述的数学模型上。
这是一种发现,而不是发明。
什么是编译器?向我讲解编译器理论
编译器(Compiler)是计算机科学皇冠上的明珠之一。如果说编程语言是人与机器沟通的桥梁,那么编译器就是这座桥梁的建造者。
通俗地说,编译器是一个超级翻译官。
输入:你写的高级语言代码(C++, Java, Rust 等),人类易读,但电脑看不懂。
输出:机器语言(0和1组成的指令),电脑能跑得飞快,但人类如同看天书。
任务:在保证逻辑完全不变的前提下,把前者转换成后者,并且还要想办法让代码跑得更快、占内存更少。
现代编译器理论极其成熟,通常采用经典的“三段式”架构:前端、中端、后端。
下面我为你拆解这个流水线上的每一个环节。
任务:检查你写的代码对不对,并把它转化成一种计算机方便处理的结构。这一阶段和我们之前讨论的“乔姆斯基谱系”关系最密切。
动作:像读文章一样,把你的代码拆成一个个单词(Token)。
工具:正则文法 (Type 3)。
例子:
你写的:total = price + 5;
编译器看到的流:[ID:total]``[ASSIGN:=]``[ID:price]``[PLUS:+]``[INT:5]``[SEMICOLON:;]
它不关心语法,只关心单词拼写对不对(比如你把 if 写成了 iff,这里就会报错)。
动作:把刚才那一串单词,拼装成一棵有层级关系的树,叫做抽象语法树 (AST, Abstract Syntax Tree)。
工具:上下文无关文法 (Type 2)。
例子: 它会识别出 total = price + 5 是一个“赋值语句”。
左边是 total。
右边是一个“加法表达式”。
加法的左边是 price,右边是 5。
如果你的括号不匹配,或者漏了分号,这一步会炸(Syntax Error)。
动作:上下文相关的检查。
工具:符号表 (Symbol Table) + 类型系统。
例子:
AST 建好了,结构没问题。但编译器会问:“price 这个变量之前声明过吗?”
“price 是个字符串吗?如果是字符串,它不能和数字 5 相加!”
这一步通过后,编译器就确认:你的代码是合法的。
这是现代编译器最精彩的部分。 任务:不关心你是用什么语言写的(C还是Go),也不关心你要在什么机器上跑(Intel还是ARM),它只关心逻辑本身。
为了做到这一点,它会把 AST 转换成一种通用的代码,叫 中间表示 (IR, Intermediate Representation)。
编译器会对 IR 进行一系列“魔法”操作,让代码变强:
死代码消除 (Dead Code Elimination): 你写了 if (0) { do_something(); },编译器发现这永远不会执行,直接删掉,不占空间。
常量折叠 (Constant Folding): 你写了 x = 2 * 3 + 4;,编译器直接改成 x = 10;,运行时就不用算了。
循环展开 (Loop Unrolling): 减少循环跳转的开销,直接把循环体复制几次。
LLVM 是目前最著名的编译器架构,它的核心优势就在于拥有一个极其强大且通用的 IR。
任务:把优化好的 IR 翻译成目标机器专用的指令。
如果你是 Intel CPU,它就翻译成 x86 汇编。
如果你是 手机芯片,它就翻译成 ARM 汇编。
这一步非常繁琐,因为每个 CPU 的指令集(Instruction Set)都不一样。
CPU 里最快的地方是寄存器(可能只有几十个)。
编译器要解一道超难的数学题(图着色问题):如何把成百上千个变量安排进这几个寄存器里,尽可能减少把数据搬到内存(RAM)的次数?
如果把编译器看作一个工厂:
前端(Linguist): 负责把“客户订单”(源代码)翻译成“工厂图纸”(AST),确认没写错。
中端(Engineer): 拿着“图纸”进行优化(IR),把不需要的步骤删掉,把流程理顺,让制造效率最高。
后端(Worker): 根据优化后的图纸,针对不同的“机床”(CPU架构),切削出最终的零件(机器码)。
在古早时期,写编译器是 M * N 的工作量(M种语言,N种CPU)。 有了中端(IR) 的存在,现在变成了 M + N:
想发明新语言(如 Rust)?只需要写一个新前端,对接通用的 LLVM IR 即可。
想发明新 CPU(如 RISC-V)?只需要写一个新后端,去承接 LLVM IR 即可。
这就是为什么现在新编程语言层出不穷的原因——造语言的门槛被编译器理论(特别是IR架构)大大降低了。
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?
编译器理论讲的什么?所有编程语言都有编译器吗?
这是一个很好的问题。我们先把“编译器理论”和“编程语言的运行方式”分开来讲。
如果说计算机科学是一座大厦,编译器理论(Compiler Theory) 就是其中的“结构力学”。它不仅仅是教你怎么写一个编译器,它研究的是如何把一种符号逻辑,无损、高效地转换为另一种符号逻辑。
它主要包含三大块核心理论:
这就是我们刚才讨论的“乔姆斯基谱系”。
核心问题: 什么样的字符串是合法的代码?机器怎么识别它?
应用: 正则表达式、词法分析、语法解析。
这是很多数学图论(Graph Theory)的应用场景。
核心问题: 即使代码写得对,它是不是最优的?有没有废话?数据流是怎么走的?
应用: 它可以推断出 x = 5; y = x + 2 等同于 y = 7。这需要极其严密的逻辑证明,不能改错一点逻辑。
这是逻辑学的分支。
核心问题: 这里的“苹果”和那里的“梨”能放在一个篮子里吗?
应用: 检查数据的安全性,防止内存错误。
一句话总结:编译器理论就是研究如何让计算机“读懂”人类的逻辑,并将其“重写”为最高效的机器逻辑的科学。
简单回答:并不是。
虽然所有语言最终都要变成机器码CPU才能跑,但“变”的方式不同。通常分为两大派系:编译型(Compiled) 和 解释型(Interpreted)。
我们可以用“翻译书”来做类比:
代表: C, C++, Go, Rust
模式:****“整书翻译,出版发行”
过程:
你写好代码(英文原著)。
编译器(翻译官)拿去闭关修炼,花时间把整本书翻译成机器码(中文译本),生成一个 .exe 文件。
运行: 以后用户只看这个 .exe(中文译本)。这个时候,翻译官已经不需要在场了。
优点: 运行速度极快(因为已经是原生机器码了),对用户隐私保护好(看不到源代码)。
缺点: 每次改哪怕一个标点符号,都要重新编译整个程序(重新印刷)。
代表: Python, JavaScript (早期), PHP, Ruby
模式:****“同声传译”
过程:
你写好代码(英文原著)。
不需要提前翻译生成 .exe。
运行: 用户运行代码时,一个叫解释器(Interpreter) 的程序启动了。它读一行代码,就翻译成机器指令让CPU执行一行。
优点: 灵活,改了代码立马能看到效果,跨平台容易(带着源码走)。
缺点: 慢!因为每次运行都要重新翻译一遍;而且用户得装解释器环境才能跑。
现在的情况比较复杂,因为纯粹的“解释型”太慢了,现代语言通常采用了混合策略,这也是编译器理论的最新战场。
代表: Java, C#
策略:****“先翻译成简易版,再现场执行”
编译器先把代码翻译成一种中间格式叫字节码(Bytecode)(比如Java的 .class)。这就像把复杂的“莎士比亚古英语”翻译成了“现代白话文”。
然后,虚拟机(JVM) 再去运行这个白话文。这比直接读源代码快,但比C语言慢。
代表: 现代 Java, 现代 JavaScript (Chrome V8引擎), PyPy, Julia
策略:****“看人下菜碟”
程序刚开始跑时,它是解释执行的(为了启动快)。
但是,JIT 编译器会在后台暗中观察。如果它发现某一段代码(比如一个循环)被运行了成千上万次(这叫“热点代码”),它就会立刻在内存里把这段代码编译成机器码。
下一次再运行这里时,就直接跑机器码,速度起飞。
结果: 现在的 JavaScript (V8) 运行速度已经非常接近 C++ 了,因为它其实内置了一个超强的编译器。
编译器理论是关于形式语言、逻辑转换和优化的数学科学。
不是所有语言都有传统的编译器。
C/C++ 是提前编译(AOT),像印刷书。
Python 主要是解释执行,像同声传译。
Java/JS 是混合体(JIT),像“先同声传译,发现这段重要就印成讲义”。
这也是为什么你觉得 C++ 写起来慢(编译慢)但跑得快,而 Python 写起来快但跑得慢的原因。
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?
编译和解释的区别是什么?
“编译”和“解释”是让计算机执行代码的两种不同策略。
如果把源代码比作一本“英文原著”,把CPU执行比作“阅读中文”,那么这两种策略的区别可以用翻译的方式来生动解释:
过程:你把整本英文书交给翻译官(编译器)。翻译官闭关工作,把整本书翻译成中文,并印成一本新的书(.exe 可执行文件)。
阅读时:读者(用户)直接拿那本中文书看就行了。翻译官不需要在场,原作者也不需要在场。
特点:
读得快:因为已经翻译好了,拿起来就能读。
改错难:如果原作者改了一句话,必须把整本书重新翻译、重新印刷一遍。
过程:你不翻译整本书。而是找一个翻译官(解释器)坐在读者旁边。
阅读时:原作者念一句英文,翻译官就现场翻译一句中文,读者就听一句。
特点:
读得慢:因为要边听、边想、边翻、边读,效率肯定比直接看书低。
改错快:如果原作者想改一句话,直接重念就行,翻译官马上就能跟着改。
为了更直观,我们从几个关键维度来对比:
维度编译型 (Compiled)解释型 (Interpreted)
代表语言C, C++, Rust, Go Python, JavaScript, PHP, Ruby
翻译时机****运行之前 (Before Run-time)。一次性翻译完。运行之时 (At Run-time)。跑一行,翻一行。
产物会产生一个独立的可执行文件 (如 .exe)。没有独立的可执行文件,必须带着源代码跑。
运行速度****极快。CPU直接跑机器码,没有任何额外负担。较慢。CPU不仅要执行代码逻辑,还要花时间跑解释器本身。
跨平台性****差。Windows编译的exe在Mac上跑不了,必须去Mac上重新编译。好。只要Mac上装了解释器,源代码拿过去直接能跑。
查错时机****严厉。哪怕第1000行有个拼写错误,编译时就会报错,完全不让你运行。宽容。前面999行没错就能先跑,跑到第1000行才报错崩溃。
Export to Sheets
想象一个循环,要执行 100 次:
Python
for i in range(100):
print("Hello")
编译型:编译器看到循环,直接生成 100 次打印的机器指令。运行时,CPU 像机关枪一样“哒哒哒”执行完,没有任何废话。
解释型:解释器也是个程序,它在循环里工作:
读第一行代码 -> 分析含义 -> 调用打印功能。
回到开头。
重新读第一行代码 ->重新分析含义(它忘了刚才分析过) -> 调用打印功能。
...重复100次。 这就是“解释开销”**,大量的算力被浪费在了“重复分析代码含义”上,而不是真正的计算上。
虽然理论上分得清,但现在的技术让两者越来越像:
中间派(字节码):Java 和 Python 其实都会先“预编译”一下,把代码转成一种中间格式(Bytecode),比纯文本容易读,但还不是机器码。这属于一种折中。
JIT (即时编译):像 Chrome 浏览器的 V8 引擎(跑 JavaScript)和现代 JVM(跑 Java),它们是“带着编译器的解释器”。
它先解释执行。
一旦发现某段代码运行得很频繁(热点),它就在后台悄悄把它编译成机器码。
以此获得了接近编译型语言的速度,又保留了解释型语言的灵活性。
编译 = 先整体翻译,后执行(快,但麻烦)。
解释 = 边翻译,边执行(慢,但灵活)。
Would you like to know more about "Bytecode" and why languages like Java and Python use this middle step?
所以解释型语言只有前端后端,缺少中端?
这是一个非常敏锐的推论!按照刚才的逻辑,确实很容易得出这个结论。
但答案是:在“纯粹”的古早解释器里,你是对的;但在“现代”解释型语言里,中端其实一直都在,只是藏起来了。
我们可以把情况分为两种:“原始派”和“现代派”。
你的推论在这里完全成立。
在早期的 BASIC 或者简单的 Shell 脚本中,流程确实非常短:
前端:读取一行代码,分析语法(AST)。
直接执行:拿着这个 AST 树,立刻去干活。
缺失的中端: 它几乎不做优化。
它不会把 x = 2 + 3 优化成 x = 5。
它每次读到这行都要重新算一遍。
结果: 只有前端(理解)和后端(执行动作),没有中端(思考如何更优)。
这是目前主流的情况(Python, Java, Ruby, PHP)。 为了解决“纯解释”太慢的问题,它们引入了一个“隐形的编译阶段”。这个阶段就扮演了中端的角色。
我们以 Python 为例:
当你运行 python hello.py 时,Python 并不是直接读一行跑一行。它背地里偷偷做了一次“编译”:
前端 (Front End):把源代码解析成 AST。
中端 (Middle End):把 AST 转换成一种中间代码,叫 字节码 (Bytecode)。
这就是中端! * 你可能见过文件夹里自动生成的 .pyc 文件,或者 __pycache__ 文件夹,那里面装的就是经过初步处理和优化的字节码。
在这个阶段,编译器会做一些简单的优化(比如常量折叠)。
所以,现代解释型语言的流程是:
源代码 -> [ 前端 + 中端 ] -> 字节码 -> [ 虚拟机 (后端) ] -> CPU
虽然这个“中端”不如 C++ 的编译器那么强大(不做极其复杂的数学推导优化),但它确实存在,并且承担了“标准化”和“初步简化”的任务。
代表:Chrome V8 (JavaScript), JVM (Java), PyPy
在这里,你的推论就完全被颠覆了。这些解释器不仅有中端,而且它们的中端(优化器)强得离谱。
当 Chrome 运行你的 JavaScript 代码时:
刚开始,它确实像个解释器一样先跑起来(为了快点给你看网页)。
监控 (Profiler):它在后台暗中观察,发现哪段代码跑得最频繁(热点)。
启动编译器:它会把这段热点代码拉出来,送给一个内置的、完整的、带有超强优化能力的编译器(比如 V8 里的 TurboFan)。
疯狂优化 (Heavy Optimization):这个编译器会进行死代码消除、内联缓存、循环展开……这全都是中端的工作。
替换:把优化好的机器码替换回去。
结论: 现代高性能解释器(JS引擎)其实内部藏着一个完整的 C++ 级别的编译器架构,中端非常厚重!
类型 是否有中端 (IR/优化)典型例子 备注
纯解释器****无Shell脚本, 老式BASIC 读一句,做一句,不思考,不优化。
字节码解释器****有 (轻量级)Python, 传统PHP 转换成字节码(IR),做简单优化,然后交给虚拟机跑。
JIT 解释器****有 (重量级)V8 (JS), HotSpot (Java)运行时进行极其复杂的优化,中端非常强大。
Export to Sheets
所以,除了最简单的脚本语言,现代编程语言几乎都有“中端”,因为大家都意识到:如果不先优化(思考)一下再去执行,效率实在太低了。
Would you like me to explain what "Bytecode" actually looks like? I can show you the difference between Python source code and its bytecode.
那编译型语言就是多一个编译的步骤,实际跑起来现在已经和解释型语言已经没有很大区别了吗?我有点糊涂了。
这是一个非常棒的思考点!你并没有糊涂,而是触碰到了现代计算机科学中“性能 vs 灵活性”的最前沿地带。
简单回答是:区别依然很大。虽然现代技术(JIT)让解释型语言变快了,但它们背负的“负重”和编译型语言完全不同。
我们可以用“赛车”来打个比方,帮你彻底理清这个区别。
这是两者最大的区别,也是为什么 C/C++ 依然是性能之王的原因。
编译时:在比赛开始前(发布软件前),编译器已经把所有能做的准备都做完了。
运行时:生成的 .exe 文件里,全是精简后的机器指令,没有任何多余的东西。
状态:CPU 拿到指令直接跑,轻装上阵。
即使有 JIT(即时编译)把代码变成了机器码,但它们必须背着一个沉重的“背包”才能跑。这个背包叫 Runtime (运行时环境)。
这个“背包”里装了什么?
C++ 程序:程序员自己管内存,用完自己扔。
Java/JS 程序:有一个自动清扫机跟在后面跑,一边跑一边问:“这个变量还要吗?不要我扔了啊。”这会消耗大量的 CPU 和内存。
结论: 哪怕 JIT 生成的机器码质量和 C++ 一样好(实际上通常做不到),因为背着“垃圾回收”和“即时编译”这两个大包袱,它永远很难跑赢“空手”的 C++。
编译器的优化能力,取决于它有多少时间去“思考”。
场景:你发布游戏前,在服务器上编译。
时间:编译器有无限的时间。
它可以花 1 个小时去分析你的代码,尝试 100 种优化方案,最后选出一种最完美的指令排列方式。它能看穿整个程序的逻辑,进行极其激进的全局优化。
场景:用户点开了网页。
时间:只有几毫秒。
JIT 编译器必须在用户察觉不到卡顿的瞬间完成编译。它不可能花时间去推导复杂的数学公式来优化你的代码。它只能做一些“短平快”的简单优化。
结论: C++ 的编译器是国际象棋大师(思考很久走一步);JIT 是快棋手(必须马上走棋,不能想太久)。大师的棋步质量通常高于快棋手。
编译型:
启动:极快。操作系统加载完 .exe 就直接开始执行指令。
性能曲线:一条直线,从头到尾都很稳。
解释型/JIT:
启动:慢。先要启动虚拟机,初始化垃圾回收器,加载字节码...
性能曲线:“热身”曲线。刚开始是解释执行(慢),跑了一会儿 JIT 介入了(变快),过了一会儿垃圾回收触发了(突然卡顿一下)。这就是为什么安卓手机有时候会莫名“掉帧”,而 iOS(主要用 Objective-C/Swift 等 AOT 语言)相对顺滑的原因之一。
你之所以会产生“区别不大”的错觉,是因为硬件太强了。
对于 90% 的应用场景(写个 Web 后端、写个脚本、做个简单的 App):
CPU 性能过剩。
你根本不在乎是 10 毫秒跑完还是 1 毫秒跑完,反正人类感觉都是“一瞬间”。
在这些领域,开发效率(Python 写得快)比运行效率(C++ 跑得快)更重要。
但是,一旦到了以下领域,区别就暴露无遗:
3A 游戏引擎 (Unreal Engine):必须用 C++。每一帧只有 16 毫秒,垃圾回收器卡顿 1 毫秒都是不可接受的。
操作系统内核 (Windows/Linux):必须用 C/Rust。不能带“背包”。
高频交易:必须用 C++。纳秒级的竞争,解释器的开销是致命的。
编译型 (C++/Rust) = F1 赛车。
极速、极致轻量、难驾驶(写代码难)。
没有“中间层”。
现代解释型 (Java/JS/Go) = 高性能轿跑。
很快,有自动挡(垃圾回收),好开(写代码容易)。
虽然加了涡轮(JIT),但它依然是一辆民用车,很难跑赢 F1。
所以,并没有“降维打击”成一回事,而是解释型语言在努力逼近编译型语言的车尾灯,但那个“背包”(Runtime)决定了它们很难真正追平。
Would you like me to explain what "Garbage Collection" (GC) is in detail? It is the heaviest item in the "backpack" of modern languages.
jit是什么?
JIT 是 Just-In-Time Compilation(即时编译)的缩写。
它是现代编程语言(如 Java、JavaScript、C#、Python 的 PyPy)为了兼顾“解释型语言的灵活性”和“编译型语言的高性能”而发明的一种“混合动力”技术。
用一句话总结:JIT 就是“带了记性”的解释器。
为了让你秒懂,我们继续用翻译做比喻:
场景: 也就是如果你的代码里有一个循环
for (1 to 1000),他就会把循环里的句子笨笨地翻译 1000 次。
JIT 编译器: 是一个聪明的翻译官。他一开始也是一句句翻译(解释执行)。但他手里有个小本本。
暗中观察:他发现第 10 页的第 5 句话被读者反复读了 100 遍(这叫热点代码 Hot Spot)。
即时编译:他想:“这一段太常用了,我别每次都翻了。” 于是,他趁读者不注意,把这一段直接翻译成完美的中文(机器码),写在小纸条上贴在书里。
直接使用:第 101 次读到这里时,他直接指着小纸条让你看(直接运行机器码),速度瞬间提升 50 倍!
JIT 不是一上来就工作的,它非常狡猾,通常遵循以下步骤:
程序刚启动时,JIT 不工作。由解释器老老实实地一行行解释执行。
在程序运行时,JIT 引擎会在后台启动一个监控器(Profiler)。它会给每个函数或循环装一个“计数器”。
“这个函数被调用了 10 次……没关系。”
“这个函数被调用了 10,000 次!警告!这是热点(Hot Spot)!”
一旦发现热点,JIT 编译器就会介入。它会把这段“热点代码”抓出来,在后台把它编译成高度优化的机器码(Native Code)。
下次程序再运行到这里时,JIT 引擎会把“解释器”推开,直接让 CPU 跑刚才生成的机器码。
这是 JIT 最神奇、也最难的地方。
像 JavaScript 或 Python 这种动态语言,变量类型是不固定的。
JavaScript
function add(a, b) {
return a + b;
}
JIT 观察发现,你过去 1000 次调用的 add 都是传的整数 (1 + 2)。
大胆假设:JIT 猜测“你以后肯定也传整数”,于是它把代码编译成了“整数加法机器指令”(非常快)。
突发情况:第 1001 次,你突然传了个 字符串 ("Hello" + "World")。
去优化:刚才生成的整数加法指令跑不通了!JIT 会立刻“甚至”(Bailout),把优化好的机器码扔掉,退化回原始的解释器模式,重新慢吞吞地执行。
这就是为什么它是“上下文相关”的极致体现:它不仅看代码,还看你运行时传了什么数据。
优点:
快:对于长时间运行的程序(如 Web服务器、大型游戏),性能可以逼近 C++。
启动快:不像 C++ 需要全部编译完才能跑,JIT 程序可以边跑边编译,启动速度接近解释型语言。
动态优化:它能根据当前的 CPU 型号(是 Intel 还是 AMD)生成最适合当前硬件的代码,而静态编译的 C++ 往往只能选一个通用的。
缺点:
内存占用大:它需要内存来存源代码、字节码,还需要内存来存编译后的机器码(Code Cache)。
运行时开销:编译代码这个动作本身是占 CPU 的。程序刚启动时,可能会因为 JIT 在疯狂工作而出现短暂的卡顿(Warm-up phase)。
不可预测性:有时候因为触发了“去优化”,性能会突然掉下来。
V8: Chrome 浏览器和 Node.js 用,让 JavaScript 跑得飞快。
HotSpot: Java 虚拟机(JVM)默认引擎。
PyPy: Python 的 JIT 实现(比官方 Python 快 5-10 倍)。
Would you like me to explain why, despite JIT, languages like Python (standard version) are still considered "slow" compared to C++?
那编译器也需要不断优化?
这是一个非常深刻的问题。答案是:是的,编译器不仅需要“被优化”,而且这场优化的战争永远不会结束。
编译器工程师(Compiler Engineers)是计算机领域里薪资最高、最稀缺的人才群体之一,因为他们的工作永远做不完。
为什么编译器不能“写一次就完事”?为什么要不断更新优化?主要有以下三个核心原因:
编译器是软件和硬件之间的翻译官。虽然 C 语言的代码没变,但底下的 CPU 一直在变。
新指令集: Intel 发布了新的 CPU,支持了 AVX-512 指令(能一次性处理更多数据)。
旧编译器:不知道这个新指令,只会用老土办法一个个算。
新编译器:必须升级,学会用这个新指令。一旦学会,原来的老代码重新编译一下,性能立马提升 20%。
架构变更: 比如苹果从 Intel 芯片换成了 M1/M2/M3 (ARM架构)。
这对于编译器后端来说,简直是“换了个人种”。编译器必须针对 M1 芯片的特性(比如超大的指令缓存、统一内存架构)进行专门的重新设计和调优,才能榨干这块芯片的性能。
你可能会想:“能不能写一个完美的编译器,总是能生成世界上最快的机器码?”
数学告诉我们:不可能。
在计算机科学中,代码优化被证明是一个 “NP完全问题” (NP-Complete) 或 “不可判定问题”。
通俗解释:对于任何一段稍微复杂点的代码,想要找到“绝对最快”的指令排列方式,需要的计算时间可能是宇宙的寿命。
现实做法:编译器工程师只能设计各种“启发式算法” (Heuristics) —— 也就是“经验法则”或“聪明的猜想”。
比如: “我觉得把这个循环展开 4 次可能比展开 8 次要好,但不一定。”
工程师们每一代都在寻找更准、更好的“猜想”,试图无限逼近那个理论上的“完美”,但永远到不了。
优化不是免费的。编译器“思考”得越久,生成的代码通常越快,但你等待编译的时间就越长。
这也是为什么编译器会提供 “优化等级” (Optimization Levels) 给用户选:
-O0 (不优化):
编译器状态:完全不思考,直译。
特点:编译极快(点一下就跑),方便调试(代码行号能对上),但跑得慢。
-O2 (标准优化):
编译器状态:常规思考,做一些稳赚不赔的优化。
特点:工业界的默认标准,速度和体积的平衡。
-O3 (激进优化):
编译器状态:绞尽脑汁,尝试各种激进手段(比如把大函数拆散、把循环拆得面目全非)。
特点:编译很慢,生成的程序可能跑得飞快,但程序体积会变大(代码膨胀),甚至极少数情况下会把逻辑搞错(Bug)。
既然人类工程师找不到“完美解”,现在大家开始用 AI 来做编译器优化了。
以前:人类工程师写死规则 —— “如果循环少于 10 次,就展开它”。
现在 (Machine Learning in Compilers): Google 和 Facebook 正在训练 AI 模型。 让 AI 看几十亿行代码,然后告诉编译器:“嘿,根据我的经验,遇到这种形状的代码,你把这三行挪到那一行后面,运行速度能快 5%。”
这也是为什么像 LLVM 和 GCC 这样的编译器项目,至今仍然活跃得像刚诞生一样,每天都有成千上万行代码的更新。
总结:编译器不是一个静态的工具,它是一个有生命的、不断进化的“超级大脑”,它的智慧程度直接决定了全世界软件的运行效率。
Would you like me to explain what "Loop Unrolling" (a classic optimization technique) looks like with a simple code example?
jit开发难度会比普通的编译器更难吗?
这是一个毫无疑问的答案:是的,开发 JIT 编译器的难度要远高于普通的 AOT(提前编译)编译器。
如果说写一个普通的 C 语言编译器是“造房子”,那么开发一个高性能的 JIT(如 V8 或 JVM 的 HotSpot)就是在“边开飞机边换引擎”。
JIT 开发之所以难,是因为它不仅要懂所有的编译理论,还面临着三个地狱级的限制条件:
这是最直接的区别。
普通编译器 (AOT):
心态:如果你编译一个大型 C++ 项目需要 10 分钟,程序员虽然会骂,但也能接受。
算法选择:你可以使用极其复杂的算法(比如 O(n 2) 甚至 O(n 3) 的复杂度)来计算最优解。你可以把整个程序看作一个整体(全程序优化 LTO),反复推敲。
JIT 编译器:
心态:用户在点网页,你的编译器如果敢卡顿超过 50毫秒,用户就会觉得“这网页好卡”然后关掉。
算法限制:你不能用太好的优化算法,因为太慢了!
矛盾:你必须生成高质量的机器码(为了跑得快),但你又必须用极快的速度生成(为了不卡顿)。这需要在算法上做极其精妙的平衡(Trade-off)。
这是 JIT 开发中最劝退的新手村 Boss。
想象一下:
用户的一个循环正在解释器里跑,已经跑到了第 5000 次(在一个 while 循环中间)。
JIT 觉得:不行,这太慢了,我要把它编译成机器码。
难点来了:代码正在运行中!你必须在不停止程序、不重置变量的情况下,把当前的“解释器状态”无缝切换到“机器码状态”,然后让 CPU 接着第 5001 次继续跑。
这意味着你需要:
把解释器内存里的变量,精确地搬运到 CPU 的物理寄存器里。
把解释器的虚拟栈帧,重构成机器码的物理栈帧。
只要错一个字节,程序直接崩溃(SegFault)。
这就像是在心脏跳动的时候做心脏移植手术,难度可想而知。
普通编译器只需要保证“正确”,而 JIT 必须学会“赌博”。
AOT:看到 a + b,它必须考虑所有情况(溢出怎么办?类型不对怎么办?)。生成的代码非常保守、臃肿。
JIT:
观察:我看刚才 1000 次这里都是整数相加。
下注 (Speculate):我就赌它下次还是整数!我生成一条极其精简的整数加法指令,把那些复杂的检查全删了。
埋坑:但是,万一赌输了怎么办?(比如第 1001 次来了个浮点数)。JIT 必须在机器码里埋一个“陷阱”。
回滚 (De-optimization):一旦踩中陷阱,程序必须瞬间从机器码的世界“穿越”回解释器的世界,恢复刚才的状态,用慢速模式继续跑。
难点:实现“从优化的机器码”安全地“退化回解释器”,还需要保证数据完全一致,这个工程量和逻辑复杂度是极高的。
AOT:如果代码编译错了,每次跑都在同一个地方错。你可以慢慢调试。
JIT:
Bug 出现是因为:用户先点了A,再点了B,导致 JIT 触发了第 3 层优化,刚好此时内存不够触发了 GC,GC 移动了对象地址,导致 JIT 生成的机器码访问了错误的地址……
Heisenbug:你一想调试(加个断点),程序运行变慢了,JIT 的触发时机变了,Bug 就消失了!
JIT 工程师日常面对的是无法复现的崩溃,需要极强的底层功底。
维度 普通编译器 (AOT)JIT 编译器
可见范围只有源代码 源代码 + 运行时数据 (Runtime Profile)
编译时机开发者睡觉时 用户使用时
内存限制随便用,编完就释放 必须极其节省,和应用抢内存
核心难点复杂的数学算法优化状态迁移 (OSR)、去优化 (De-opt)、并发编译
形象比喻****建筑师 (画好图纸慢慢盖)特种兵 (战场上根据情况实时应变)
Export to Sheets
正因为如此,世界上成熟的商用 JIT 引擎屈指可数(V8, SpiderMonkey, JavaScriptCore, HotSpot, .NET CLR)。能手写 JIT 引擎的工程师,通常是各大科技巨头争抢的顶级架构师。
编译器实际由谁在开发
开发编译器(尤其是现代、高性能的编译器)是一项极度烧钱、极度消耗智力资源的“重工业”。
如今,主流编译器的开发早已不是“单打独斗”的时代,而是形成了由科技巨头、芯片厂商、学术界和开源社区共同编织的庞大网络。
我们可以把他们分为以下几个梯队:
动机:掌握生态控制权。 如果我控制了语言和编译器,我就控制了开发者,也就控制了软件生态的未来。
V8 引擎 (JavaScript):Google 为了让 Chrome 浏览器跑得快,组建了一支世界顶级的编译器团队(位于丹麦和慕尼黑等地)。
Go 语言编译器:为了解决 Google 内部大规模并发编程的问题。
LLVM 贡献:Google 是 LLVM(现代编译器基石)最大的贡献者之一,为了安卓系统和数据中心。
LLVM & Clang:这原本是个大学项目,被乔布斯慧眼识珠,把作者 Chris Lattner 挖过去。苹果全资资助了这个项目,目的是摆脱对 GCC 的依赖。现在你用的 iPhone、Mac 上跑的所有软件,基本都是这套东西编译出来的。
Swift:在 LLVM 基础上自研的语言,为了巩固 iOS 生态。
Roslyn (C#):微软把 C# 编译器彻底重写了一遍,做成了开源且模块化的。
TypeScript:由编程大神 Anders Hejlsberg(也是 C# 和 Delphi 之父)亲自带队开发。
MSVC:Visual Studio 自带的 C++ 编译器,历史悠久,Windows 软件的基石。
动机:卖芯片。 如果软件在我的芯片上跑不快,没人会买我的芯片。所以他们必须开发极强的编译器后端 (Backend)。
动机:探索理论边界。 很多革命性的编译器技术诞生于大学实验室。
UIUC (伊利诺伊大学厄巴纳-香槟分校):LLVM 的发源地。Chris Lattner 当年只是那里的一个硕士/博士生,他的导师 Vikram Adve 现在依然是编译器学术界的大佬。
Stanford & Berkeley:很多关于 AI 编译器、形式化验证的研究源自这里。
特性:学术界通常产出原型 (Prototype) 或论文,然后被工业界(第一梯队)拿去工业化。
动机:自由与协作。
GNU 社区 (GCC):
一群信仰自由软件的黑客。虽然现在很多大公司的员工也在给 GCC 贡献代码,但它依然保持着独特的“草根”和“反商业垄断”气质。
Linux 内核主要是用 GCC 编译的。
Rust Foundation:
Rust 语言虽然起源于 Mozilla,但现在由一个独立的基金会管理,AWS、Google、华为、微软都在里面出钱出人。
开发编译器的人通常被称为 Compiler Engineer (编译器工程师)。他们是程序员中的特种兵。
技能树:
精通计算机体系结构(懂 CPU 流水线、缓存、指令集)。
精通离散数学与图论(懂控制流图、图着色、格理论)。
精通汇编语言(x86, ARM, RISC-V)。
薪资:
非常高。在硅谷或国内大厂,编译器专家的薪资通常比普通应用层开发(写网页、写App)高出一个段位。
因为人才太稀缺了。全球能手写高性能 JIT 引擎的人,可能只有几百人到几千人。
现代编译器的开发模式,看 LLVM 就懂了:
发起:Chris Lattner 在 UIUC 大学读博时发起的学术项目。
资助:Apple 把他雇了,投入资金把它变成工业级产品,用来替换 GCC。
壮大:Google、Sony、Intel 发现这东西好用,纷纷加入。
Google 贡献了 sanitizer(内存检测工具)。
Sony 用它来编译 PS4/PS5 游戏。
Intel 帮它优化 CPU 指令生成。
所以,“谁在开发编译器?” 答案是:拿着大厂高薪的顶尖数学家兼计算机科学家,在开源社区的旗帜下协同工作。
Would you like me to tell the legendary story of Chris Lattner? He is basically the "Steve Jobs" of the modern compiler world.
中国没什么人做吧?
这是一个非常普遍的误解。
实事求是地说:在 10 年前,你这句话是对的。 那时候国内确实基本都在“用”编译器,很少有人“造”或“改”编译器。
但现在的各种情况是:中国目前是全球编译器开发最活跃、投入最大的地区之一。
为什么?因为“被逼出来了”,也因为“新赛道来了”。
我们可以把国内做编译器的势力分为四大阵营:
华为是目前国内拥有编译器专家最多、技术最硬核的公司,没有之一。他们是被美国的制裁逼出来的。
毕昇编译器 (Bisheng Compiler):
华为自研了鲲鹏芯片(ARM架构)和昇腾芯片(AI芯片)。
如果没有编译器支持,这些芯片就是废铁。华为必须基于 LLVM 深度定制,开发能把 C/C++ 代码高效翻译成鲲鹏指令集的编译器。
方舟编译器 / ArkTS:
为了鸿蒙系统 (HarmonyOS)。鸿蒙要把 Java/JS 的性能跑出花来,必须魔改编译器。他们搞了静态编译技术,让 Java 代码可以直接编译成机器码,不用虚拟机动态解释,解决了安卓的卡顿问题。
规模:华为内部有数千人的团队在死磕编译器、操作系统和数据库等底层软件。
这几家公司拥有海量的服务器。如果编译器能优化 1% 的性能,对于拥有百万台服务器的大厂来说,每年省下的电费和硬件采购费就是几个亿。
阿里巴巴:
Dragonwell (龙井):阿里是全球最大的 Java 用户之一。他们深度定制了 OpenJDK,搞出了自己的 JDK 版本。他们对 JVM(Java虚拟机)里的 JIT 编译器做了极深的优化,用来支撑双11那种恐怖的流量。
RISC-V:达摩院在推玄铁芯片,这需要全套的编译器工具链支持。
字节跳动 (ByteDance):
他们对 Go 语言编译器和 V8 引擎(JS)投入极大。因为抖音/TikTok 的后台大量使用 Go,前端大量使用 JS。优化编译器就是直接省钱。
腾讯:
Konajdk(腾讯版JDK),以及在游戏领域对 C++ 编译器的优化。
这是目前最缺人的地方。也就是所谓的 AI 编译器。
背景:现在的 AI 模型(比如 Llama, GPT)是基于 PyTorch/TensorFlow 写的。但底下的芯片五花八门(华为昇腾、寒武纪、地平线、摩尔线程、壁仞)。
痛点:怎么把 PyTorch 的代码,翻译给这些国产芯片听?
现状:每一家国产芯片公司,必须养一大帮写编译器的人。做不好编译器,芯片性能再强也发挥不出来(这叫“算力利用率低”)。
代表人物/项目:陈天奇(机器学习编译领域的超级大神,TVM 的作者,虽然在卡内基梅隆/OctoML,但影响了大批国内开发者)。国内的中科院计算所、清华在 AI 编译领域非常强势。
这里必须提到一个名字:吴伟 (Wei Wu) 和他的 PLCT 实验室(中科院软件所智能软件研究中心)。
目标:他们是国内极少见的、成建制地死磕开源编译器工具链的“国家队”。
RISC-V:中国押注 RISC-V 架构作为未来的芯片标准。PLCT 实验室正在疯狂给 GCC 和 LLVM 贡献代码,确保 RISC-V 的编译器支持达到一流水平。他们的口号是要培养 10000 名核心编译器开发者。
门槛太高,圈子太小: 国内能手写 JIT 引擎的人,确实可能相互都认识。这个圈子不像“前端开发”或“Java开发”那么大,它属于金字塔尖。
不直接面向用户: 你用的是鸿蒙系统,你感觉流畅,但你不知道背后是方舟编译器在干活。你玩《黑神话:悟空》,你只看到画面,不知道背后是着色器编译器在工作。
起步晚,但追得快: 我们在“创造新语言”(像创造 Python, Go, Rust)这方面确实还很少。但在“优化编译器”、“适配国产芯片”这方面,中国已经是世界级玩家了。
你可以去招聘软件搜一下“编译器工程师”或“LLVM 开发”:
薪资:通常是同级别普通程序员的 1.5 倍到 2 倍。
抢人:华为、大厂、芯片独角兽都在疯狂抢人。
所以,中国不是没人做,而是正在疯狂招人做,只是这群人平时都在读 Paper、看汇编,比较低调罢了。