Skip to main content

转码记录 Vol.02 | 25 Winter:校内三门 + 自学四门,这学期开始有了层次感

· 11 min read
Yi Wang
Full Stack & AI Engineer

上学期(24 Fall)用三门课完成热身。这学期开始不一样了:校内三门课 + 自学四门课同步推进,工作量直接翻倍。CS 61A、CS 61B、CS 61C、Nand2Tetris——技术知识开始有了层次感,从 NAND 门到高级语言的完整链路第一次在脑子里贯通。


上学期(24 Fall)用三门课完成了热身——离散数学建立了形式化推理的思维方式,微积分稳住数学基础,EECS 1012 给了第一次写出可运行程序的感受。

但那个学期结束时,有一种感觉:框架有了,还很空。知道命题逻辑是什么,能写简单的 JavaScript,但还停留在"知道是什么"而不是"理解为什么"的阶段。

这学期开始不一样了。校内三门课 + 自学四门课同步推进——其中 CS 61A 实际在 24 Fall 期间并行完成,放在这里一起说。工作量直接翻倍,但收获也是。


24 Fall 同期自学:CS 61A – Structure and Interpretation of Computer Programs(UC Berkeley)

这门课用 Python 和 Scheme 讲,但重点不是语言语法,而是计算的基本思想:抽象、递归、解释器、高阶函数。

对我影响最大的是"函数是一等公民"这个概念——函数可以作为参数传入另一个函数,可以作为返回值返回。这和命令式编程(先做 A 再做 B)是不同的范式。Lambda、闭包、尾递归优化,在这门课里都有系统讲解,而不是在某个框架文档的角落里顺带提一句。

课程还要求用 Python 实现一个简化的 Scheme 解释器。解释器的工作是:源代码字符串 → 词法分析(Tokenizer)→ 语法分析(Parser)→ 求值(Eval)。每一步都自己实现。做完这个之后,对后来 Nand2Tetris 里实现编译器的过程有了直接的认知基础——两者的结构几乎一样,只是输出目标不同。


校内课程

EECS 1022 – Programming for Mobile Computing

正式课程名是移动计算编程,用 Java 和 Android 环境做实际的移动应用开发,成绩 A+。

难点不在语法,在于从脚本思维切换到面向对象思维

JavaScript 里写逻辑,函数是主角,数据跟着函数走。Java + Android 的世界里,首先要问的是:这个功能属于哪个类?这个类有什么状态?状态由谁管理?Activity 的生命周期(onCreate → onStart → onResume → onPause → onStop → onDestroy)是 Android 的基本框架,每个阶段对应不同的资源分配和回收策略。在写代码之前,必须先想清楚对象的结构。

Android 开发引入了事件驱动编程的完整模型:用户点击按钮,触发 OnClickListener,回调函数执行。这个模式和上学期 EECS 1012 里 JavaScript 的事件处理是同一套思想,但现在用类和接口来描述,结构更明确,边界更清晰。

课程 Lab 最后要做一个完整的 Android App,从 UI 设计到数据逻辑到设备传感器接入。第一次真正体会到"一个可以装进手机的程序是怎么从代码变出来的"。


MATH 1090 – Introduction to Logic for Computer Science

这门课的前置是 EECS 1019(离散数学),把上学期学的命题逻辑推向了更系统的形式化方向,成绩 A。

EECS 1019 讲的是命题逻辑的基本概念——真值表、逻辑连接词、证明方法。MATH 1090 在此基础上引入谓词逻辑(Predicate Logic),以及程序规约和验证的应用。

谓词逻辑的关键升级是引入了量词(∀ 和 ∃)和变量。∀x ∈ ℤ, P(x) 这类语句允许对一类对象统一描述性质,而不只是处理具体的真/假值。这让"描述系统行为"变得可能:可以用形式化语言说清楚一个函数"对所有合法输入都返回正确结果"是什么意思,而不只是靠测试用例来近似。

这门课最有实用价值的部分是前置条件(Precondition)和后置条件(Postcondition)

  • 前置条件:函数调用前,输入必须满足的约束
  • 后置条件:函数执行后,输出和状态必须满足的约束

这套语言让"正确性"从模糊的直觉变成了可以被形式化描述的东西。后来在 EECS 2101(数据结构)里,每个 ADT 操作都要用这套语言来规约;在 EECS 3311(软件设计)里,这套方法会进一步系统化。这学期学的是基础,那些课里用的是应用。


MATH 1014 – Applied Calculus II

接续上学期的 MATH 1013,这学期完成微积分下册,成绩 A。

内容:积分技巧(分部积分、换元、三角代换)、级数(比值法、根值法、积分判别法)、多元函数入门(偏导数、梯度)。

级数收敛判别值得单独记一句:判断级数收敛与否用的是渐近估算思维,和算法分析里判断复杂度是否可接受用的是同一套工具。现在看起来是数学课,后来在 EECS 3101 算法分析里会用到。


自学课程

CS 61B – Data Structures(UC Berkeley)

系统学习基础数据结构和算法:链表、树(BST、AVL、Red-Black Tree)、哈希表、图(BFS / DFS / Dijkstra / Prim)、排序(MergeSort / QuickSort / HeapSort),以及对应的时间空间复杂度分析。

Berkeley 的这门课有完整的 Project 体系,Gitlet 项目是其中难度最高的:用 Java 实现一个简化版 Git,支持 initaddcommitbranchcheckoutmerge 命令。

核心设计挑战:

提交历史是 DAG(有向无环图),不是线性序列。 每个 commit 节点有指向父节点的引用,branch 名只是一个指针,指向某个具体的 commit;HEAD 是指向当前 branch 的指针。只有在脑子里建立这个图结构,checkoutmerge 的行为才能讲得通。

