Home系统日知录
系统日知录

系统日知录

@青藤木鸟

《系统日知录》会持续更新数据库、分布式系统、存储、ML System 相关的想法、翻译、笔记和文章,通过深入浅出的持续解析,帮助业务开发程序员建立底层知识体系。

写代码不是全部,系统是综合学问。

不谋全局者,不足谋一域;
不学系统者,不足学编程。

专栏是买断制,在保证每篇文章的知识密度的基础上,持续不定期更新。会随缘将一些文章分享到公众号:“木鸟杂记”。关注该公众号后回复:“优惠券”,可以领取本专栏八折优惠券。

关于专栏内容,想交流可以留言、也可加我微信 qtmuniao。有个交流群,如果想加群可备注。
订阅351
文章119
最后更新:2024-1-25 18:13
查看 【系统日知录】 详情查看 【青藤木鸟】 主页
分享到微信打开

免费内容

2025-5-26 22:15

一文科普 19 个核心 AI 术语(上)

本文编译自一个非盈利组织的科普杂志平台:quantamagazine,虽然是非盈利的,但是背后有量化大金主,里面的文章质量还挺高的,感兴趣的同学可以去看看。文章术语来自:What the Most Essential Terms in AI Really Mean,结合我转到 AI 行业后一些时间,用更接地气的语言对原文“编译”和扩充了一遍。即使你不做大模型,但是在 AI 如此日新月异的发展速度下,对一些 AI “行话”的理解,有助于建立一些基本认知,以听懂一些常见的 AI 讨论。本文遴选除了 19 个高频被提及的概念,分享给你。由于篇幅略长,本篇是上。1. 人工智能(Artificial Intelligence)根据约翰·麦卡锡(John McCarthy)1955 年的定义,人工智能是“制造智能机器的科学与工程”。几十年来,人工智能的理念和实践与认知科学、机器学习、机器人学和语言学等多个领域交叉融合。如今,AI 主要指使用人工神经网络的系统,尤其是基于生成模型(如 ChatGPT,Claude)的应用,通常被直接称为 AI。AI 的浪潮基本上是每十来年就有一阵复兴。这一轮我们都知道,是以 “生成式大语言模型”(LLM)技术为代表。与“生成”对应的是“理解”,但毫无疑问,现在的大模型也有非常卓越的理解能力,才能生成。这里叫生成式 AI 可能和其主要利用了 Transformer 中 Decoder-Only 的架构有关系。上一轮 AI 浪潮大家应该也有印象,以“深度卷积神经网络”为基本技术,通过批量归一化、残差连接、Dropout 等技术让训练深层网络成为可能(以前会有梯度消失和梯度爆炸问题,得需要精密的微调数据分布和模型结构)。最有代表性的应用领域是计算机视觉,比如大家现在熟知的技术“人脸识别”。所以 AI 是一项很泛的概念,日常大家也用的很模糊,当下的语境里,AI 基本等于大语言模型。2. 生成式 AI (Generative AI)上面提了一嘴,AI 主要有两种类型的任务——理解和生成。大模型在文本模态上通常同时具有理解和生成能力。但在图片模态上,这两者分的更开一些,因此举例可能更好理解。如果大模型可以做我们拍照给他的课后题,我们就说该大模型有图片模态的理解能力;但该模型同时并不一定能按你的指令生成新的图片。当然,这也是因为,当前基础语言模型通常使用 Transformer(自回归)架构,而图片生成模型通常使用 Diffusion 架构,还没有融合到一块。当然,最近谷歌发布的 Gemini Diffusion 已经开始将 Diffusion 模型应用于文本生成。3. 基准测试(Benchmark)用于评估 AI 系统能力的一组特定任务,一个基准测试主要由数据集+评测方法构成。好的基准测试是推动 AI 快速进展的重要力量,有大量论文都是围绕基准测试发表的,引用量也相当大。2012 年,ImageNet 基准测试确立了神经网络的主导地位;2018 年,GLUE 基准测试对基于 Transformer 的语言模型也起到了类似作用。设计一个好基准测试时需要格外谨慎,在对应领域中数据集分布要合理、评测方法要科学。例如,BERT 曾利用“not”一词常出现在正确答案中的规律,在某个推理基准测试中取得高分;但移除这一偏差后,BERT 的表现降至随机水平。前阵有篇圈内很有名的文章“(AI的)下半场”,出自 OpenAI 一个华人研究员:姚顺雨。大体意思是说,在 AI 发展的上半场,正是一个个 Benchmark 不断推动了展示和验证新训练方法和模型的改进。但在下半场,AI 要转到更关注真实世界的应用价值,而非高度抽象出的侧面—— benchmark。4. 偏差(Bias)AI 输出结果不符合人类认知或价值观。比如训练于大型互联网数据集的生成模型常含有负面偏见:2023 年时,图片生成模型 Stable Diffusion 倾向于将“CEO”生成为较浅肤色的男性。模型有偏,通常是因为数据有偏,这主要涉及喂给大模型的数据分布的问题,因为大模型本质上就是在拟合你给它的数据的概率分布,再按照这个概率分布进行采样生成。当然,造成数据有偏的原因有很多:人类的价值观本来就是多样的。很难用同一个大模型去满足不同国别、族别人的需求。互联网数据并不符合物理世界信息分布。比如互联网上英文数据贼多、有偏见的信息可能会更大范围的传播。预训练阶段在遴选数据时方法有问题。比如过滤策略有问题导致某些种族偏见或者成人信息没有过滤干净,比如聚类方法有问题导致数据分布中某一个类别的数据格外多。当然,还有很多其他原因,但从逻辑链条来推导数据来源时,大致可以按以上类别分。我们有时候会说,本质上模型是用数据进行编程。其实是指,我们在训练模型时需要精确控制送进模型中的数据覆盖度和分布。当然,我们也可以通过后训练( alignment 和 RL)来对模型进行纠偏。5. 嵌入(Embeddings)高维数据(比如一张图片、一个文档)在低维空间中的归一化表征(比如一个向量)。这么说可能有点抽象,举个例子,假设我们有 1w 张图片,可以为每个图片使用某种方法抽出一个 768 维度浮点型向量。如果该向量抽的好,能包含原图片所有信息。则可以很方便用这些 embedding 加很薄几层 MLP(多层感知机,可以理解为很薄的神经网络),训练出各种分类模型,比如判断是不是截屏、判断是不是游戏解说、判断是不是地貌风景;也可以利用这些 embedding 对原图片或者文档进行聚类。可以从例子看出,这就是传统机器学习中常见的降维(从高维图像空间到低维向量空间)的一种方法,只不过现在有了大模型后,其对 embedding 抽取能力要比传统机器学习方法(比如 PCA,主成分分析)强太多,这就是所谓的大模型压缩能力更强嘛。此外,还可以将不同模态映射到同一个向量空间,从而让其可比,这样就能通过文字检索图片。6. 模型 (Model)每个模型有其自己独特的神经网络结构、有其特定的训练数据集和特定的训练方法。这三者结合,决定了每家公司产出的模型的能力范围。概括来说,模型使用神经网络作为基本构造法,然后利用互联网上的海量数据来对神经网络中的参数进行训练迭代。最后产出的模型可用于推理,进行预测。具体当前最常见的聊天形式,就是会以对话的方式,以语言为载体响应所有用户的输入。7. 基础模型(Foundation Model)斯坦福大学研究人员在 2021 年创造的一个短语,用于描述具有通用能力的模型基座。比如用于图片生成的 DALL-E 和用于文本生成的 GPT-3 。这些模型在预训练时见过的数据足够多,因此稍加微调(复杂一点需要微调来改变模型参数、简单一点只要尝试合适的提示词就行;前者称为 finetune,后者称为 few-shot)就能适应相当广泛的任务。而不像传统机器学习中的小模型那样,由于见的数据太少,只能满足特定领域的任务(如人脸识别),也没有什么后期微调的可能。当前,由于微调方向不同,每家模型可能都有自己的特色。比如 Deepseek V3 号称(benchmark上)在数学和编码方面比较强;Claude 在复杂代码能力上(真能干活)上比较强;ChatGPT 总体比较均衡等等。最近各家大模型公司,在研究通过加入物理学、医学、生物学等专业数据,来对模型进行微调以获得垂直领域能力更强的专家模型。比如听说谷歌的 AlphaFold  能够帮助发现新的蛋白质结构。9. 泛化(Generalization)模型的泛化能力的核心是指模型在训练完成后,应用于它从未见过的、新的数据时表现如何的能力。更严谨地说,泛化能力衡量的是一个在特定训练数据集上训练的模型,对于来自同一数据分布但未在训练中出现过的数据(即测试数据集或真实世界数据)进行准确预测或判断的能力。为什么基础模型有被进行 finetune 的可能性,就是因为其泛化能力。而传统的小模型,通常由于过拟合,导致泛化能力相对较差。比如,我们在利用传统卷积神经网络训练了一个猫脸识别的小模型是,只给它喂猫的正脸数据;该模型在面对猫的侧脸或者倒脸时,就不一定能识别出来。10. 幻觉(Hallucination)你问大模型问题时,他可能会一本正经的胡说八道:编造一些听起来合理但事实上错误或者虚构的输出。幻觉并不一个内涵精确的术语,更像是对上述现象直觉的描述。例如,大模型可能会捏造一些不存在的历史;也可能会生成具有六个手指的人。通过后训练(比如强化学习)可以缓解幻觉,但目前没有一种办法能完全消除幻觉——因为模型本质上是基于学到的概率分布来按采样生成下一个 Token。这决定了它肯定是有概率出错的。

