仓库源文

.. Kenneth Lee 版权所有 2019-2020

:Authors: Kenneth Lee :Version: 1.0

限制管理


本文讨论模块分解的策略问题。

想起要写它,是上周写这个推演时引入的概念:

    :doc:`从CPU和TPU的不同语言抽象看抽象原则`

当时讨论到这样一个现象:TPU/NPU的程序员给了一个请求序列给TPU或者NPU,如 果要求指定得太多,编译器就失去了优化的机会了。这个问题无处不在,也不需要一定是 TPU,我们用给一些更简单的例子来类比。比如说,你有这样一个执行序列:::

    def add_all(a, b, c):
      x=a
      x+=b
      x+=c
      return x;

其实你关心的是把三个输入参数加在一起,但你的执行序列要求比这个要求多出了很多东 西,比如:

  1. 你指定了临时变量

  2. 你指定了执行顺序(先加a,b,再加c)

这些可能不是并非你的需求,但执行者不知道,它不一定可以自作主张帮你自动把临时变 量去掉,或者把你的执行顺序打乱——说不定你别有深意呢?比如x是个寄存器变量,这样加 比直接把两个内存的值相加快很多——反正只要体系结构足够奇葩,什么考量都有可能的。

从这里可以看出来,需求提出方提供的“指定”越多,提得越精确,执行方的效率就越低。 如果软件给编译器提供的执行要求仅仅是add_all(a, b, c),编译器就有余量,只保证a, b, c相加的结果就可以了,如果软件直接提供了“如何计算这个a, b, c的方法”,编译器( 或者说CPU),就不能怎么样了,只能一步步计算,也许明明CPU可以一条指令就搞定3个数 相加的,也只能变成两个两个来完成了。

一般模块也普遍存在这种情况,比如你要压缩一个文件,你直接给我整个文件的路径,压 缩模块说不定可以直接从OS内部把数据喂给硬件加速器,然后流式回写到磁盘上。但你非 要把它从文件系统读到用户态,再分成一块一块喂给压缩模块,压缩模块就只好用CPU一块 一块给你单独压(由于哈夫曼树在块之间不能共享,这个压缩率也会下降),这个效率就 低了。

所以,我们首先有第一个认识,高层设计,并非越精确越好的,精确意味着更多的限制, 更多的限制破坏了下一层设计的自由度,导致下一级无法进行优化。

所以,“极致的最自由的”高层设计是仅提需求。比如我要做一个压缩软件,收集了需求, 直接设计成:“实现命令compress,把stdin的数据流压缩到stdout”。这样,压缩命令的实 现者就有最大的自由度,根据最好的方法实现整个压缩功能。

问题是下一层设计怎么办?你不可能一次就实现到具体的代码,也不可能仍告诉下层“实现 命令compress...”,那么下一层的切分在哪里?这是本文主要想讨论的问题。

我们从另一个层面考量这个问题:每层设计,本质上都是逻辑链(“先XX,再XX,遇到XX则 XX……”),而逻辑链是没有根的,整个逻辑链最前面的节点(“因为”),其实都是“限制”: 因为用户要求压缩stdin,所以我们必须读入stdin的数据。读入数据需要缓冲区,所以我 们需要缓冲区,但因为我们使用的平台的缓冲区是有限的,所以一次读入的数据不能超过 4M,因为用户的提供的输出可能超过4M,所以每次读到的数据超过4M后,就要启动压缩算 法……”。你看,其实我们的逻辑链全部都是建立在“限制”上的。

而前面提到的下层软件的优化,其实也是限制:本来我把要相加的3个数都给你了,你不能 三个一起加,非要两个两个地加,这是你下层模块的“限制”。

好了,现在的问题是,限制其实一直在那里,我们是在上层认知这个限制呢?还是在下层 认知这个限制?答案就回到原来的高内聚低耦合这个问题了,也就是说:相关的限制,一 体处理,接口承载接口双方的共同限制。

所以呢,我推演了半天,看来是什么结论都没有。但它有几个边界效应,它让我们认识到 :

  1. 我们聚合一个模块的关注重点不是聚合它的实现,而是聚合它的限制

  2. 我们决定一个逻辑在某个模块中实现,不是因为它“名义上”属于这个模块,而是它的逻 辑链根植于这个模块所管理的限制。只有这样,我们才不为整个系统引入多余的“限制” ,导致系统最终自相矛盾,无法再加入新的需求(本质也是“限制”)

  3. 当我们进行接口设计的时候,接口制造的“限制”,必须是接口双方逻辑链中的限制

这样,我们就可以解释上一个文档中提到的那些接口问题,例如为什么我们不能接受直接 把线程作为TPU的输入——因为TPU内部内存如何使用,是TPU内部的限制,是TPU的灵活性的 范围,CPU本身提交需求的时候,并没有这个限制,所以,在接口上引入这个限制,是不利 于接口发展的。