仓库源文

.. Kenneth Lee 版权所有 2019-2020

:Authors: Kenneth Lee :Version: 1.0

写程序和写小说的区别


周末去上健身课,教练说我坐太多了,深层肌群在萎缩,虽然肌肉量够,但不平衡类的动 作缺乏肌肉支撑,“最好每个小时起来做一下‘最伟大拉伸’(Forward Lunge Elbow to Instep)),才能避免这种情况恶化下去”。我说这个太难做到了,我写程序或者做设计的 时候,有人靠近我我都会觉得受到威胁,想打人。更别说什么一个小时起来一下了。

教练就问,写程序到底在干什么?为什么要求这么高?

我想了一下,这样解释:写程序就像写小说,想好一个个情节怎么发生,然后把他们描述 出来。但程序和小说的主要区别是:写程序的情节需要考虑墨菲定律——如果一件事情有可 能发生,就一定会发生。

比如,你每天拿把刀在手上玩,划伤自己的手的可能性是千分之一(每天)。那么一个月 这个可能性就是3%,一年就是30%,十年就是97%,考虑100个人,这个事情发生的可能性就 是100%。

写小说呢,对程序来,就是写test to pass。只写关键流程,比如你会这样写:

    | 刻不容缓,金大侠飞身而起,腰间宝剑已到了手上,向前一挥,
    | 击落两枚金钱镖,顺势抢到悬崖边,一手抱住女婴,另一手挺剑急刺,
    | 堪堪挂在了崖边的迎客松上。

写程序呢,这样写就不行了。程序你得这样写:

    | 刻不容缓,金大侠飞身而起,腰间宝剑已到了手上,向前一挥,
    | 此处有三种可能:
    |    1. 击落两枚金钱镖
    |    2. 只击落其中一枚金钱镖,但金大侠已经穿了金蚕衣,所以无妨
    |    3. 其他未考虑之情形,金大侠卒,全书完
    | 若为前述1、2之情形,则金大侠顺势抢到悬崖边,此处有两种情形:
    |    1. 金大侠没站稳,掉下悬崖,卒,全书完
    |    2. 金大侠站稳了,同时,本文设定,女婴被扔出到金大侠出发,
    |       有2秒的时延,金大侠出手时间根据经验应为1秒左右,
    |       有一秒的保险期,故女婴仍可被抱住,如果本条件不成立,
    |       全书完
    | 承前,金大侠一手抱住女婴,另一手挺剑急刺,堪堪挂在了崖边的迎客松上。
    | (为保证金大侠必然能挂住,本书场务已提前检查迎客松和使用之宝剑,
    | 并进行加固,按可承载500公斤进行过验收,所以,此处假定,金大侠必然
    | 可以挂于树上)

写程序大部分时候是写状态机,所谓“面面俱到”,每个状态都对所有可能输入有反馈,这 才是正经程序的一般写法。(注1)

当然,读者应该可以看出,所谓面面俱到,是一个灰度问题,因为任何时候都有其他意外 ,我们这里只是在讨论一个“度”的问题。有一段时间,我看到有团队追求所谓“防御式编程 ”,他们定下类似这样的弟子规式的要求:所以变量都必须初始化。

他们觉得这样就“防御”了,所以,即使这样的代码:::

    def foo(v):
            int a       #这不是纯Python,我做个比喻而已
            a = get_a(v)
            ...

他们也觉得这个a必须初始化。或者v在foo中已经检查过范围了,到了get_a()还要再检查 一次。我经常讽刺这些人:你们要不要这样写代码?:::

    def foo(v):
            int a = 0
            if a != 0:  #double check a 是不是初始化成功了
                    raise Exception("太阳黑子运动过于强烈,请稍后再运行一次")
                    raise Exception("""可能硬件系统受到激烈电磁冲击,
                            前一个raise流程没有起作用,请保存您的数据,
                            然后念经求上帝保佑""")
                    print("Mayday, Mayday,we are under attack...")
                    sys.exit(-1)
            a = get_a(v)
            ...

这它么神经病么,对不对?

说远了,回到主题:小说和程序的核心区别,程序需要运行在所有的变化上,要面对墨菲 定律的挑战。你一个程序要在线运行几个月,乃至几年,可能有数百万的运行实例。任何 一个小概率的事情,都可以发生,你写下的每个逻辑,都要推演它的状态机,把所有的异 常流程都推演出来,然后基于这个来组织你的“文字”,这个就是程序的难处。所以,程序 一点都不好看,这和小说没法比。但程序比小说实用。为此,要付出额外的脑力。

国内自己建立的软件架构少,很多工程师只是使用别人定义好的架构,对这个问题感受不 强烈。所以他们常常习惯这样一种模式:先把主流程写出来,然后开始测试,补漏洞,测 出一个补一个,补着补着,大部分漏洞都看不见了。就觉得可以出厂了。

这种方法看着是种最佳实践,但实际上是靠向写小说,建不出架构来。

因为架构是无法测试的。你写一个程序,要在你的PC上运行,这很容易,但要在所有的PC 系统上运行,这就难了。要在一种配置上运行容易,在各种配置组合上运行,这也更难了 。架构情形无法穷举,不能靠测试覆盖,面对的组合是无穷的,我们只能用脑子去运行它 。要用我们的脑子代替电脑,这种情况下,打断了,就是重新开始。我们在这个过程中用 了大量的人脑的Cache,如果Reset一下,尽管外存还有这些数据,但要重新load回内部 Cache里,要花大量的时间。