2024-3-12 14:58

本专栏的正确“打开姿势”和“优惠信息”

首先非常感谢你的订阅。本专栏是一个大规模数据系统从业者的絮絮叨叨,涵盖的主题包括存储、数据库、分布式系统、AI Infra 、计算机基础等庞杂的系统知识。由于主题零散、内容驳杂,以下是一些建议的本专栏的“打开姿势”。在一切开始之前,可以关注我的公众号:“木鸟杂记”,回复:“优惠券”,即可获取订阅本专栏的八折优惠券。 订阅了之后欢迎分享你喜欢的文章,如果你的朋友通过你分享的文章订阅后,你都可以拿到之后订阅费(会随着文章数的增多不断上调)的百分之二十!订阅后如果专栏内容不符合预期,二十四小时内可以随时退款。目录和索引我会定期更新所有文章目录到简介中,根据此目录,作为一个新读者,你可以快速通过标题找到你想要的文章。注:📘 代表该篇是论文解读🔥 表示该篇启发+留言人数较多也可以通过顶部的标签,来快速筛选想要看的某类文章:当然,也可以修改排序方式,来从老文章读起:最后,对于某些你感觉不错,以后想回看,或者想稍后阅读的文章,也可以点击收藏。启发和留言如果某一篇文章帮到了你,一定要不要吝啬点一个“有启发”,或者留言,根据这些反馈,我可以在未来调整写作方向,产出更多大家感兴趣的文章。让“赞”和“吐槽”都来的更猛烈些吧!读者社区我们有个相关的小论坛:https://distsys.cn/ ,我专门开了版面:https://distsys.cn/t/column,每次发文章后,会将摘要发到论坛上,如果你想进行公开讨论可以去该贴下回复,或者单开主题。我们有个读者群,但是由于我没有定期组织活动,所以群里长期也没有人发言。如果这样你还想进,那就扫下面二维码吧。这个群,以后我能想到的作用,也就是发了新文章之后,转到群里,如果大家有即时想法,可以直接在群里讨论了。

