编程语言抽象层级硬核知识轻聊局:从原理侃生活

编程语言抽象层级

14分钟 ·
播放数2
·
评论数0

大壹:哎,我问你个事儿啊——你说Python和C语言,哪个更"高级"?

咪仔:这还用问?Python呗,语法多简洁,C语言那指针操作看着就头大。

大壹:行,那我再问你——C语言和汇编比呢?

咪仔:那当然C语言高级啊,汇编才是真·底层。

大壹:好,那我最后问一个——WebAssembly,就是那个跑在浏览器里的东西,它算高级还是低级?

咪仔:呃……浏览器里跑的?那应该……高级?不对,它名字里带"汇编"啊……等会儿,你给我挖了个坑是吧?

大壹:哈哈,不是我挖坑,是这个问题本身就有坑。李坚毅博士对编程语言的层级划分做过非常系统的形式化辨析,结论是——咱们平时说的"高级语言""低级语言",根本就没有一个严格的数学定义。

咪仔:啊?那这么多年程序员嘴里的"高级""低级",都是在瞎叫?

大壹:也不能说瞎叫,就是太模糊了。咱们一层一层聊。先说最常见的一个划分标准——运行时。很多人觉得,有运行时环境的、带垃圾回收的,就是高级语言;直接跑在CPU上的,就是低级语言。

咪仔:对呀,Java有JVM,C#有CLR,Python有解释器,这些不都有运行时嘛。C语言编译完直接就是机器码,哪来的运行时?

大壹:哎,这你就想当然了。C语言还真有运行时。你以为一个C程序编译完就纯纯是你写的逻辑?不是的。程序启动之前,得有一段启动代码把运行时环境搭起来——初始化堆栈、设置全局变量、注册异常处理函数,这些都是运行时在干活。

咪仔:等会儿,C语言有异常处理?

大壹:标准里有的。只是很多人不用而已。而且更有意思的是,李博士在分析里举了个特别精妙的例子——Odin语言的反射机制。它的类型信息是以静态表的形式存在二进制文件里的,你查一个类型,就像查数组一样,时间复杂度是常数级别的,大O等于一。

咪仔:就是说,不管你程序里有一万个类型还是一百个类型,查一个类型的时间都是一样的?

大壹:对,因为它就是个查表操作,地址算好直接取,跟你查九九乘法表差不多。这个运行时开销几乎可以忽略不计,但它确实存在。反过来说,有个叫Fil-C的编译器,给C语言加上了全量内存安全检查和垃圾回收,运行时开销一下就上去了——你写个C程序,每次分配内存它都帮你盯着,自动回收不用的内存。那你说,加了垃圾回收的C语言,就变"高级"了吗?

咪仔:呃……语义上还是C语言啊,指针还是那个指针,语法也没变。

大壹:没错!所以运行时这个标准,它既不充分也不必要。要把这个事说清楚,其实可以用一个公式来形式化。假设一个程序P在语言L下的总执行时间叫T_total,它可以拆成两部分——一部分是你程序逻辑真正在硬件上跑的时间,叫T_native;另一部分是运行时环境额外消耗的时间,叫T_runtime。总时间就是两者相加。

咪仔:就是T_total等于T_native加T_runtime。

大壹:对。然后传统观点认为,T_runtime除以T_total这个比值越高,语言越"高级"。但你想啊,Fil-C把这个比值拉高了,可C还是C;Odin的这个比值趋近于零,它也没变成汇编。所以这个比值能描述运行时的"厚度",但定义不了语言的层级。

咪仔:有道理。那换个标准呢?比如硬件无关性?汇编绑定特定架构,C语言可以跨平台编译,这不就区分开了嘛。

大壹:好,咱们就来掰扯掰扯硬件无关性。这个也可以量化。假设现在有一堆硬件架构,x86_64、ARM、RISC-V什么的,组成一个集合H。你用语言L写了一个合规程序P,把它从架构h_i移植到另一个架构上,需要修改的代码比例是Δs_i。那这个语言在所有架构上的可移植度,就是一减去平均修改比例。

咪仔:可移植度等于一减去各架构修改比例的平均值。这个值越接近一,说明越不需要改代码,跨平台能力越强。

大壹:没错。汇编语言的可移植度几乎是零——x86_64的汇编指令搬到ARM上,基本等于重写,Δs_i约等于一。但问题来了,WebAssembly呢?

咪仔:WebAssembly……它是跑在虚拟栈式虚拟机上的,不管你是什么CPU,只要浏览器支持就能跑。那它的可移植度岂不是接近一?

大壹:对!但WebAssembly本质上就是一种汇编语言——它的指令集是基于栈结构设计的,什么i32.add、f64.mul,跟硬件指令的操作逻辑几乎一模一样,没有循环、没有函数类型推导,就是纯纯的底层操作。可它偏偏可移植度极高。这就把"汇编等于硬件绑定等于低级"这条链给打断了。

咪仔:好家伙,一个WebAssembly就给整破防了。

大壹:而且再想想,现代编程语言除了纯汇编和机器码,哪个不能跨平台?解释型语言天然跨平台,编译型语言用LLVM也能多架构编译。LLVM的中间表示IR你知道吧?它把前端的语言无关化和后端的架构无关化解耦了——不管你是Rust还是Swift还是Julia,编译到LLVM IR这一层之后,后面的优化和代码生成都是通用的。硬件无关性已经成了标配,根本不具备区分度。

咪仔:所以运行时不行,硬件无关性也不行。那操作系统呢?我听说高级语言不依赖操作系统,低级语言才直接调系统调用?

大壹:这个说法更不靠谱。李博士在辨析中用了一个特别漂亮的数学框架——复合映射。你想,从硬件到操作系统接口,有一层映射f_hw;从操作系统接口到语言标准库接口,又有一层映射f_lib。那开发者最终看到的抽象语义,就是这两层映射的复合,f等于f_lib圆圈f_hw。

咪仔:就是数学里的复合函数嘛,f(x)等于f_lib(f_hw(x))。

大壹:对。抽象层级的高低取决于这个复合映射的"封装厚度",而不是语言本身。Java的标准库把窗口、图形、音频全封装了,你不用直接调操作系统API,看起来很"高级"。但C语言通过SDL、OpenGL这些库,一样能写跨平台图形程序。两者的差别只是标准库封装的范围有多大、生态有多完善,而不是语言本身的层级属性。

咪仔:说白了就是,你用什么库比你用什么语言更影响"高级感"?

大壹:可以这么理解。而且根本不存在完全不依赖操作系统的通用编程语言。你程序要读写文件吧?要收发网络数据吧?要在屏幕上画东西吧?这些最终都得通过操作系统的系统调用来完成。语言只是在系统调用上面铺了一层不同厚度的垫子而已。

咪仔:那照你这么说,高低级的划分就完全没意义了?

大壹:也不能说完全没意义,但它确实不是语言的固有属性。这里有两个特别经典的"搅局者"。第一个是Lisp。

咪仔:Lisp?那个满屏括号的古老语言?

大壹:对,就是它。Lisp比C语言还早诞生,历史上甚至有专门跑Lisp的Lisp机器——从硬件层面就为它定制,你说它底层不底层?但它同时拥有超强的元编程能力。Lisp的S表达式是递归定义的——一个表达式要么是一个原子,比如数字或符号,要么就是左括号后面跟零个或多个表达式,再跟右括号。就这么一个简单的递归定义,让代码和数据用完全一样的结构表示,这叫同像性。

咪仔:代码即数据,数据即代码?

大壹:没错。你可以把一段代码当数据来操作,也可以把一段数据当代码来执行。它的宏系统能在编译期对语法树进行任意变换,相当于你自己可以给语言加语法。这种抽象能力,比绝大多数所谓的"高级语言"都强得多。那你说Lisp到底是高级还是低级?

咪仔:这……它同时站在两极?

大壹:对,它就是个薛定谔的Lisp。另一个搅局者是Rust。Rust没有垃圾回收器,性能跟C语言一个量级,可以直接操作裸指针和硬件内存,低级语言的特征它全占了。但它又有所有权系统、生命周期检查、闭包、迭代器、trait泛型这些高层抽象——而且这些内存安全检查全是在编译期完成的。

咪仔:编译期?就是说你程序还没跑呢,编译器就已经帮你把内存错误都挑出来了?