加架构级别的功能,基本上问题都不在功能,而在于怎么和过去的逻辑自恰。比如你做一 个类似DPDK的框架,可以在用户态直接访问硬件,这直接看呢,可以提高效率。做这样的 功能,你在Linux会面对什么挑战呢?

功能肯定会做出来的,但你如何解决这些问题呢?:

你需要让硬件访问DMA内存,这是Linux内存管理之外的行为,Linux通过rlimit已经限制进 程可以允许的内存总量了,你这里开了一个口子,如何保持原来的承诺?Linux承诺用户进 程不能访问内核内存,你如何保证你的用户程序不会通过网卡作为跳板直接访问内核的内 存?Linux的cgroup和name space已经保证被加入对应组中的进程只能访问所分配的调度资 源,如何保证你这个进程不会通过网络越过这个限制?Linux禁止没有CAP_NET_RAW权限的 用户进程直接发RAW包,你把网口直接暴露出去,如何维持这个语义?你的硬件在做DMA的 流程的时候,对应的内存被Swap线程交换到磁盘上,你怎么保证两个流程的同步?……

你看,加功能的难度已经完全不在那个基本功能上了,而在于这个新功能怎么加进去以后 不破坏已经存在的那些旧功能,而且你怎么保证你知道哪些旧功能?我肯定连Linus自己都 搞不清楚内核已经对外做了多少承诺了。

我看过太多人在企业内部得意洋洋宣传自己的功能如何“核心竞争力”,如何比开源代码快 ,如何不能免费送给开源社区——你也不看看你那玩意儿人家收不收。

你只是破坏掉一堆的特性,取得一点点优势,这东西能在多大范围内用呢?你在消耗架构 高度,解决一个具体的问题而已,离“写软件”还远得很呐。

这种问题,越往下层走越明显(因为依赖多)。比如老有人跟我说你们的芯片要加这个指 令那个指令的,他们觉得加一个指令就是加这个指令了,最多就是把两个寄存器处理成第 三个寄存器,或者把内存里面的某个数据修改修改。也不想想就算你就加一条简单的 vector_load指令(下面简称vld),你都要考虑一下:这会放开st.ex的锁吗?这个vld的 结果什么时候对所有的核可见?这个vld对IO Region的行为和内存有什么不同?它作用在 两个Region的边界上会怎么样?目标地址不对齐又会怎么样,这个vld在什么权限下可用? 你的向量加载和大部分应用的行列模型是否一致?为了做这个指令增加的ALU或者MEM单元 的利用率有多高?……你觉得加条指令只是保证这条指令运行就行了?

但这也恰恰说明了,为什么国内很难做架构。架构这种东西啊,就和可靠性、安全性、可 扩展性这些东西一样,缺乏真心做事的人很难做成的。你辛辛苦苦做一个安全特性出来, 只要还没有发生安全事故,所有人都只会认为这是负担。当发生安全事故了,也不会说你 好,他们会说:“问题都出来了,说这些风凉话有什么意思,赶紧一起解决问题啊。”问题 是,留下漏洞是个已成的事实,产品已经卖得满天都是,说解决就可以解决的?

架构一样的,架构的所有目的,是为了加功能,加代码的可行性。到加不了的时候,他们 就会说:“问题都出来了……巴拉巴拉”,构架乱都乱了,从一开始加功能的时候,逻辑就是 互相冲突的,现在说要解决,除了重新开始,还能怎样?看看现在这个Cache侧信道问题, 都被修成啥模样了,有人敢说都修掉了吗?这个是你做OoO的时候把Cache当做透明的天生 制造的问题,OoO的好处你都宣传出去了,现在就没有什么办法好吧。只能有什么问题补什 么问题喽。

更大的问题是,就算你重视架构,这东西又没有样子看,谁知道会不会白干了半天,一样 架构不好呢?

这真是一件困难的事情,但无论如何,至少你得明白这是件什么事情,不要那么多“显然” 好吧(参考::doc:狂人日记读后感——名称空间囚笼

最后,总有人认为所有东西都需要有一个结尾,觉得我没有点题,我就写个结尾吧:世间 安得两全法,不负如来不负卿?

注1

这个例子其实也说明了“设计文档”和“代码”有什么不同。

    | 金大侠飞身而起,……,抢到悬崖边,……接住女婴。

这是代码,这是最终的呈现。设计文档是什么呢?设计文档是这样的:

    | 令女婴被人抛出为时刻t1,抛出的初始速度范围为[n1..n2],
    | 然则其彻底离开悬崖之间的距离可算得为[m1..m2];
    | 令金大侠预判出女婴要救的时刻为t2,t2-t1=DeltaT,则金大侠剩余时间为t3;
    | ……
    | 由此可得,金大侠的初始加速度只要达到a,即可满足情节要求。

这叫“设计文档”,设计文档不是代码的粘贴复制,设计文档是一个独立的分析逻辑,是代 码之外的其他逻辑链,用于保护代码的可靠性的和演进性的。那些总要等写完代码才能写 出设计文档的人,就没有搞清楚怎么写程序。他们以为写程序是写散文呢。