源码追踪,是所有开发工程师要迈出的一道门槛。不会源码追踪,或者不习惯于研究出色开源组件的源码,注定会限制自己的成长,也无法广泛借鉴更多的编程技巧和工程思想。不会追踪源码,就像是不会识字的文盲,没办法在海量的知识海洋中遨游与成长。
前两天看到一篇知乎热文:《你都有哪些面试时被虐的经历?》,以极其幽默的方式描述了在技术上被蹂躏的过程。究其原因,毫无疑问是基础不牢固。但为什么会基础不牢固?无非就是在研究、钻研技术时,没有打破砂锅问到底。但我们可以进一步细问,什么叫做打破砂锅问到底?要问到什么程度才叫做到底了?
这几乎就等价于,当你追踪源码时,到底要追踪到什么程度?是追踪到你当前所用框架的实现工具层?还是要追踪到系统API调用层?又或是追踪到你语言本身的实现层次?哪里是个尽头?应该到哪里才是尽头?
回顾上面那篇知乎热文,其实有很多可以琢磨的细节。如果完全以虚心学习的态度来讲,面对拷问细节的面试官,当然是要承认自己实力的不足,知耻而后勇地努力学习。但如果从另一个角度去思考,会体现出一些非常无奈的事实:我自己的工作中就用到这个程度,为什么要问到如此细节?好不容易看了一下这个feature的实现,知道调用了系统底层的某个函数,你偏偏要问,那个系统函数是如何实现的。好不容易知道那个系统函数的实现思路,你非要问,在语言层面是什么特性保证了该实现的合理性。
如此种种,到底何时是个头?我到底应该追踪源码追踪到哪里才算是一个尽头?不是我不想打破砂锅问到底,而是,要问到哪里才是所谓的底(《用于深入思考的一些小工具》)?
这当然不是计算机领域才会存在的问题,每个领域可能都会有这样一个元问题横亘在菜鸟和高手之间。理解清楚这个“底”是什么,几乎就代表了你对这个学科最基本框架的透彻理解。
对于数学来讲,这个问题其实不难解决,因为数学领域天生提供了一个最理想的“底”,叫做公理。凡是你的结论,无法归结为公理的组合,你都叫做没有问到底。任何的证明,要一步步地追踪它的命题链条,一个个命题地追踪,直到某个命题是由公理直接推导出来,否则,你必须继续去探寻这个命题的证明细节。
但即便是如此,也会有很多人在学习数学时,浅尝辄止。例如,谈论闭包时,会涉及到聚点,而聚点又和极限有关系。但很多初学者会止步于聚点这个貌似直观的概念,而无法进一步去追问,我们这里谈论的聚点,到底是什么意思。
也即是,打破砂锅问到底这件事情,存在两方面的难度,第一个是你必须界定好或者说知晓,你这里的“底”是什么。第二,你必须有意识地拷问自己,我的追问链条真的到底了吗?如果这个底,没办法由整个领域里最基本的几个结论构成,那还叫做追问到底吗?
回到刚开始的问题,当我们追踪源码时,我们的“底”是什么?而这个问题,等价于:在计算机领域,什么才是整个大厦的元操作,或者说这个体系的公理体系?我们只需要掌握那几个最小元操作的集合,就能够推演出整个信息科技大厦?如果我们能够找到这个元操作集合,那我们的“底”也就显而易见了,即,你追踪代码时必须追踪到这几个元操作,才可以停止。否则,你就必须像追踪数学的命题链那般,要进一步去研究每个操作的实现,直到某个feature是由计算机的元操作构成为止。
那么,什么是计算机的“元操作”公理体系呢?
如同探寻任何一个“最小存在集合”的问题,要理解最小的必不可少的元素,我们不妨反过来问一下自己,当什么都没有时,我会添加那些元素,来保证我要构建的产品的完整性。也即是:如果我来设计“计算机”这个产品,将会如何设计?
所谓:无需求,不设计。计算机这个产品要解决的问题是什么?或者说,计算机的产品需求是什么?如果按照图灵的可计算来考虑,计算机要解决的磅礴需求无疑是:解决一切可计算的问题。
这个需求听起来无疑是野心勃勃,无处下手。能够承载“一切”可计算问题的解决方案,几乎让你感觉只有引入“上帝”这个外援,才能够满足产品的需求。“一切”是这个需求的变量,这个变量的变化程度是如此之大,以至于我们无处下手。那么反过来,这个需求的不变量是啥?如同字面意思所言:可计算。
什么叫做“可计算”的问题?显然,就是通过“计算”能够推导出来的问题。那什么是计算呢?无疑,它就是数学中最核心的概念,函数:使用一个输入的数字,通过某种变换,吐出一个输出的数字。如此,什么是一切可计算的问题呢?就是一切可以通过“函数变换”推导出来的问题,就可以称之为可计算的问题。
(这里其实还隐形地引入了一个必不可少的前提功能:存储。因为你要接受输入的数字,对其做变化,那么中间必然需要有暂存结果的地方。就好比是笔算需要草稿纸,中间的步骤需要被存储。)
显然,下一个需要回答的问题便是:如果要描述“函数变换”,我们需要提供哪些功能?这或许需要我们再次借助一点数学知识。在数学中,其初等函数是由“基本函数”(如四则运算、三角函数、指数函数等)的有限次组合构成,也即是,有了“基本函数”,只要能够对它们进项有限次的分步操作,便能够形成一个初等函数。而数学中的非初等函数,几乎只限于符号上的推导以及逻辑上的操作。而要把它们具象为一个个可计算的实体,便只能通过初等函数的逼近,这基本上就是“计算数学”所要研究的核心。(例如,无论多么高深的偏微分方程,如果落实到最后的计算上,它必须归集为矩阵的运算,否则就是不可计算的。而如果强行要求解理论上不可计算的问题,也只能通过可计算的解法来做逼近。)
所以,这里的核心便是,有了基本的数学函数,如四则运算、三角函数、指数函数,再把它们做有限次的组合,便能够描述所有的“可计算”操作。稍微抽象一点,如果把基本函数看作是一条条的指令,那么,这些指令的有限次组合,便可以描述所有的计算问题。
再根据计算领域的Böhm-Jacopini定理,任何复杂的指令集组合,都可以归结为三种操作:基本指令操作、条件跳转、循环。而无疑,条件跳转、循环,也可以抽象成“指令操作”。也即是,只要提供了基本的数学指令集,条件跳转、循环指令集,我们便能够描述我们的“计算”行为。
(再一次的,既然是指令集,这同样隐含了“存储”指令集这个大前提。“存储”和“计算”是密不可分的。并且,如果按照这里的“指令集”描述计算行为的思考方式,functional programming中“把函数当做数据”的思想也就自然而然地出现了。为什么说作为动词的“function”可以被当做data来看待?因为它被归结为了一堆指令集data的组合啊:=)!)
回头考察我们最开始的需求“解决一切可计算的问题”,我们所设计的产品,计算机,只需要提供指令集的操作,以及它所隐含的“存储”功能,便可以描述所有的计算问题,便能够承载所有可计算问题的。对应到现代计算机,那便是“cpu+内存”的自然组合。
讨论到这里,似乎已经将需求全部满足了。但如果进一步考虑“解决一切可计算的问题”这个需求本身,其实还有一点缺陷,就是计算和存储,可以承载问题的解决方式,可是,问题从哪里来呢?这有点像是,你有一颗充满智慧的大脑,具备解决复杂问题的能力,但是,如果没有眼睛、四肢、神经系统,你的这个“问题解决器”就成了一个自闭的一无是处的黑盒了。
所以,这里的问题是什么呢?就是你只具备解决问题的能力是不行的,你还必须有同外界交互(即吸收问题、突出解决方案)的能力。这便是I/O(input, output,输入输出的能力)这一模块的不可或缺性。有了I/O的能力,你所设计的计算机产品,便能够得到最大限度的可扩展化,是满足“一切”可计算问题的变量部分的重要元素。无论你的问题源自汽车引擎的参数调试,还是源自商业物流的最优调度,又或者源自图像视频的编辑,只要它们可以归集为“可计算问题”,便能够通过I/O模块,接入到计算机中去处理。于是,“一切”形形色色的问题,都可以通过I/O接入到你设计的这个小盒子中得到解决。这非常像人对问题的处理方式:一切问题,通过五官、四肢的感知,将信息发送给大脑。大脑经过神奇的思考(即计算),便能够输出解决方案。
如此,我们便得到了计算机的元操作公理体系,仅需要三部分:
计算
存储
I/O
这个体系简直漂亮得令人难以置信!而上面所讨论的这个关于计算机的需求设计成果,便是大名鼎鼎的冯·诺依曼架构体系。
回到我们这篇文章的标题,当我们追踪源码时,要追踪到什么程度?答案很简单,你必须追踪到这段代码,仅涉及“CPU基本指令集的操作、内存存储的分配、I/O protocol接口调用”这几个操作的组合时,你便可以说,我的代码就追踪于此了,我算是打破砂锅问到底了。
本文写作得益于许世伟老师《许世伟架构体系》专栏的启发,如果你也有兴趣,不妨通过下面的邀请链接购买,也算是对本文的一种支持,谢谢!
近期回顾
《穷忙与第一宇宙速度》
《人生不是棋局,而是德扑》
《追回丧失的集中力》
如果你喜欢我的文章或分享,请长按下面的二维码关注我的微信公众号,谢谢!
26
更多信息交流和观点分享,可加入知识星球: