【第 002 讲】从机器语言到 Python:编程语言的演进 | 程序的执行机制 计算机的工作由程序驱动而程序需要用编程语言来编写。从最初直接书写由 0 和 1 组成的指令到用符号代替二进制编码再到用接近自然语言的方式描述任务编程语言先后经历了三个发展阶段编写程序也随之变得越来越容易。沿着这条演进脉络本篇将依次讲解机器语言、汇编语言与高级语言这三代编程语言的核心特征梳理演进的内在逻辑并说明程序从代码到执行的完整过程为后续正式编写 Python 程序打好基础。1 编程语言的演进1.1 机器语言计算机硬件能够直接识别和执行的只有由 0 和 1 组成的二进制指令序列。为什么偏偏是 0 和 1这与硬件的构成方式有关计算机硬件由数字电路构成对电路而言最容易稳定区分的是低电平与高电平两种状态。人们约定用 0 表示低电平、用 1 表示高电平0 和 1 便与这两种状态一一对应电路无须分辨更多状态就能可靠工作。指令以二进制编码的形式存放在存储器中CPU 取出指令后将其译码再由内部的数字电路完成相应操作。这种由二进制序列写成的指令集合称为机器语言Machine Language它是计算机历史上的第一代编程语言。机器指令通常由两部分构成操作码与地址码。操作码指明要执行的操作类型如加法、传送等地址码给出要处理的数据或指明数据存放的位置。一条指令的具体长度即二进制位数与编码格式由所用的 CPU 架构决定。下面给出一段机器语言程序其任务是计算 3 与 5 的和并把结果保存到内存中。示例中的每条指令写为 16 位并按 8 位一组分成两段——分段与空格只是为了便于阅读真实的指令在机器中是一串连续的 0 和 110100000 00000011 10100001 00000101 10110000 00000001 11000000 00010000这四条指令对应的操作依次是把数值 3 载入寄存器 A。把数值 5 载入寄存器 B。把寄存器 B 的值加到寄存器 A 上。把寄存器 A 的值存入内存地址 16。四条指令依次执行完毕后寄存器 A 与内存地址 16 中保存的都是 3 与 5 的和即 8。这些指令都提到了寄存器。寄存器Register是 CPU 内部用来临时存放数据的存储单元存取速度远高于内存CPU 的运算主要围绕它进行。需要说明的是上述编码是为讲解而设计的示意写法并非某款真实处理器的实际指令编码但它足以反映机器语言的基本面貌。作为第一代编程语言机器语言具有以下核心特征执行效率极高机器指令无须任何转换环节可直接被 CPU 执行。硬件依赖严重机器语言深度绑定特定处理器的指令集架构Instruction Set Architecture简称 ISA。不同架构如 x86 与 ARM的指令编码规则互不兼容为一种架构编写的机器语言程序无法直接在另一种架构的机器上运行。开发成本高昂开发者需要记住每条机器指令的二进制编码及其含义编写与阅读都极易出错错误也难以排查即便只编写一个很短的程序也要耗费大量时间。1.2 汇编语言机器语言的根本困难在于人类必须以二进制形式直接操控硬件。要在程序中完成一次载入操作就得写出 10100000 00000011 这样的序列这要求开发者熟记指令的编码方式认知负担相当沉重。为了摆脱这一困境人们改用便于理解的符号来书写指令由此诞生了第二代编程语言——汇编语言Assembly Language。汇编语言最核心的变化是引入助记符Mnemonic用 LOAD、ADD、STORE 这样的英文缩写表示载入、加法、存入等操作。不仅如此寄存器与内存地址也用 A、B、16 这样的符号和数字表示数据可以直接写成十进制开发者不必再书写 0 和 1 的长串序列。由于采用符号化的表达方式汇编语言也常被称为符号语言Symbolic Language。把前面示例中的四条机器指令转换为对应的汇编代码语义结构会清晰许多LOAD A, 3 LOAD B, 5 ADD A, B STORE A, 16与前面的二进制序列相比汇编代码的可读性明显提升。当然不同架构的汇编语法各有差异此处写法仅作示意。然而CPU 只能识别二进制形式的指令无法直接执行 LOAD 这样的符号。因此需要借助一个专门的翻译程序——汇编器Assembler把汇编代码逐条翻译为对应的机器指令CPU 执行的正是翻译后的二进制指令。尽管可读性有所改善汇编语言与机器语言仍同属低级语言Low-level Language一条汇编指令通常只对应一条机器指令开发者仍然需要直接面对寄存器、内存地址等硬件层面的概念。汇编语言的核心特征可以概括为以下三点底层控制力强汇编语言允许开发者精确控制 CPU 寄存器与内存地址能够生成十分精简的二进制指令。在对性能和资源占用有极高要求的场景如操作系统内核、设备驱动程序、嵌入式实时系统中汇编语言至今仍在使用。平台依赖明显汇编指令与特定的指令集架构强绑定为 x86 架构编写的汇编程序无法直接在 ARM 架构上运行代码复用率通常较低。开发门槛偏高编写汇编程序要求开发者对目标计算机的底层硬件有较深入的了解否则难以写出正确的程序。1.3 高级语言汇编语言解决了可读性问题却没有解决硬件依赖问题。用汇编语言编写程序仍需关注寄存器如何分配、数据存放在哪个内存地址——这些细节与要解决的实际问题如计算一个公式、处理一批数据并无直接关系。为了解决这一问题人们创造出了第三代编程语言即高级语言High-Level Language。这类语言对底层硬件细节的抽象程度远高于前两代开发者无须再直接操控寄存器与内存地址能够把精力集中于待解决的任务本身。高级语言提供贴近人类自然语言与数学表达的语法结构。你只需描述 “做什么”至于 “怎么做”则交给相应的翻译程序完成承担这一工作的主要是编译器与解释器它们的工作机制将在下文详细讲解。假设要计算 3 与 5 的和并把结果显示到屏幕上。这一任务若交给汇编语言前面示例中的程序只能完成求和与保存的部分要把结果显示出来还得把数值转换成一串字符再调用操作系统的输出功能。而在 Python 中同样的任务只需两行代码total 3 5 print(total)即使从未接触过 Python你也不难猜出这两行代码的意图第一行计算 3 与 5 的和并把结果存入 total第二行把 total 的值输出到屏幕。至于数据在底层如何存放与传递、屏幕上的字符如何生成Python 都会自动安排妥当你只需关注 “求两数之和并输出” 这一任务本身。这种高度抽象正是高级语言大幅提升软件开发效率的重要原因。这两行代码涉及的语法知识此处暂不展开后续文章将详细讲解。高级语言的另一项关键优势是跨平台能力用高级语言编写的程序通常只需为不同平台选用相应的编译器或解释器即可在多种硬件架构上运行这与机器语言、汇编语言 “一种架构一套代码” 的局面截然不同。高级语言种类繁多下表列出了业界具有代表性的语言及其核心特征与应用领域语言名称核心特征与应用领域Python语法简洁清晰贴近自然语言在数据分析、人工智能与自动化脚本领域应用广泛。C通用过程式编程语言以高效与灵活著称广泛应用于操作系统内核、嵌入式系统及底层驱动开发。C在 C 语言基础上扩展了面向对象与泛型编程特性常用于高性能计算与大型基础软件。Java面向对象的跨平台语言借助 Java 虚拟机实现 “一次编写到处运行”广泛用于企业级后端服务。C#微软推出的多范式语言依托 .NET 平台运行常用于 Windows 应用、Web 服务与游戏开发。JavaScriptWeb 交互的核心脚本语言依托 Node.js 亦可运行于服务端是现代 Web 前后端开发的基础。TypeScriptJavaScript 的超集增加了静态类型系统适用于大型 Web 应用开发。Go语法极简且原生支持高并发常用于云计算基础设施与微服务架构。Rust以内存安全与高性能著称常用于操作系统、命令行工具等系统级程序的开发。此外还有用于苹果平台的 Swift、面向 Android 开发的 Kotlin、专注统计计算的 R、广泛用于服务端开发的 PHP、面向关系型数据库的结构化查询语言Structured Query Language简称 SQL等它们同样应用广泛各有专长。1.4 三代语言的对比与演进逻辑把三代语言放在一起对比各自的特点与演进方向一目了然对比项机器语言汇编语言高级语言表达形式二进制序列助记符与符号贴近自然语言与数学表达的语法可读性极低有所改善高与硬件的绑定程度深度绑定特定架构强绑定特定架构通常与硬件平台无关翻译环节无须翻译CPU 直接执行由汇编器翻译为机器指令由编译器或解释器翻译为机器指令从这张表可以看清演进的内在逻辑每一代语言的出现都是为了解决前一代最突出的问题——汇编语言用助记符化解了机器语言的可读性难题高级语言则把寄存器与内存地址的安排等硬件细节交给了编译器或解释器处理。经过三代演进程序的表达方式越来越接近自然语言与具体硬件的关联越来越弱。不过演进并没有让前两代语言失去作用。直到今天三代语言在计算机系统中依然各司其职任何程序的运行最终都要落实到一条条机器指令由 CPU 执行对性能要求极高的场合仍离不开汇编语言而绝大多数应用软件则由高级语言开发。2 程序的执行机制高级语言把 “怎么做” 交给了相应的翻译程序。但翻译本身大有讲究在什么时机翻译按什么方式翻译不同的选择直接影响程序的执行效率与跨平台能力。在展开分析之前先明确两个基本概念用高级语言写出的程序代码称为源代码Source Code翻译的最终产物——CPU 可以直接执行的二进制指令序列称为机器码Machine Code。源代码无法被 CPU 直接执行必须先翻译成机器码程序才能真正运行。本节将依次分析编译型、解释型与混合型三类执行方式最后回到 Python 本身考察它的底层执行机制。2.1 编译型语言编译型语言Compiled Language以 C、C 为典型代表采用“预先整体翻译”的执行方式翻译机制程序运行之前由编译器将全部源代码一次性翻译为特定平台的机器码并生成独立的可执行文件。技术优势翻译工作在运行前已经完成程序执行时没有任何翻译开销运行效率高。这类语言通常应用于操作系统内核、游戏引擎等对性能要求严苛的领域。技术局限跨平台能力受限。生成的可执行文件深度绑定特定的指令集架构与操作系统若要在不同平台上运行通常需要针对目标平台重新编译。2.2 解释型语言解释型语言Interpreted Language以 JavaScript 与 Python 为典型代表采用“边翻译边执行”的方式翻译机制程序运行时由解释器逐条读取源代码翻译一条、执行一条。技术优势天然具备跨平台特性只要目标平台安装了相应的解释器同一份源代码就可以直接运行程序修改后无须等待编译可以立即执行调试反馈十分及时。技术局限翻译发生在运行期间且每次运行都要重新翻译因此整体运行效率通常低于编译型语言。2.3 编译与解释的对比两种执行方式的差异可以汇总为下表对比项编译型语言解释型语言翻译时机运行前一次性整体翻译运行时逐条翻译并执行运行效率高执行时没有翻译开销通常较低翻译与执行同时进行跨平台能力受限通常需针对目标平台重新编译只需目标平台安装相应的解释器修改源代码后需重新编译方可运行新版本无须重新编译可立即再次运行代表语言C、CJavaScript、Python 提示编译与解释的类比编译型语言好比出版译本一本中文原著想给日本读者看就要先完整翻译出日文版再出版想给韩国读者看就要另外翻译出版韩文版。读者读译本又快又顺畅但每种语言的译本都要单独翻译。解释型语言则好比同声传译译员听一句、译一句台下无论坐着哪个国家的听众只要配上相应的译员同一份原稿都能现场传达。只是听译的速度通常不及阅读译本。2.4 混合执行字节码与虚拟机编译型语言执行效率高解释型语言跨平台灵活二者各有长短。为了兼顾这两种能力许多编程语言把编译与解释结合起来在源代码与机器码之间引入一个独立于硬件架构的中间层——字节码Bytecode。源代码先被翻译为字节码再交由一种称为虚拟机Virtual Machine的专门软件执行。Java 与 Python 是其中最具代表性的两个例子。Java 的执行流程Java 的执行流程分为两步显式编译Java 源代码.java 文件首先由编译器如官方工具 javac编译为字节码文件.class 文件。这一步由开发者主动执行可以清楚感知因此称为 “显式” 编译。虚拟机执行程序运行时位于操作系统之上的Java 虚拟机Java Virtual Machine简称JVM负责加载字节码文件把字节码逐条翻译为当前平台的机器码并执行。字节码不依赖任何具体硬件同一份字节码可以运行在任何安装了 JVM 的平台上这正是 Java “一次编写到处运行” 的实现基础。 扩展即时编译器逐条解释执行字节码难免带来性能开销。为了弥补这一短板现代 JVM 普遍集成了即时编译器Just-In-Time Compiler简称JIT它在运行期间持续监控一旦发现被高频执行的热点代码就将其直接编译为机器码并缓存起来后续调用无须再逐条翻译热点代码因此能以机器码的速度运行。Java 从源代码到执行的完整流程如下图所示Python 的执行流程尽管业界习惯把 Python 称为解释型语言但它的底层执行同样经历了编译过程只是整个过程对开发者高度透明。以 Python 的官方实现 CPython 为例一段 Python 代码的执行分为两步隐式编译解释器接收到源代码后会首先自动将其编译为字节码。这一步无须开发者进行任何操作因此称为 “隐式” 编译。虚拟机执行字节码随后交由Python 虚拟机Python Virtual Machine简称PVM执行。PVM 是 Python 解释器的核心执行引擎负责逐条读取字节码指令并执行相应操作最终完成程序的计算任务。 扩展字节码的磁盘缓存对直接运行的程序而言编译得到的字节码只存在于内存中程序结束便随之释放。而当一个代码文件被程序中的其他文件使用时Python 会把编译得到的字节码缓存到该文件所在目录的__pycache__文件夹中扩展名为.pyc此后再次使用时只要源代码没有改动解释器就直接加载这份缓存省去重新编译的开销。Python 从源代码到执行的完整流程如下图所示两种实现的对比Java 与 Python 的核心流程是一致的源代码先被编译为字节码字节码再交由虚拟机逐条执行。差异主要体现在两处编译主体与字节码的去向不同Java 的编译由开发者主动执行编译得到的字节码以文件形式保存在磁盘上Python 的编译则由解释器在幕后自动完成字节码通常只存在于内存中。相应地Java 代码修改后需要重新编译才能运行Python 代码修改后可以立即再次执行。优化策略不同Java 普遍借助即时编译器加快字节码的执行Python 则依靠磁盘缓存减少重复编译其字节码始终由 PVM 逐条解释执行——这也是它运行效率相对有限的直接原因之一。可见混合执行并非一套固定不变的流程编译由谁执行、字节码存放在哪里、优化从何处入手都可以按需取舍。对使用者来说Python 的编译过程完全透明修改代码后可以立即运行这种使用体验与解释型语言十分接近业界习惯把 Python 称为解释型语言正是对这一使用体验的通俗概括而不是对其底层机制的精确描述。 总结从编写到执行本篇回答了两个问题程序用什么语言编写程序如何真正运行。编程语言先后经历机器语言、汇编语言与高级语言三个发展阶段表达方式越来越贴近人类对硬件的依赖越来越小但无论用哪种语言编写程序最终都必须以机器码的形式由 CPU 执行。围绕 “翻译” 这一环编译型语言在运行前一次性完成翻译执行效率高而跨平台受限解释型语言边翻译边执行跨平台灵活而运行效率通常较低混合执行借助字节码与虚拟机在执行效率与跨平台能力之间取得平衡。Python 采用的正是混合执行源代码先由解释器自动编译为字节码再由 Python 虚拟机逐条执行。