merge 的逻辑依赖 LCA(最近公共祖先)。 当两个 branch 要合并时,系统需要找到它们提交历史的分叉点。这是图遍历算法在实际工程问题里的直接应用——BFS 从两个节点出发,找到第一个共同访问到的节点。

持久化靠内容寻址存储。 Git 的底层是一个文件系统:每个 blob 和 commit 对象用 SHA-1 哈希值命名,存在 .gitlet/objects/ 目录下。Gitlet 用 Java 对象序列化实现这个机制。做完这个项目之后,git log --graph 里的那张图不再是黑盒。


Nand2Tetris – Build a Modern Computer from First Principles(Coursera)

Part I 是从 NAND 门开始搭数字电路,一路到 CPU 和内存(硬件层)。这学期的重点是 Part II:软件栈

完整的翻译链路:

Jack 源代码(高级语言)
↓ Jack 编译器(Tokenizer + Code Generator)
VM 中间代码(基于栈的虚拟机语言)
↓ VM 翻译器
Hack 汇编语言
↓ 汇编器(Part I 已实现)
二进制机器码

VM 翻译器:VM 语言用栈机器模型——所有运算通过压栈和弹栈完成,没有寄存器的概念。VM 翻译器的工作是把这些栈操作映射到 Hack 汇编的寄存器操作上,同时处理函数调用约定(调用帧的创建、局部变量分配、返回地址保存)。

Jack 编译器:分两步。Tokenizer 把源代码字符串切成 token 序列(关键字、符号、标识符、整数常量、字符串常量);Code Generator 按语法规则递归处理 token 序列,生成对应的 VM 代码。递归下降解析(recursive descent parsing)是实现的核心结构,每种语法结构对应一个递归函数。

这门课完成之后,对"高级语言到机器指令"的翻译过程有了完整的、可以逐行对应的认知,而不是一个模糊的"编译器负责这件事"。


CS 61C – Great Ideas in Computer Architecture(UC Berkeley)

这门课从 C 语言出发,一路向下到 RISC-V 汇编、CPU 微架构、内存层级、并行计算。是这学期最重要的自学内容,也是下学期 EECS 2021(计算机组织)的直接前置。

C 语言和内存模型:C 没有垃圾回收。局部变量在栈上分配,malloc 的内存在堆上(必须手动 free),全局变量在静态段。C 的指针直接操作内存地址——理解内存布局,才能理解为什么空指针解引用会崩溃,为什么数组越界的后果是不确定的(而不是报一个整洁的 IndexOutOfBoundsException)。

RISC-V 汇编:没有变量,没有类型,只有寄存器(32 个通用寄存器)和内存地址。函数调用靠约定(参数放 a0-a7,返回地址存 ra)。写了几个 RISC-V 程序之后,开始能直接把 C 代码在脑子里翻译成汇编结构:一个 for 循环是什么,一个函数调用是什么。

CPU 流水线:现代 CPU 将一条指令拆成 IF(取指)→ ID(译码)→ EX(执行)→ MEM(访存)→ WB(写回)五个阶段并行运行。数据冒险(后续指令依赖前面指令还没写回的结果)靠转发(forwarding)或流水线暂停(stall)解决;控制冒险(分支跳转方向未知)靠分支预测处理。

内存层级:寄存器 → L1 缓存 → L2 缓存 → 主存 → 磁盘,每级延迟差一到两个数量级。程序的访存模式(时间局部性和空间局部性)决定缓存命中率,进而决定实际性能。经典例子:矩阵乘法两种循环顺序(ijk vs ikj),算法复杂度相同,但访存模式不同,性能可以差几倍。这个例子之后在 OS 课和数据库课里都会再次出现。


这学期结束时的状态

上学期学到的离散数学和逻辑,这学期有了实际的应用场景:MATH 1090 把它系统化,CS 61A 用它描述计算的本质,CS 61B 的数据结构用它做正确性证明。

更重要的是,技术知识开始有了层次感。CS 61C 提供了最底层的视角——CPU 怎么工作,内存怎么组织;Nand2Tetris 打通了高级语言到机器码的翻译链路;CS 61A 和 CS 61B 建立了抽象层面的编程思维。以前这些是孤立的工具,这学期第一次感受到它们之间有结构,而且这个结构是自洽的、可以推导的。


学期之外的思考

Berkeley 和 MIT 的课程放在网上是免费的。但"免费"和"容易"是两件不同的事。

CS 61C 一门课的内容量大概相当于三门普通校内课,Project 的代码量和难度是认真设计的,不是随便浏览一遍就能过去的。这学期把四门自学课和三门校内课并行推进,实际有效学习时间远超正常学期。

自学最大的成本不是内容难度,而是自律的持续性。课程没有截止日期,没有同学压力,没有强制反馈——唯一的外部约束是自己给自己设定的。这种情况下,拖延是系统性的,不是偶然的。我的应对方式是主动制造约束:把每门课拆成 weekly checkpoint,把 Project 截止日期写进日历。没有这些结构,自学的完成率会断崖式下降,这是这学期验证过的事实。

做这些自学课的过程中,有几次想过"这些东西到底有没有用"。不是理性层面的质疑——路径已经想清楚了——而是在某次 Lab 卡了很久之后,情绪上的那种怀疑。后来的判断是:这种怀疑在学习路径的早期几乎是必然出现的,因为知识还没有连接成网,看不到它的用处。真正有效的应对不是反复说服自己"有用",而是继续做,等到某一刻它自己显示出用处。

那个时刻在后续的学期里确实到来了。


下一篇写 25 Summer——三门校内课集中在系统层,用 Verilog 从零实现了一个 CPU。