2023-7-2 0:34

数据处理的大一统——从 Shell 脚本到 SQL 引擎

“工业流水线”的鼻祖,福特 T 型汽车的电机装配,将组装过程拆成 29 道工序,将装备时间由平均二十分钟降到五分钟,效率提升四倍 ,下图图源。这种流水线的思想在数据处理过程中也随处可见。其核心概念是:标准化的数据集合:对应待组装对象,是对数据处理中各个环节输入输出的一种一致性抽象。所谓一致,就是一个任意处理环节的输出,都可以作为任意处理环节的输入。可组合的数据变换:对应单道组装工序,定义了对数据进行变换的一个原子操作。通过组合各种原子操作,可以具有强大的表达力。则,数据处理的本质是:针对不同需求,读取并标准化数据集后,施加不同的变换组合。Unix 管道Unix 管道是一项非常伟大的发明,体现了 Unix 的一贯哲学:程序应该只关注一个目标,并尽可能把它做好。让程序能够互相协同工作。应该让程序处理文本数据流,因为这是一个通用的接口。— Unix Pipe 机制发明者 Malcolm Douglas McIlroy上述三句话哲学正体现了我们提到的两点,标准化的数据集合——来自标准输入输出的文本数据流,可组合的数据变换——能够协同工作的程序(如像 sort, head, tail 这种 Unix 自带的工具,和用户自己编写的符合管道要求的程序)。让我们来看一个使用 Unix tools 和管道来解决实际问题的例子。假设我们有一些关于服务访问的日志文件(var/log/nginx/access.log ,例子来自 DDIA 第十章),日志的每一行格式如下:// $remote_addr - $remote_user [$time_local] "$request" // $status $body_bytes_sent "$http_referer" "$http_user_agent" 216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" 200 3377 "<http://martin.kleppmann.com/>" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" 我们的需求是,统计出日志文件中最受欢迎的五个网页。使用 Unix Shell ,我们会写出类似的命令:cat /var/log/nginx/access.log | # 读取文件,打入标准输出 awk '{print $7}' | # 取出每行按空格分割的第七个字段 sort | # 对每行按字面值进行排序 uniq -c | # 归并重复行,并给出重复次数 sort -r -n | # 按重复次数降序进行排序 head -n 5 # 输出前五行 可以看出上述 Shell 命令有以下几个特点:每个命令实现的功能都很简单(高内聚)所有命令通过管道进行组合(低耦合),当然这也要求可组合的程序只面向标准输入、标准输出进行编程,无其他副作用(比如输出到文件)输入输出面向文本而非二进制此外,Unix 的管道的另一大优点是——流式的处理数据。也即所有程序中间结果并非都计算完成之后,才送入下一个命令,而是边算边送,从而达到多个程序并行执行的效果,这就是流水线的精髓了。当然,管道也有缺点——只能进行线性的流水线排布,这也限制了他的表达能力。GFS 和 MapReduceMapReduce 是谷歌 2004 年的论文 MapReduce: Simplified Data Processing on Large Clusters 提出的,用以解决大规模集群、并行数据处理的一种算法。GFS 是与 MapReduce 配套使用的基于磁盘的分布式文件系统。MapReduce 算法主要分为三个阶段:Map:在不同机器上并行的对每个数据分区执行用户定义的 map() → List<Key, Value> 函数。Shuffle:将 map 的输出结果(KV 对)按 key 进行重新分区,按 key 聚集送到不同机器上, Key→ List<Value>。Reduce:在不同机器上并行地对 map 输出的每个 key 对应的List<Value> 调用 reduce 函数。(下图源 DDIA 第十章)每个 MapReduce 程序就是对存储在 GFS 上的数据集(标准化的数据集)的一次变换。理论上,我们可以通过组合多个 MapReduce 程序(可组合的变换),来满足任意复杂的数据处理需求。但与管道不同的是,每次 MapReduce 的输出都要进行“物化”,即完全落到分布式文件系统 GFS 上,才会执行下一个 MapReduce 程序。好处是可以进行任意的、非线性的 MapReduce 程序排布。坏处是代价非常高,尤其考虑到 GFS 上的文件是多机多副本的数据集,这意味着大量的跨机器数据传输、额外的数据拷贝开销。但要考虑到历史上开创式的创新,纵然一开始缺点多多,但会随着时间迭代而慢慢克服。GFS + MapReduce 正是这样一种在工业界开创了在大规模集群尺度上处理海量数据的先河。SparkSpark 便是为了解决 MapReduce 中每次数据集都要落盘的一种演进。首先,Spark 提出了标准的数据集抽象——RDD,这是一种通过分片的形式分散在多机上、基于内存的数据集。基于内存可以使得每次处理结果不用落盘,从而处理延迟更低。基于分片可以使得在机器宕机时,只用恢复少量分片,而非整个数据集。逻辑上,我们可以将其当做一个整体来进行变换,物理上,我们使用多机内存承载其每个分片。其次,基于 RDD,Spark 提供了多种可灵活组合的算子集,这相当于对一些常用的变换逻辑进行“构件化”,可以让用户开箱即用。(下面图源 RDD 论文)基于此,用户可以进行任意复杂数据处理,在物理上多个数据集(点)和算子(边)会构成一个复杂的 DAG (有向无环图)执行拓扑:关系型数据库关系型数据库是数据处理系统的集大成者。一方面,它对外提供强大的声明式查询语言——SQL,兼顾了灵活性和易用性。另一方面,他对内使用紧凑、索引友好的存储方式,可以支撑高效的数据查询需求。关系型数据库系统同时集计算和存储于一身,又会充分利用硬盘,甚至网络(分布式数据库)特点,是对计算机各种资源全方位使用的一个典范。本文不去过分展开关系型数据库实现的各个环节,而是聚焦本文重点——标准的数据集和可组合的算子。关系型数据库对用户提供的数据基本组织单位是——关系,或者说表。在 SQL 模型中,这是一种由行列组成的、强模式的二维表。所谓强模式,可以在逻辑上理解为表格中每个单元所存储的数据必须要符合该列“表头”的类型定义。针对这种标准的二维表,用户可以施加各种关系代数算子(选择、投影、笛卡尔乘积)。一条 SQL 语句,在进入 RDBMS 之后,经过解析、校验、优化,最后转化成算子树进行执行。对应的 RDBMS 中的逻辑单元,我们通常称之为——执行引擎,Facebook Velox 就是专门针对该生态位的一个 C++ 库。传统的执行引擎多使用火山模型,一种属于拉( pull-based )流派的执行方式。其基本概念就是以树形的方式组织算子,并从根节点开始,自上而下的进行递归调用,算子间自下而上的以行(row)或者批(batch)的粒度返回数据。近些年来,基于推(push-based)的流派渐渐火起来了,DuckDB、Velox 都属于此流派。类似于将递归转化为迭代,自下而上,从叶子节点进行计算,然后推给父亲节点,直到根节点。每个算子树都可以拆解为多个可以并行执行的算子流水线(下图源,Facebook Velox 文档)我们把上图顺时针旋转九十度,可以发现他和 Spark 的执行方式如出一辙,更多关于 velox 机制的解析,可以参考我写的这篇文章。但无论推还是拉,其对数据集和算子的抽象都符合本文一开始提出的理论。小结考察完上述四种系统之后,可以看出,数据处理在某种角度上是大一统的——首先抽象出归一化的数据集,然后提供施加于该数据集之上的运算集,最终通过组合的形式表达用户的各种数据处理需求。

