这篇小作品从几个角度解释为什么确定性是工程技术的核心。
先从LMD开始。
学过电路的同学可能还记得“集总电路(Lumped Circuit)”。在经典的MIT 6002课中,教授不断强调的就是“集总事物守则”(Lumped Matter Discipline)。
从物理的角度讲,我们可以用麦克斯韦方程组描述一切电子电气系统,并对系统进行计算模拟。但在工程实践中我们却不能这么做,因为用计算机求解这样一个系统,时间复杂度实在是太高了。换句话来说,如果大家都用麦克斯韦方程组设计电路,其实什么也设计不出来。
比如电流流经一个环路,会在环路周围产生磁场;如果是交变电流则会产生交变磁场,最终会有一部分能量以电磁波的形式向外传播。而一个简单的电子系统,比如一台手机,内部环路数量在亿级别,工作频率也不低,环路之间自然也会互相以电磁波的形式传递能量。要对这样一个电磁系统建模仿真,所需的数值计算量大概是10后面加40个零,当然是不实际的。
但是人类最终还是造出了手机。这主要就是因为工程师们坚持了“集总事物守则”。
在上一个例子中,手机中的所有元件、所有环路之间都会相互影响。如果不考虑环路的互相影响,把元件都看成独立的个体呢?
以电阻为例,我们可以规定电阻必须遵守欧姆定律,即 I = U/R 。实际通过电阻的电流肯定要受周围物理环境的影响,但我们可以忽略这些影响,强行要求遵守欧姆定律。
然后我们再强行规定,环路必须遵守基尔霍夫电压定律(KVL),即任意环路电压之和为零。这忽略了实际电路中的互感现象,即环路与环路之间的互相影响。
经过我们的强行规定,问题的复杂度降低了很多。现在元件只和与其相连的元件发生作用,而不是像原来那样和所有其他元件(以及外部环境)都发生作用,作用的种类也大大减少了。然后我们就可以在PC机,而不是超级计算机上,利用节点法求解一般的电子电路了。
上面这个例子向我们证实了:要高效地解决工程问题,就要尽量消除元件之间的相互影响。相互影响的因素越少,实现某个工程目标的方法就越简单,系统就越容易设计。(其实社交生活也是一样的,大家都希望自己的各种社会关系、活动之间不要相互影响,以便活得轻松些)
因为我们要求电阻必须遵守欧姆定律,所以电阻厂家就专门生产严格符合欧姆定律的电阻;我们要求环路之间不能相互影响,所以我们经常在电路设计中增加屏蔽和缓冲。总而言之,电路和电路元件的特性越确定,工程师的工作就越轻松。
如果要再进一步提高确定性呢?于是人们定义了一类比较特殊的电路,这些电路几乎总是工作在饱和状态,用高低电平表示和传递信息,称为数字逻辑电路。它们的行为几乎完全不受环境干扰,而它们的工作规律也从基尔霍夫电路定律(信号可以顺路径来回传递;信号可以取实数值)简化到了逻辑运算(信号只能朝某个方向传递;信号只能取逻辑值)。
提高确定性当然有很多好处。在线性(模拟)电路中,我们虽然强行规定环路之间不会相互影响,但这种影响肯定是存在的,区别只是程度高低而已;而元件本身的特性也总是很难完美。到了数字逻辑电路中,这种影响就几乎可以忽略不计了,元件特性也非常容易保证完美,这就是为什么数字逻辑电路的规模可以比模拟电路的规模大成千上万倍。
正是因为元件确定性高,互相影响小,我们总是可以把一个大的设计拆分成许多小的设计,其中每个小设计的特性都是确定且无须怀疑的。在模拟设计中,要保证1000个电阻的误差在0.01%以内是一件非常奢侈、只有高端设计才会考虑的事情;而在数字设计中,由1,000,000个逻辑门组成的、连续工作10年不出错的芯片,比快递费还要便宜得多。
我把这种特性概括成系统的“确定性”。这个性质可以这么理解:
我实在是想不到什么好的表达方式了。直接看下面的例子吧。
比如我们要写一份文档,考虑下面两种途径:
大家通常会选择第一种方法,因为第一种方法的确定性最高:只要把DVD放进光驱,基本上99%的电脑都可以一次性成功。如果选择第二种途径,你会先遇到一些驱动问题,然后遇到软件兼容性问题,最后还可能需要在终端敲命令。而且每一台电脑上的安装过程可能还不一样,这边可以那边未必就行。这都是令人难以接受的成本。如果是比写文档稍微复杂一点的事情,绝大多数人拿Linux是毫无办法的。
Windows的高确定性,源自微软持续不断的投入,当然也给微软带来了持续不断的收入。大家需要的正是这种千篇一律:只有一成不变的东西,使用和维护的成本才会低。
不过有些事情大家更乐于在Linux上做,比如数不胜数的开源软件。开源的好处就是:对于那些微软并没有花时间解决的问题,开源开发流程的确定性要高得多。对于开发者来说,在Linux上开发遇到的任何问题,只要花一些时间,都是可以解决的;而在Windows上遇到的同类问题,并没有这个保证。
但我们也可以看到,越来越多的开源软件被推荐在Ubuntu或者CentOS上运行(比如TensorFlow, Docker等),因为这些系统的保有量大,版本稳定。对这些软件而言,操作系统就是它们的基本组成元件,这个元件的特性自然是越确定越好的。
一个软件系统的基本组成元件是操作系统、语言(编译器或者解释器)和库。正如电子设计要假设元件是理想且确定的,软件设计也要假设操作系统、语言和库是理想且确定的。如果malloc()返回的不是指针,如果for循环的执行顺序发生改变,如果函数的入口名称改动了一个字,软件都是无法正常工作的。
举软件的例子,是因为这方面我比较熟悉,而且互联网软件最近10年的发展也比较好地反映出了大家对确定性的要求。
简单的说,程序员和网管都希望世界上只有一种操作系统、一种语言、一个代码库,这样就很容易保证高确定性,省下很多功夫。然而现实是我们需要不同的语言,不同的操作系统,以及浩如繁星的库。怎么办呢?你可能会说没有好的解决办法,而Google作为目前世界第一的软件公司,给出的答案如下:
这样做当然是有代价的。比如静态链接会极大地增加编译时间,需要靠分布式系统加速编译;全局代码库非常庞大,IOPS极高,需要建设成分布式系统;等等。但这也带来了很多好处,比如静态编译的程序不需要目标系统安装动态运行库(从而也就不会因为缺少运行库或者运行库版本不正确而浪费大量时间),而全局代码库则避免大家在不同项目之间来回黏贴代码。
因此一个Google程序员需要掌握的基本组成元件,不仅数量上很少,而且确定性也非常高,在设计功能强大的系统时就比其他公司的程序员轻松得多。可以说这一系列设施使得Google在软件工程的沙场上战无不胜。
运用同样的逻辑,我们可以解释软硬件工程领域各种各样的现象。
问:为什么C语言流行?
答:在X86上是这样,在ARM上也是这样(消除底层架构的影响)
问:为什么PHP流行?
答:(最早的时候)直接嵌入在HTML里面,HTML是这样,PHP出来也是这样
问:为什么Java流行?
答:在Windows上是这样,在Linux上也是这样(消除操作系统的影响)。招一个程序员是这样,招100个程序员也是这样(消除人事因素的影响)
问:为什么云服务器流行(即便比自己维护服务器贵)?
答:一台是这样,20台也是这样
问:为什么容器技术(Docker, Kubernetes)流行?
答:在本机是这样,到了云端也是这样(消除有和没有运维团队的区别)
问:为什么STM32流行?
答:毕业论文是这样,上班也是这样
问:为什么Web应用要连数据库,而不写本地文件?
答:一个实例是这样,20个实例也是这样(消除单进程和分布式系统的区别)
可以说,工程领域大量的工作都是在解决确定性问题。毕竟我们是人类:我们总是希望用尽量少的脑力解决尽量多的问题,而确定性可以帮助我们实现这一点。
来自不同地区的人使用不同的语言,仅仅因为历史原因。为了提高协作的效率,世界上的语言数量越来越少,而说主要语言的人口则越来越多,这个趋势正变得越来越显著。正如上一节末尾谈到的:追求确定性是人的本性。
展望未来,软件工程师们也许会使用几种固定的操作系统(很可能是云上运行的某种极简系统环境,参考CoreOS)来运行软件,几种固定的语言(很可能是Python和JS)来设计软件,正如我们今天会用几种固定的方法和工具来设计电子系统一样。