.. Kenneth Lee 版权所有 2020
:Authors: Kenneth Lee :Version: 1.0
分层抽象
今天给别人提了一个设计意见,但估计他没有理解我的意思。我这里写一个补充说明。
当我们说某个设计“带入了细节”,进而说它“不正确”。我们说的是一个“错误”,不是可有 可无的“说得太多了”。
我找一个通用的例子来说明这个区别,比如ARM CPU上,提供了一个寄存器MPIDR_EL1,它 的定义是这样的:
.. figure:: _static/mpidr.jpg
这个寄存器每个CPU核一个,说明自己在整个CPU互联架构中在什么位置,比如属于哪个物 理核,哪个DIE,哪个封装(Socket)等。但我们注意一下上面这个定义,它里面没有给出 哪个域是物理核,哪个域是DIE或者哪个域是Socket。它只给出了一个抽象:Aff0-3。至于 Aff0-3如何和那些什么物理核,封装对应,这是下一层的事情。
这就叫抽象。抽象的作用是把逻辑信息分层,让一部分程序强行看不到部分信息,从而那 部分信息更改了,那个程序仍可以成立。好比这样:
.. figure:: _static/mpidr软件构架1.jpg
在一个硬件和软件实例中,这样分层其实没有什么用的,但如果你有很多个硬件实现,就 完全不同了:
.. figure:: _static/mpidr软件构架2.jpg
这里,如果概念上没有这个分层,右边的软件就没有上面那个公共的部分,你就必须给每 个硬件都写一份上面那个公共部分了。
所以,我们其实付出了很大的代码才能维持这种关系和获得这种收益的。软件很大部分的 设计压力,都是在做这种抽象设计。这种设计是非常难的,因为对于ARM来说,它认为这个 寄存器的作用就是用来做调度距离判断的,Aff0一样,我们就认为两个CPU靠的很近,进程 在这两个CPU之间迁移,门限可以很低,如果Aff1也一样,门限就高一点了……如此类推。
但如果你使用这个细节的人不是调度器,比如你需要知道哪部分CPU核属于同一个封装,所 以可以整体“拔走”。你这个程序需要的就是确切的SocketID,这时,你的程序“觉得”你的 程序对硬件A很熟悉,所以你直接读了Aff2,并且认为这个就是SocketID,并且你这个程序 的概念在“只认知Aff0-3不知道它们表示什么的软件”中广泛传播,那么,辛辛苦苦写的这 个抽象层就废了。因为基于抽象层写程序是很难的。本来我明明知道两个CPU在同一个 Socket内,进程迁移成本是10ns,我程序很好写的。现在要变成“两个CPU在同一个Aff2内 ,迁移成本为w”,这个程序很不好写的。但写成这样,以为我达成在各种情形下都能跑这 个“成就”了,结果这个软件里面出了叛徒,还是绑上了硬件A的具体细节,这些工作就都浪 费了。
所以,高以下为基,贵以贱为本。抽象不抽象,完全是在细节的支撑上决定的,不是你写 一个叫“封装层”的东西就叫“封装”的。
如果回到上面这个问题,你需要SocketID,你就需要用一个SocketID的封装,要你抽象的 所有实体,都用这个封装来给你承诺,否则你就没有封装,你就应该把这个概念圧在下面 的硬件实现感知的模块上。
在这种情况下,我说你说多了,我可不是指你在锦上添花,我是说你做错了。在上面这个 例子中,你甚至可以收缩你的范围:比如所有供应商A的Aff2,都必须表示SocketID。然后 限制你的抽象驱动,只用于供应商A,这样你直接用Aff2也行,但这需要一个“设计定义”。 这个要完整写出来,其实我需要你写的东西更多,你别以为我表扬你做得“超出预期”。