2023-6-11 23:23

生活工程学(一):多轮次拆解

我们在工程实践中,有些构建代码的小技巧,其背后所体现的思想,生活中也常常可见。本系列便是这样一组跨越生活和工程的奇怪联想。这是第一篇:多轮次拆解,也即,很多我们习惯一遍完成的事情,有时候拆成多个轮次完成,会简单、高效很多。我在进行 code review 时,有时会看到一些新手同学在一个 for 循环中干太多事情。这常会造成多层嵌套,或者 for 循环内容巨大无比。此时,如果不损失太多性能,我通常建议同学将要干的事情拆成多少个步骤,每个步骤一个 for 循环。甚至,可以每个步骤一个函数。当然,这些全是从维护角度着眼的。因为人一下总是记不了太多事情,一步步来,而不是揉在一块来,会让每个步骤逻辑清晰很多。后者,我通常称之为”摊大饼“式代码,这种代码的特点是写时很自然,但是维护起来很费劲——细节揉在一起总会让复杂度爆炸。软件工程中的最小可用原型,也是类似的理念。这种理念,其实在”函数式“编程中也随处可见,即对一个数据集操作时,我们会链式的应用一系列变换函数,从而让数据流清晰的展示出来。在大数据处理中,这种范式就更常见了,比如 spark 论文中提到的:errors.filter(_.contains("HDFS")) .map(_.split(’\\t’)(3)) .collect() SQL 查询引擎在实现时也是用的类似机制,即将一个查询语句,转换成对一个行列组成的二维数据集,施加多轮次的算子变换。如下图所示。图源:CMU15445,查询引擎讲义。我高中时学过一点点素描,虽然没有入门,但其多轮次的做图技法给我印象很深:先勾轮廓,再逐层完善。打线的时候也是一层层的打,而非一个地方画完再画另一个地方。我最近常常翻译文章,开始时,我总是务求一遍翻译好。但结果就是非常慢,且很容易放弃。后面开始使用多轮次、逐层打磨法。一开始用 ChatGPT 帮忙翻译一遍,然后自己再对照原文订正语义,最后扫一遍调换语序理顺词句等等。常言道,好文章是改出来的,应该也是这个道理。滑铁卢大学教授 Srinivasan Keshav 在其 ”How to Read a Paper“ 中阐述了经典的”三遍(three-pas approach)读论文“方法,也是类似的思想:The first pass:鸟瞰式略读,抓摘要、章节标题、结论等重点内容。The second pass:稍微细一些,但不要陷入细节。The third pass:细读,完全理解。其中任何一步都可以及时停止:这可能不是你需要的论文。但我之前读论文就长陷入一个误区,我愿称之为”地毯式读法“——逐字句过每一个细节。包括我刚开始进行 code review 时,也常常陷入这个误区。一次性的、按顺序把事情做完,是大部分人的天性,但这种天性往往是低效的,我们要通过不断地训练来克服。说起来,我出去点菜的时候,也常用两遍法——第一遍把想吃的都加上,第二遍考虑各种约束(偏好强弱、价格高低、吃过与否等等)来将菜品去到一个合理的范围内。我想背后的原因是:人的注意力是有限的,因此只擅长一次专注的做好一件事情。人的认知也是一个由浅入深的过程,一层层细化便是利用了这个特点。

