标准和设计的区别

本文给刚进入标准定义领域的工程师科普一下怎么做标准,减少走弯路。

标准和设计非常相似,都是一种Specification,比如,都会说诸如:把系统分成三个模块,A模块提供几个函数或者几个系统寄存器,编址如何如何,格式如何如何,状态机如何如何……等等等等。

但两者的工作难度是完全不同的,要做一个实际可用的标准,难度比设计大很多。这种难度体现在两个方面:完备性和抽象性。我们分这两个维度来讨论这个区别。

设计是进入具体实现前的逻辑设计,比如对于软件,设计会设计模块分块,线程布置,互斥管理,状态机模型,数据结构等,但这个设计并不包括最终实现系统的全部逻辑,因为更细节的逻辑我们是在编码的时候组织的,你不可能在设计的时候把编码都想了——当然,有些纸上谈兵的领导是这样要求的——但现实中,如果他那样要求,你一定是先编码然后拷贝回设计文档中。这是个客观事实,因为文档并没有编译器,IDE这些配套工具去支持你更容易写好它。既然如此,哪个白痴会舍近求远呢?

所以,设计不具有完备性,“一个软件怎么实现”,这个目标的全部逻辑,是设计(文档)和代码共同表述的。

但标准不同,标准的要求是“符合标准的设计在使用上都可以兼容”,这个目标,是全部由标准承载的。如果对比设计和实现,标准看起来是个文档,但实际上它的本质是代码。

换个角度说,如果我完全按你的标准设计一个东西,它就应该可以达成兼容这个目的,你不能留下别的逻辑空间让实现这个标准的人可以做出一个符合你的标准,但不能兼容所有符合要求的使用者的东西来。

比如你定义一个指令集,但你不定义字长,就算你定义了所有的指令,这个标准也无法达成目标。从指令集的角度来说,我们的目标是二进制兼容,但不同字长的指令,显然没有达成你的目的。除非你改变你的策略,说我只要求“源代码兼容”。那是可以,因为你可以用宏一类的东西表示字长。不过定义指令集只是为了源代码兼容,这件事情本身比较白痴就是了。(注1)

对于设计,不定义字长有可能是可以的,这只是编码阶段的一个决策。但对于标准定义,不定义字长是不行的,因为标准的目标没有达成。

我们说标准比设计和编码难得多,因为设计和编码的验证距离很短:你写完代码了,立即上设备运行,就可以验证这个设计大体上是否正确。但要验证一个标准,你要实现一个完整的产品,才能验证它是否是正确的。后者的验证成本是前者的成千上万倍。前者每个测试用例是一段代码,后者每个测试用例是一个产品。

所以,标准的验证,大部分时候只能靠脑子运行,有时可能可以配合各种模型和模拟器,但主体还是靠脑子,人脑和机器比算力,特别是比逻辑算力,完全就不是个个。

这是我们说到的“完备性”,你不能指望把你的设计文档拷贝过来,就想跟我说,你看我实现都实现了,放到你这个纸上谈兵的标准中不是看得起你?——你这个不能当标准,你写得不够多。

也许你争辩你已经写了很多东西了。但这样,我们就需要谈到第二个反向的要求了:我们还会嫌你写多了。

还是回到目标上,我们的目标是“符合标准的设计在使用上都可以兼容”。这个目标隐含了一个假设:将会有超过一个实现者。否则我们就没有必要定义标准了。那么,如果把你的所有实现细节——比如极端一点说,干脆就是你所有的实现代码——都作为标准,兼容这个目的肯定是实现了,问题是,那我们还需要多个实现者吗?

所以,我们需要你去进行需求分析,抓住这个设计的核心需求,然后在一个抽象的层面去实现这个“完备性”,这就是“抽象性”的要求。

还是用CPU指令来举例子,你可以要求指令访问内存有顺序性要求,因为这是写程序的人的需要,但你很可能不应该定义实现者应该有多少级的流水线,MESI算法是什么。因为那个不是需求。

但暴露什么细节和不暴露什么细节有时是个很微妙的事情。比如需求上,使用者不关心你的内存访问速度差异,他的希望是所有内存都很快。但你实现上你有排线的限制,做不到所有内存的速度一样。如果你向使用者隐藏这个概念,号称都是“内存访问”,那使用者就不会把程序和内存放到同一个NUMA Node上,你还是损害了使用者的利益。这时这个概念就必须暴露在你的“抽象”中。

所以“抽象”不是一种固定的套路。“抽象”的目的是为了给不同的实现在实现客户目的的时候有自由度,有演进能力。是一件需要具体问题具体分析的事情。

完备和抽象,就是卡在标准定义两头的一对矛盾,我们评论一个标准是否正确的时候,我们评价它写得太多,是从抽象上来说的,我们说它写得太少,是从完备性上来说的。你不能简单用文字的多少来评价这个多少。前者(完备和抽象)是形而上的判断,后者(文字多少)是形而下的判断。这最终又是一个架构问题。



注1:我们这里特别讨论一下这个“用指令集实现源代码兼容比较白痴”的主题。每个目标可以承载在什么模块上,其实是有规律的。这个规律是逻辑依赖造成的。任何设计,本质上是一种约束。比如你说,加法指令必须编码成一条32位的指令,其中op域应该等于二进制的0110011,可以支持三个寄存器rs1, rs2和rd,还有一些flag用来表示是否进位等等。这强制设计者定义的加法指令必须用这样的方式编码。这个约束同时制造了一个规律(Pattern),由于这个规律的存在,我们实现了一个目的:比如凡是这样发出加法请求的程序都可以在这个硬件上得到加法的结果。

所以,一个模块或者一个接口定义怎么样的约束,就决定它可以实现什么样的功能。这样如果你定义的约束的是二进制编码,然后你说你想达成的目的是源代码兼容。这明显就是白痴行为了。

我深入剖析这个问题,是想给设计者们说明这一点:我们分解模块,分解层次,当你决定了一种约束,你也就决定了你这个模块的属性,它能完成的功能也被决定了。你不要以为你可以把什么功能都放上去。如果你有形而上的思维,这是一件非常明显的事,这也是架构必须具有的基本思维能力。而进行标准的设计,这种架构思维是基本的。

编辑于 04-22

文章被以下专栏收录