.. Kenneth Lee 版权所有 2024
:Authors: Kenneth Lee :Version: 1.0 :Date: 2024-04-03 :Status: Released
对一个设计评审意见的深入探讨
昨天收到一个设计的评审意见,我觉得可以作为深入探讨设计外延\ [#extension]_\ 的 好例子。本文对这个主题进行一些论述。
.. [#extension] :ref:s_extension
这个评审意见,去掉和细节相关的信息后,大致是这样的:
| 你说的优化点3,现在编译器没有实现,但我看到文档其他地方也有一些没有落进去 | 设计,我觉得这可能指一种可能性,你觉得合适就放着吧。
这个意见让我嗅到了危险的味道,进而让我担心起其他的评审意见来。因为这段意见反映 了评审者对“设计是什么”的理解可能都是错的,这一点如果双方没有一样的认识,那谈什 么都是鸡和鸭讲。
我说的“错误理解”主要包括两点:
我重点想谈的是第二点,但为了谈第二点,我们需要首先理解一下第一点。我经常用从深 圳出差去北京作为具象来谈设计的问题,这样大部分人都有共同的基础认识。所以我们继 续用这个例子来具象化理解这个第一点。
知道要出差北京了,马上出门向北跑,这显然就是“没有设计”,有设计的行为是先考虑大 的:坐飞机还是高铁去?去几天?票什么时候有?都什么价钱?这些几乎都不是和“到北 京”直接相关的,但这些是做这件事的关键控制点(主要矛盾或者矛盾的主要方面)。如 果我们直接开始做那件事(立即出门往北走),那我们就容易走错方向,因为没有一件复 杂的事情是可以按直线操作就完成的。要去机场你可能需要往南走,但只有往南走你才能 真的到达北京,向北走你大概率饿死在半路上。
所以,我们谈设计,本来谈的就是端到端的,我们要达成目标的每个关键要素(或者说特 征),我们保证这些要素被实现了,我们就“愈加靠近我们的目标”了。特征之间的逻辑空 隙(比如如何从家里到机场),是留给下一层设计的。留下这个空隙,本来就是设计的本 意。设计就是为了在路径上给细节插上标杆,保证我们可以走在正确的路上的。
那么“编译器现在没有实现某个优化点”,这个信息是否应该呈现在设计中呢?这要两说: 如果这些信息可以证明:“这个工作量很大,我们一点都还没有做,所以最终决策中不能 使用这个优化点。”,那这个信息应该呈现。因为它是构成我们前面说的那个“到达目标的 路径”逻辑链的一部分。
但如果这个信息用来证明:“编译器现在没有做这个功能,所以设计这样写不符合事实”, 那这个信息就是错的,因为这个逻辑建立的前提就不对:设计是为未来服务的。
好了,理解了这一点,我们应该可以理解第二点的前提了:“设计是严谨的模糊约束”。设 计是模糊的,因为它确实不会描述关键控制要素之外的细节。但设计也是严谨的,因为设 计加入的每个控制要求,都是我们逻辑链上必须的。这个约束出现,要不是因为证明这个 逻辑链,我们必须有这个控制要求;要不就是错误的,不应该加这个约束,而应该留给细 节设计作为自由度。多余的约束,在设计上会给细节设计留下过多的限制,这个上一层的 设计就犯错了。比如高层逻辑要求在某个地方实现一个算法,设计者因为个人喜好要求: 算法必须用Ocaml实现。结果这个约束和达成目的没有任何关系,本来这个算法有现成的 C++库可以用的,现在被迫开发了半年。那这个设计约束就是失败的,错误的。
所以,没有一个设计点是可以“你觉得合适就可以”的。要不合适,要不不合适,没有其他
观点。设计是技术上权衡利弊,不是工程师互相卖人情。设计给定的约束确实有猜的成分
(参考:\ :doc:架构设计中猜的成分
\ ),但这种猜也是基于当前所有信息的猜,是
一种技术决策,而不是“都可以”。设计本来就是执行前的筹划,你要说“都可以”,其实什
么都行,反正不会立即执行,但我们所有目的都是为未来“真的可以”服务的。从这个角度
说,设计点还是只有合适或者不合适两种情况,不是“都可以”的。
设计,特别是架构设计,通常都是一群人的工作。做一个大型的产品,我们可能有人做软 件,有人做硬件,有人做芯片,有人做开发,有人做测试,有人做营销,有人写文档……每 个人的经验都不一样,了解的细节不同。我们在架构设计上共享我们的细节经验,得到一 个大部分都认可的“共识”。所以设计通常是这样组成的:
首先我们会分很多层,这样让所有人都知道端到端的整件事是怎么做成的,这里模糊 了很多的细节,但大家至少知道别人是怎么承认那个细节是可以实现的。同时可以依 托一个树状的结构,找到自己熟悉的领域。
我们会在每一层写下我们针对各种细节的“总结”,这些总结有猜的成分,置信度有高 有低,但我们做成整件事就靠这些总结来搭,所以我们尽量找对我们这群人置信度最 高的事情来总结。
我们用这些总结的结论来搭建逻辑,来说明整件事情如何达成。
用维特根斯坦的理论来解释,我们写的“总结”,就是所谓的“Can be said clearly”(可 以说清楚)的部分,而这个总结如何被得到的,就是所谓的“Must be passed over in silence”(只能在沉默中传递)的部分。而最后的设计,就是用所有Can be said clearly的观点,搭建一个逻辑公式,说明事情如何做成。对于一群不同领域的专家组成 的高层(架构)设计,这个Must be passed over in silence的部分会特别显眼,因为确 实我们每个人对对方的细节其实都是非常缺乏认知的。
所以,我们评审一个设计的过程,包括两个部分:第一部分,是每个专业领域的专家,都 来看,这些Can be said clearly的部分,是不是可以接受。第二部分,在这些Can be said clearly的部分被大部分人认可后(包括其中猜测的成分被大部分人认为可以冒这个 险),所有人一起来校验基于它们搭建的逻辑是否正确。
这其中,其实最难的是第一部分。Can be said clearly的部分,三言两语就可以说清楚 了。但如果它的基础都是错的,那说多少都没有用。我们评审一个设计的时候,主要工作 量,其实是在这里。但按维特根斯坦的理论,这个结论的语义,取决于我们在Can be said clearly的时候,使用了它的什么属性。所以,对于这个结论的认识,也需要结合着 我们怎么使用这个结论来理解。关于这一点,请参考下面对内涵和外延的解释。
总结起来,评审的时候,也请聚焦到“校验结论”-“校验逻辑”这个逻辑上,不能觉得“反正 又不马上用,差不多就行了”。我们其实想要的是评审者关于“某某结论其实不成立,因为 在细节上,有巴拉巴拉这样一个问题……”,或者“就算这些结论都成立,但你没有考虑到巴 拉巴拉的情况,所以这个方案风险还是很大”这样的意见,这些才是有效的评审意见。
.. _s_extension
:
内涵和外延都是一个语义的一部分,但我们很容易犯的一个错误是用内涵代替了语义,导 致我们做出很多错误的判断。所以我们这里必须澄清一下,这样读者可能会更容易理解本 文说的问题。
内涵通常指我们直接区分一个概念的方法,比如熟,你查字典,它其中一个解释是:
| 食物烧煮到可吃的程度。
当然,熟这个字本身还有其他解释,但我们可以简单理解为这是一字多用。在我们这个讨 论中,我们认为熟就是这个语义。这里我们有了一个判断标准,“烧煮到可吃的程度”,我 们很容易由此区分开“熟”和“不熟”的区别,这个我们称为“内涵”。我们查字典主要查到的 都是内涵。但内涵并不能严格定义一个词语的语义。因为,这里还有一个关键在于,我们 区分了这个东西,是为了什么目的。
比如熟这个问题,我们关心熟不熟(集合内还是集合外),可能有很多理由:
内涵定义了一个集合,而外延定义了其他集合。其实两者的区分不那么严格。因为我们完 全可以用其中一个外延来取代内涵的。比如:
| 食物好保存的程度称为熟
不过,通常我们会选择一个最容易区分,而且最容易用作其他外延的子集的集合来做内涵, 因为这样好用。我们在设计中关心内涵和外延,其实就是在校验我们选择的这个名字(用 了它的内涵),是否确实是我们需求中的外延的最小集合?我们是否被名字的内涵左右了, 顾着内涵忘掉了我们的原始需求?如果我们的目的仅仅就是某个特定的外延,我们是否应 该改用这个外延作为我们的新概念的内涵,而不应该继续用原来的内涵?
这种情况下,区分内涵和外延就很有必要了。这本质就是所谓的“第一性原理”:我们回到 原始需求上,不是被已有的概念左右自己的思路。就好比你做一套新的指令,谁说指令就 必须是一个连续的执行序列?谁说指令必须一条条执行的?谁说访问内存就必须用访存指 令,一定要经过VMMA做地址翻译?做这些东西背后的驱动需求是什么?我们需要那个需求 吗?想明白这些问题,我们才有可能轻装上阵,扔下过去的负担,在新的现实面前走到新 的高度上。
最后举一个具体的例子。最近我们项目中在讨论在指令中使用定长还是变长指令的问题, 从接口的角度来说,我们很容易给出定长指令和变长指令的区别:
这个定义的内涵是清晰的,如果仅仅从内涵上理解这个语义,我们会简单认为定长指令比 较简单,但空间利用率不高,因为每条指令一样长,那么不需要那么多信息的指令多余的 长度就浪费了。而变长指令比较复杂,放置起来不容易对齐,但利用效率就高一些。
无论如何,这个决策对软件问题其实不大。但定长还是变长,芯片设计人员就争得死去活 来。原因是,CPU是一次取一段比较长的内存进行并行解码的,如果指令是定长的,那么 解码单元可以在一个时钟周期中完成整片内存所有指令的解码。但如果指令是变长的,它 就需要用一个周期解第一条指令,然后再用一个时钟周期解下一个指令,这样会拖慢它的 效率,从而影响他的性能。
你看,你不去抓住这个外延,你都不知道在芯片设计者眼中那个内涵的真正含义。