2023-3-7 0:53

影响我写代码的三个 “Code”

国内很多大学的计算机专业,比较偏重基础和理论的“灌输”(就我当时的体验,现在可能会好一些),对于代码能力,虽然也有一些课程实验,但往往不太够用。于是,在进入正式工作前,很多同学就会对自己代码水平不太自信。下面我就根据我自身的写代码经历提供一些建议。一些经历我是 2010 年上的北邮,当时也是很迷糊的就进了计算机专业。自然的,在大学一开始也谈不上什么学习规划。只能是沿用着高中的学习方法,懵懂地跟着老师走——上课就听课,课余就自习做作业。结果便是,学习效率很低,上课听不太懂、题目做不通透。但总归,上完计算机导论后,编程作业都是自己啃出来的,跌跌撞撞的完成之后,慢慢地竟感受到了编程的乐趣。我们当时大作业最多的几门课,C++ 程序设计、算法和数据结构、操作系统、计算机网络、微机原理等,现在想来,大部分都都跟玩具一样。后来做了国外一些知名大学公开课的实验才知道,要打造好一个实验项目,是非常难的事情:首先,得适配学生的水平,准备详尽的实验材料。其次,得搭好代码框架,在合适的地方“留白”,给学生“填空”。最后,还得构建足够好的自动化测试平台,进行打分。如果从头开发,这里面涉及到的复杂度、需要花的心思,并不比发一篇顶会论文简单。那作为教授来说,有这些时间,我为什么不去发一篇论文呢?毕竟国内高校都是科研第一、教学老末。因此,我在本科课内,代码水平也并没有打下太好的基础。后面在读研和工作中,不断摸索。回头来看,对我代码能力提升比较快的有这几个 “Code”:LeetCode、Writing/Review Code Loop、Clean Code。LeetCode在说 LeetCode 前,想先说说工作后,见到的一类神奇的人——打过算法比赛(通称 ACM,其实是 ICPC 和 CCPC)的同学的印象。这类同学的一大突出特点,用最简练的语言来形容,就是:出活快。几年的竞赛经历,让他们只要在脑袋中对需求(题目)理解之后,就能在最短的时间内转化为代码。由于太过懵懂,我自然是没有打过竞赛,等反应过来竞赛的诸般好处时,已经大三下了。当时,校队也不会招这么“大龄”的队员了,就算招,门槛也非常高。也是大学诸多憾事中的一件了。后来读了研,在找工作前一年时,LeetCode 已经相当流行了,便也和同学组队,互相激励着刷了起来。当时题目还不是特别多,到研二暑假找实习时,大概把前两百多道刷了两遍。一开始,会不断思考题目是什么意思,该用什么算法解,有时半天想不出来,便去看高票答案。很多高票解真的是精妙而简练,这大概也是当时 LeetCode 最吸引人的地方之一。慢慢的对各种类型题目有些感觉之后,就开始练速度和通过率。也就是上文说的,在理解题目后,能够迅速转变为 bug free 的代码。因此,虽然没有打过比赛,但是通过 LeetCode 的训练,确实也有了类似竞赛的收获。但自然,在深度、广度和速度上都远不及那些“身经百赛”的同学。不过我已经是受益匪浅:对常见数据结构和算法掌握纯熟。比如现在说起六种排序,特点、使用场景、背后原理,可以做到如数家珍;比如说起树的各种递归非递归遍历,脑动模拟递归执行过程,也是信手拈来;再比如链表、队列、图等特点,也能在脑中边模拟,边换成代码。学到了很多精巧的代码片段“构件”。比如如何二分、如何迭代、如何处理链表头尾节点、如何设计基本数据结构的接口等等。这些偏“原子”的构件,是我后来工作中写代码的血肉来源。但只有这些,是远远不够的,一到大项目里,写出的代码就很容易——“有佳句无佳章”。Writing/Review Code Loop遇到上述窘境,便是因为缺少中大型项目的磨练。表现在空间上,不知道如何组织上万行的代码,如何划分功能模块、构建层次体系;体现在在时间上,没有经过项目“起高楼、宴宾客、楼塌了”的构建-腐烂-重构循环。工程中在理解代码和组织代码时有个矛盾:可理解性。作为维护人员,我们学习代码时,多喜欢顺着数据流和控制流来理解,所谓根据某个头,一路追查到底,是为纵向。可维护性。但作为架构人员,我们组织代码时,为了容易维护,多是按照围绕模块来组织代码——把关联紧密的代码聚合到一块,是为横向。所以我们在拿到一个大工程时,如果立即按模块、地毯式的看代码,肯定会昏昏欲睡、事倍功半。不幸的是,由于多年读书养成的强大习惯,这个毛病跟了我很多年。正确的打开方式是,要像对待团在一起的多条线一样,找到“线头”,然后慢慢往外揪。在项目中,这些线头是:service 的 main 函数、各种单测入口。但我们在构建一个大工程时,又得反着来:先搭建一个揉在一起的主流程,然后逐渐迭代。就像盘古开天辟地一样,随着时间而演化,让天慢慢的升高、地慢慢下降,让整体化为地上四极、山川河流、太阳月亮。如是迭代,将一个混沌的流程,慢慢地模块化。比如常用的工具模块(utils)、业务相关基础模块(common)、控制模块(controller、manager)、RPC HTTP 等回调处理模块(processor)等等。但当然,如果你已经有了构建某种类型系统的经验,则并不需要在构建初期经历这个漫长过程,可以直接按经验分出一些模块。更有甚者,你已经形成了自己的一个代码库,比如时钟、网络、多线程、流控等等,在构建新的工程时可以直接拿来就用。剩下的问题就是对于细节的微调:1. 我们在进行分层时,边界处的功能,是往上升,还是往下沉;2. 某个较完整的结构,是拍平到使用类里,还是单独拎出来;这些形形色色的决策,都没有一个定则,更多的还是根据场景的需求、工期的长短等诸多实际情况,便宜行事。而这种背后的决策,则是在长时间对中大型项目的学习、对别人修改的 Review、自己上手搭架子和修修补补中,一点点形成的直觉和偏好。就像股票市场有周期一样,工程代码也是有其周期。不经历一个股市牛熊周期,我们不敢轻言空多;不经历过一个工程构建-成熟-腐烂的周期,我们也不敢轻言取舍。即,没有这些经验,我们就没办法在工程构建初期,预见到其最常用的打开方式,进而面向主要场景设计,牺牲次要场景的便利性。单元测试的重要性,怎么强调都不为过。一方面,能不能写出的单元测试,意味着你代码的模块边界是否清楚;另一方面,通过设计好的输入和输出,测试能够保证某种“不变性”,之后无论你怎么微调、重构,只要能跑过之前的测试,那就可以放心一半。另一半,就要靠自己和别人不断 Review 、测试集群线上集群不断地迭代了。所以,这个过程是一个无休止的 loop,不断的磨,尔后不断地提升。Clean Code最后说说对代码的品味。小节标题是:Clean Code,是因为我对代码的品味,最初是从 Clean Code: A Handbook of Agile Software Craftsmanship 这本书建立起来的。其第二章对命名——这个工程中“最难”的事情——的阐述,给我印象很深。工作中,我们常说,某某某对代码有“洁癖”。我也多少有一些,但我不以为是洁癖,而是一种对美的欣赏和追求。代码的美体现在哪里呢?我这里稍微抛个砖(当然,我之前也写文章就代码命名问题啰嗦过,感兴趣的可以点这里可以去看看):一致性。比如具有相同含义的实体,使用相同的命名;而需要区分的实体,则要通过命名域(namespace)、前缀来进行甄别。从而给读者造成最小的心智负担。体系性。是指我们在做一组相关接口时,要考虑其体系性。比如增删改查,比如生产消费,比如预处理、处理、处理后,比如读取写入等等。体系性又包括对称性和逻辑性,让人在拿到一组接口时,就能最小成本地理解其是如何相互联系、又是如何具有区别的。没有赘肉。写代码,不要啰嗦,不要啰嗦,不要啰嗦。如果不小心啰嗦了,说明你可能没有想清楚所解决问题的本质。复杂的表象,在不断地剥离杂质后,往往有很简单的关窍。抓住这些关窍,再往其上附着骨肉,同时理清楚一对多、多对多等依赖关系,往往能化简为繁。在审美之外,还要说说建模(在某种程度上和隐喻是相通的)。毕竟,我们在说工程的“构建”时,本身就是借助的建筑学中的隐喻。软件工程中,类似的隐喻随处可见。我们大脑在认知新事物时,多建立在对旧的模型套用(好一点叫化用)上。因此,如果在处理模块时,如果能从经典的模型库中,找到一个相对合适的抽象,往往能够极大降低用户理解门槛。比如经典的生产者消费者模型、树形组织模型、路由器模型、线程调度模型、内存模型等等。此外,也可以使用某种常见意像、隐喻来命名项目,往往也能在易理解性上收获奇效。比如监控系统,可以叫“鹰眼”;比如各种流水线管控,可以叫“富士康”(手动斜眼);再比如更常见一些的数据采集,我们都知道他叫——“爬虫”。最后,世间的事情往往是多方印证、互相补足的——如果你想写好代码,就不能只写代码,你得去读读历史、学学美术、写写文字,建立一套你自己的审美偏好,然后将其理念平移到写代码里来,才能写出符合直觉、具有美感的好代码。