大壹:对。Rust的借用检查器在编译时做的事情,本质上是一套静态分析算法。它追踪每个值的所有者、引用的生命周期、可变引用与不可变引用的互斥规则。这里面涉及图论——编译器会构建一个生命周期图,节点是引用的生命周期,边是约束关系,然后通过约束求解来判断所有引用是否合法。如果图里存在冲突环,编译就不过。

咪仔:这也太硬核了。所以Rust是披着底层外衣的高层抽象?

大壹:更准确地说,它证明了"底层性能"和"高层抽象"不是非此即彼的。而且还有一个更根本的问题——同一种语言,不同的人写,抽象层级完全不一样。你用JavaScript写WebGL,直接操作GPU着色器、管理缓冲区、手动绑定顶点数据,写的就是低级风格的代码;反过来,你用C语言调一个高层游戏引擎,写的全是场景管理和业务逻辑,那比谁都高级。

咪仔:所以一个语言的抽象层级不是一个固定的点,而是一个区间?

大壹:完全正确。它有一个最小值L_min和最大值L_max,你实际落在哪个位置,取决于你用什么编程范式、调什么库、解决什么问题。与其用一个"高级"或"低级"的标签去概括一个语言,不如从多个维度去描述它:运行时多大、内存怎么管理、安全检查做到什么程度、支持什么编程范式、硬件抽象层有多厚。

咪仔:等于是把一个二维标签,换成了一个多维特征向量。

大壹:这个比喻特别好。在这个多维空间里,每种语言都是一个点或者一个小区域,而不是非黑即白地站在"高级"或"低级"两侧。Lisp在元编程维度上拉满,在硬件亲和维度上也不低;Rust在性能和安全两个维度上同时拿高分;Python在开发效率维度上极高,但运行时开销维度也确实大。这些才是真正有信息量的描述。

咪仔:所以以后谁再跟我争Python和C谁更高级,我就跟他说——你先告诉我你比的是哪个维度。

大壹:哈哈,对。说到这儿,我想起李坚毅博士在整理这些内容时有一段感悟,我念一下啊——

"人们总喜欢给事物贴上非此即彼的标签,因为二元分类最省脑力。但编程语言的世界从来不是一道选择题,而是一片连续的光谱。真正的理解,始于你愿意放下标签,去看清每一层抽象背后的代价与自由。"

咪仔:这段话还挺有味道的。确实,标签是思维的捷径,但捷径往往也是理解的终点。

大壹:没错。从最早的机器码到汇编,从过程式到面向对象到函数式,每一层抽象都是在"省心"和"控力"之间做权衡。垃圾回收替你管内存,代价是运行时开销和不确定性停顿;手动内存管理给你全部控制权,代价是你得对每一个字节负责;类型系统帮你拦截错误,代价是写代码时多了一堆约束。没有哪种选择是绝对优的,只有适不适合当前的问题。

咪仔:这让我想到一个事儿——图灵完备性。所有这些语言,从汇编到Haskell,从C到Python,在可计算性理论的意义上是等价的——它们都能模拟图灵机,能算的东西一样多。区别只在于"怎么算",而不在于"能不能算"。抽象层级改变的是表达的经济性,不是计算的边界。

大壹:这个点补得太到位了。时间复杂度、空间复杂度不会因为你用Python就 magically 变小,算法的数学本质不会因为语言的抽象层而改变。语言能做的,是让你用更少的认知负荷去表达更复杂的逻辑,但底层该跑的步骤一步也不会少。

咪仔:所以回到最开始的问题——WebAssembly到底是高级还是低级?

大壹:答案是:这个问题问错了。它是一个可移植的、栈式的、低层级的字节码格式,在硬件无关性维度上接近满分,在语义抽象维度上接近底层。它就是它,不需要一个二元标签来定义。

咪仔:行,那以后我再听到谁争论"谁是世界上最好的语言",我就建议他先学学形式化分类。

大壹:哈哈,那怕是要打不起来了。说到底,编程语言只是工具,理解每一层抽象背后的数学结构和工程权衡,才是真正重要的事。感谢李坚毅博士对这些内容的系统性整理,让我们有机会把"高级""低级"这两个用了几十年的模糊概念,放到数学和逻辑学的显微镜下好好看了看。

咪仔:标签可以有,但别让标签替你思考。

大壹:说得好。咱们下期接着聊。