Introduction
大家好。欢迎来到十一月。希望你们一切都好。
刚过去的十月我们忙得不可开交。我们刚刚对社区解答做了一次大改进。 我们现在会对它们去重,所以相似的解答只会显示一次;我们还新增了多种排序方式、按代码搜索的能力,如果你去看 C#,会发现我们还加了按不同编程概念筛选的功能。 这样你就能搜索使用位移、递归或其他任何你感兴趣的东西的解答。我们会在社区周把它推广到其他 track 上。
不过现在,我们先把注意力放到 #12in23 上。 十月是个有趣的月份,我们探索了面向对象语言,而本月我们要来点硬核的。 我们聚焦汇编语言,具体来说是 MIPS 汇编、x86-64 汇编和 WebAssembly。 和往常一样,Erik 会带我们了解这些语言为何有趣、为何独特。
The badges
和往常一样,完成这些语言中任意 5 道练习,就能获得 Nibbly November 徽章。我们还有全年徽章,我知道很多人都在朝着它努力。为此我们准备了 5 道精选练习供你完成,分别是:
- 位数统计:数出一个数字里 1 的个数
- 麦粒:计算棋盘上每格翻倍的麦粒总数
- 电阻色环:把电阻色环的颜色转换成对应的数值
- 旋转密码:实现旋转密码(又称凯撒密码)
- 核苷酸计数:计算每种核苷酸在 DNA 字符串中出现的次数
Background
Why name this Nibble November?
嗯,你大概知道,一个 byte 是 8 位。而一个 nibble 是 4 位。如果你把 byte 里的 y 换成 i,就得到 bite,这个名字就开始讲得通了。而 nibble 就是咬一小口。
What is an assembly language?
好,我们先从一些基础讲起。CPU 执行指令,比如“把两个数相加”或“把位左移”等等。这些指令被称为机器码指令,它们不过是特定的位序列。执行一个程序,“简单来说”(打个引号)就是 CPU 处理并执行这些位序列。
由于直接把指令写成位序列既繁琐又容易出错,早在 1947 年,Kathleen Booth 和 Andrew Donald Booth 就提出了一种对人类更友好的语言来表示机器码指令。这种用来表示机器码指令的语言就叫汇编语言。汇编语言再通过“汇编器”转换成机器码指令。
汇编语言一个有意思的地方是,它们与 CPU 架构直接绑定,不过通常独立于操作系统。
顺便说一句,如果你还记得穿孔卡片,就是最早期的计算机用来执行程序的那种大卡片,它们其实也是一种汇编语言!
How is assembly language different from the languages we now program in?
关键区别在于汇编语言非常底层。你会失去许多已经习惯的抽象。类或对象完全不见踪影。循环?你得通过跳转手写出来。函数?没门!写汇编代码让我感到很谦卑,因为你会意识到现代语言为你的生活带来了多少便利。不过,写汇编代码也非常有用,因为你会对事物的实际运作方式有更深入的理解。
有趣的是:RollerCoaster Tycoon 99% 的源代码都是手写的汇编代码!这是一项惊人的成就,等你亲自写一点汇编之后,会更加体会到这一点。
Do people still write in assembly code?
嗯,比过去少了。过去,手工编写的汇编常常比编译器生成的机器码性能更好(这也是 C++ 允许直接内嵌汇编代码的原因之一),但如今编译器生成机器码的水平已经非常高,这种情况已经很少见了。尽管如此,在性能关键或资源受限的环境中,你仍然能看到汇编语言的身影。
Overviews
MIPS
- MIPS(Microprocessor without Interlocked Pipelined Stages)是一系列精简指令集计算机(RISC)指令集架构
- 由 MIPS Computer Systems 开发,于 1985 年首次发布
- 有多个版本:MIPS I、II、III、IV、V 和 MIPS32/64。前两个版本只支持 32 位,而 MIPS III 引入了 64 位支持。
- 若干可选扩展,例如 SIMD 指令和压缩
- 对后来的 RISC 架构影响深远
- MIPS 在 2021 年宣布不再继续开发 MIPS 架构,并迁移到 RISC-V(一个开源、免版税架构)
- 主要用于嵌入式系统(如路由器)和服务器(Silicon Graphics 的计算机用过它,这些计算机以电影特效闻名)、NEC Cenju-4 超级计算机、特斯拉 Model S 汽车、NASA 的新视野号探测器,也用于在大学里教授汇编,以及多款游戏主机(如初代 PlayStation、PlayStation Portable 和 Nintendo 64)
x86-64 Assembly
- 由 AMD 设计,于 1999 年以 AMD64 架构发布
- 它是 x86 指令集的 64 位版本,x86 可追溯到 1978 年 Intel 推出 8086 微处理器的时候。那是一颗 16 位处理器,后来 80386 加入了 32 位指令,而那套指令集后来就成了 x86 的代名词。
- 64 位带来的核心能力是可以寻址更多内存(32 位寻址上限为 4GB),而内存曾经是个瓶颈。64 位理论上可以寻址 16 EB,但目前只用了 48 位,可寻址 256 TB(以后有需要时可以扩展)
- AMD64 扩展了 x86 指令集,并通过兼容模式设计与现有的 16 位和 32 位应用程序完全兼容
- Intel 在没有 AMD 参与的情况下设计了 IA-64。它是一套全新、迥然不同且不向下兼容的 64 位指令集。最终 AMD64 胜出,Intel 也实现了自己的版本(只有细微的语义差异)
- 无处不在。从工作站到服务器(包括超级计算机),从嵌入式系统到游戏主机(如 PS5 和 Xbox Series X)。
WebAssembly
- 由 W3C 设计,这是一个负责 Web 技术的标准组织
- 设计目标是:
- 快速、安全、可移植
- 高效且可移植的表示形式
- 当年,要在 Web 上实现快速执行,通常得靠专门的浏览器插件,比如 Flash 和 Silverlight,因为 JavaScript 本身并不太适合高性能计算。这些插件的主要缺点是往往有大量安全问题,而且没有标准化。
- Mozilla 设计了 asm.js,它是 JavaScript 的一个子集,旨在让代码在浏览器中运行时有出色的性能表现,它通过类型一致性(类型不能动态改变)和没有垃圾回收来实现这一点。各种语言于是可以编译成 asm.js,在 Web 上运行时依然能获得不错的性能。不过它终究还是 JS,能做到的事情有限。于是就有了新语言的提案:WASM。
- 类汇编语言,因为它提供了一组要执行的指令。关键在于,它不绑定特定的 CPU,因此与平台无关,每个平台都需要一个实现(虚拟机)。这意味着 WebAssembly 实际上是字节码,而不是机器码
- 静态类型(与 JS 的关键区别)
- 通常使用提前编译或即时编译(但也可以解释执行)
- 开放标准,定义了两样东西:
- 二进制格式
- 文本格式(会被编译成二进制格式)
- 所有主流浏览器都有实现
- 用于许多需要高性能的网页,比如 Google Earth、Figma、Unity 和 Autocad。在服务端也越来越受欢迎,比如用来运行微服务、运行在 SaaS 平台上(如 CloudFlare workers),或用在 Docker 中
And from a programming perspective, how do they differ?
MIPS
- 采用加载-存储架构(又称寄存器-寄存器架构),指令要么做内存访问,要么做算术运算,但都只对寄存器中的数据进行操作
x86-64 Assembly
- 采用寄存器-内存架构,允许对内存(或从内存)以及寄存器执行操作
WebAssembly
- 采用基于栈的编程(没有寄存器),并且可以选择从内存读取数据以及写入内存
What are the things that make these languages great?
MIPS
- 小巧。MIPS 指令集的所有指令一页就能装下
- 成熟的调用约定有助于弄清如何使用可用的寄存器,比如用哪些传递实参、用哪些返回结果。
- 稳定。最后一个版本发布于 2014 年。
- 文档丰富,尤其在学术教材中。
- 实际应用广泛。数十亿台设备。
x86-64 Assembly
- 虽然它是 x64 的扩展,但加入了许多新特性,包括:
- 64 位整数支持
- 更多寄存器
- SSE 指令(向量指令)
- 相对数据访问(使用共享库时更高效)
- 不可执行位(一项安全特性,防止在某些内存页中执行代码)
- 熟悉。由于它扩展了 x86 指令集,熟悉 x86 指令集的人学起来会相对容易。 文档详尽而全面。
- 稳定。虽然会定期加入新版本,但核心部分极其稳定,且向下兼容。
WebAssembly
- 与物理处理器的汇编语言(包括 RISC)相比,基于栈的 WebAssembly 虚拟机精简而简单。这使得为它编译相对容易。
- WebAssembly 文本格式使用 S 表达式的“语法糖”来实现熟悉的命令式风格,再被转换成基于栈的代码。带 S 表达式的形式称为“加糖形式”,它会被“脱糖”成另一种形式,与二进制中的内容等价。任何用过 LISP 的人都会熟悉 S 表达式。
- 与 JavaScript 的互操作性强。在 JavaScript 之间来回传递数据很简单。重要提醒:WASM 目前还不能与 DOM 交互
- 持续改进中。不仅 WASM 虚拟机在不断改进,标准本身也在积极开发中。大量新特性正在设计和开发,比如与 SIMD 相关的指令、垃圾回收、线程、尾调用优化等等
- 安全。代码会在沙箱环境中被验证并运行,相比 JavaScript 或原生汇编语言,提供了更高程度的静态验证。语义定义明确,便于检查与推理
Standout features
MIPS
- 高效。MIPS 处理器非常高效,非常适合嵌入式系统。
- 性能。性能出色,因此 MIPS 曾被用于超级计算机
- 易于学习。指令少,每条指令只做一件简单的事,因此容易上手,非常适合教学。
x86-64 Assembly
- 强大。x86-64 经过几十年的打磨,拥有大量有助于提升性能的指令。一个例子是 SIMD(单指令多数据),这类指令,嗯,能让一条指令在多个数据上并行执行。
- 无处不在。运行 x86-64 的设备随处可见。Intel 和 AMD 的 CPU 都实现了它,它已经做了相当长时间的事实标准
- 定期更新。比如新的向量指令,包括 SSE3-5、AVX、AVX-512 等等
WebAssembly
- 高效。二进制格式紧凑,可以在一次快速遍历中完成解码、验证和编译。它还支持流式处理,可以在尚未收到全部数据时尽早开始解码、验证和编译。它还可以并行处理,因此非常适合高性能的 Web 应用。
- 优秀的编译目标。让许多语言的代码都能在 Web 上运行。大多数主流语言都支持编译成 WebAssembly 二进制文件,这意味着你的代码无需编写 JavaScript 就能在 Web 上运行。有些语言不是把你的代码编译成 WebAssembly,而是把运行时编译成 WebAssembly,然后就能运行未经改动的字节码。
- 易于部署和运行。你只需要一个能解释字节码的虚拟机,而所有主流浏览器都自带一个。
- 不局限于 Web,也能在服务端运行。WebAssembly 系统接口(WASI)是一个旨在可移植到任何平台的接口(ABI 和 API)。它类似于 POSIX(Unix 系统的标准接口),并提供 I/O 之类的功能。安全是其设计的核心部分,包括沙箱化以及面向能力(你需要显式请求对文件或套接字等资源的权限)。WASI 还有潜力让不同语言之间轻松互操作。Docker 联合创始人 Solomon Hykes 在 2019 年写道:“如果 2008 年就有 WASM+WASI,我们根本不需要创造 Docker。”
Which to choose
- 如果你从未接触过汇编语言,WebAssembly 大概是最容易上手的。话虽如此,如果你想学一门会编译成机器码的汇编语言,那就试试 MIPS 汇编
- 如果你用的是 x86-64 机器(很可能),那就试试 x86-64 汇编
- 如果你熟悉 LISP,你会欣赏 WebAssembly 使用 S 表达式这一点
- 如果你在做 Web 应用,WebAssembly 是最合理的选择
- 如果你在意性能,x86-64 和 MIPS 都是很好的选择。或者,如果你在意 Web 性能,就试试 WebAssembly