这个学霸疑似巨额知识来源不明 - 第107章 三十二个证人

上一章 目录 下一章
    江临原来的计划是,是把这些长远问题全部封存,留待即將到来的废土时间去解决。
    但是回到家里,夜深人静,整个人一空閒下来,就还是忍不住把陈启明给的压缩包拖进电脑的工作目录里。
    microkernel_rank_sort_baseline.zip。
    右键,解压。
    文件夹在屏幕的树状图里一层层铺开,其內部结构的整洁程度,比江临原本想像的要乾净得多。
    根目录下躺著三个子文件夹。
    江临点开/baseline
    里面赫然是三套经典的微內核基础算法的基准实现:rank5(五元素求秩)、sort5(五元素完全排序)、top3_of_8(八元素选前三)。
    每一套算法,陈启明都严谨地给出了两份截然不同的代码。
    一份是没有任何底层优化的纯c语言可读版本,用来锚定逻辑正確性。
    另一份,则是陈启明团队经过多年打磨的手写极限优化版。
    江临扫了一眼,后者的代码里充斥著晦涩的编译器內联提示,强制循环展开,以及少量依赖特定cpu令集的平台相关写法。
    江临退出来,点开/verify。
    里面是几十个详尽的验证脚本,涵盖了各种极端的边界测试用例。
    最后是那个最让系统工程师头皮发麻的/bench(性能测试)文件夹。
    里面只有一份原始的.csv格式的性能计数统计表。
    这张表是在三台底层微架构完全不同的企业级伺服器上,经过漫长的马拉松式压测跑出来的。
    江临將表格放大。
    行末密密麻麻地標註著测试环境的绝对变量:cpu 型號,微码更新版本號,l1/l2缓存的命中率,分支预测失败率,以及频率是否强制锁定。
    频率强制锁定这一项让江临颇为讚许。
    现代商用cpu都会狡猾地根据温度和负载,动態调整时钟频率。
    在进行这种纳米秒级別的內核代码测试时,哪怕cpu的频率发生了一次轻微的抖动,都会导致测试出的时钟周期出现巨大的噪声,从而彻底掩盖掉代码优化带来的那一两纳秒的真实提升。
    陈启明那帮人不仅清楚这一点,还暴力地在bios层面锁死了频率。
    光这一张数据表就无声地证明了,陈启明这帮人,是真正懂行的老手。
    他们已经把人类能够手动控制的变量,极致地推到了物理的极限。
    这就有意思了。
    江临决定先从看起来最最基础的sort5开始解剖。
    题面很简单:给定五个隨机的整数,將它们按从小到大的顺序排列好。
    任何一个学会写for循环和if语句的新手,都能在短暂的三分钟內,交出一段基於两层嵌套循环的冒泡排序或者插入排序代码。
    当然,这仅仅只是能跑通的玩具。
    陈启明这种长期和底层性能打交道的人,要的当然不是这种充满分支跳转,在现现代cpu流水线里到处添堵的低效正確。
    而在去追求那个极致的快之前,江临的数学直觉告诉他,必须先解决一个哲学问题。
    【怎么用严谨的数学逻辑,去证明一段晦涩的排序代码,对全宇宙所有可能出现的输入组合,都百分之百地正確?】
    最直观也是最笨的办法,是暴力地把这五个数的全部大小关係组合,挨个枚举一遍。
    那么五个数到底有多少种不同的全排列?
    简单的组合数学。
    5!
    一百二十种。
    这个数字小到机器一瞬间就能跑完並验证。
    这让江临难免就想起了那块砖。
    做江氏砖的时候,刻进他骨头里的数学动作,就是把一个庞大到趋於无穷的边界状態空间,精妙地压缩成机器能穷举人能覆核的有限状態形式。
    一百二十確实不大。
    可如果是频繁出现在高级资料库索引中的,排八个数,排十六个数呢?
    8!
    四万种,机器依然能够轻鬆秒杀。
    但是到了十六个数。
    16!
    二十万亿级別。
    对普通程序来说,这已经不是多跑一会儿的问题,而是足以把最笨的全排列验证拖进泥潭。
    如果mps框架建立在这种愚蠢的全排列穷举上,它將迅速死在起跑线上。
    江临转了一下手中的原子笔,大脑的记忆宫殿开始高速检索。
    很快,他放下了笔,在键盘上敲下了一行学术检索词。
    zero-one principle sorting network
    (排序网络:零一原理)
    他其实早在废土时间里,在啃噬那些浩如烟海的计算机科学巨著时,就已经做了相关的知识储备。
    只是在这之前,它仅仅只是停留在离散数学课本里一条定理这样的程度上,並没有迫切的用武之地。
    而现在,江临认为这条优美的定理可以成为mps-kernel的第一块地基。
    在理论计算机科学中,零一原理可以高傲地宣称——
    一个由比较—交换操作固定地构成的比较网络,它是一个绝对正確的排序网络,若且唯若,它能够极其正確地將所有仅仅由数字0和数字1构成的有限输入序列,完全排好序。
    这条定理的杀伤力在於,它无情地斩断了无限与有限的边界。
    不会要求你去检验全部的120种排列,也不要求你往这段排序代码里餵进任何一个带有具体数值的实际输入。
    它只要求你检验那些由0和1拼凑出来的二进位序列。
    对於sort5(五个位置),根据零一原理,可以看做每个独立的位置上,要么是绝对的0,要么是绝对的1(非 0 即 1)。
    於是,它的验证空间一下子就被不可思议地坍缩成了2?=32。
    只要你写出的这段代码网络,能够正確无误地把这三十二个全由0和1构成的序列排成单调递增的形状
    那么,神奇且绝对的是,这段比较网络,对任何来自同一全序类型的五个输入都正確。
    原本的微小的120,被再次降维压成了32。
    如果是十六个数,原本极其恐怖的二十万亿,也能压缩成六万五千个。
    一个微处理器闭著眼睛都能跑完的数字。
    更重要的是,这三十二个简陋的0-1序列,还不是软体工程里充满玄学的抽样测试,也绝对不是测试工程师绞尽脑汁想出来的边界测试用例。
    它在数学意义上,是不留死角的完备检查。
    只要顺利地跑通这三十二个简单的序列,这段代码底层的绝对正確性,就被焊死在了真理的铁板上。
    这种利用抽象的数学定理斩杀无限状態空间的快感,简直太美妙。
    江临立刻在mps-kernel根目录下新建了一个python脚本文件。
    verify_sort5_zero_one.py
    引入itertools.product,暴力地生成全部確定的三十二个0-1序列。
    对每一个独立的序列,机械地跑一遍外部掛载的候选排序网络代码。
    严格地检查输出序列是否满足单调不减。
    三十二个严苛的证人,只要全部通过,程序就会返回绿色的【proven valid】(证明有效)。
    而只要有任何一个微小的序列无法通过,程序就会將那个反例直接吐出来,无情地枪毙这段代码。
    三十二个全过,返回已证明。
    任何一个不过,把那个反例吐出来。
    江临敲下回车键,拿陈启明团队提供的那份原始的 /baseline/sort5_pure.c 暴力地跑了一遍夹具。
    绿色。
    三十二个0-1证人,在严密的数学法庭上,没有一个翻供。
    正確性的地基,有了。
    接下来的是神仙打架的活:在所有正確的排序网络里,找出最好的那一个。
    江临开始把他在铺砌几何中经过千锤百炼的mps搜索骨架,巧妙地往这个代码问题上套。
    做砖时,状態是一块局部铺砌,动作是放下一块新砖,胜利条件是排除所有拓扑逃逸。
    现在呢?
    状態是一段已经写下的比较器序列。
    动作是往后追加一个比较器,比较两个位置,把小的甩前,大的甩后。
    胜利条件,是这段序列让三十二个证人全部点头。
    结构一模一样。
    只是把几何换成了指令。
    他写了一版搜索:从空网络出发,逐个追加比较器,每加一个就用零一原理剪枝,留下还有希望的分支,砍掉已经走死的。
    搜索先跑长度八以內的全部候选。
    mps没找到任何一个能让三十二个0-1证人全部点头的网络。
    然后长度放到九。
    第一组通过的网络出现了。
    江临把结果和knuth里那行s(5)=9对上。
    这才意味著:五个元素排序,九个比较器不只是能做到,而是最低限度。
    教科书上写了几十年的那个数字,被他这套从一块砖上长出来的框架,从头独立搜了出来。
    框架,迁移成功。
    江临靠回椅背,看著屏幕上那九行乾净的比较器。
    一瞬间,有种成了的轻飘飘然。
    可这点轻飘只浮起了几秒,就被他自己一把按了下去。
    重新调出陈启明那张流血的火焰图。
    median7_fast、rank5_inline、top3_window……
    陈启明早就说过,人工优化的红利,已经被那群长期泡在底层代码里的人压得很薄了。
    意思是他团队现有的代码,比较器数量大概率早就是九,或者贴著九。
    而他用高维数学和搜索算法搜出来的最少九个比较器,对陈启明而言,根本不是什么新东西。
    江临盯著自己那九行结果,眉头慢慢拧紧。
    他犯了一个新手才会犯的错。
    下意识地去优化了那个最乾净最好定义也最容易写出適应度函数的组合目標——比较器的数量。
    可陈启明要的,从来不是数学意义上的数量最少。
    而是在他那几台具体的伺服器上,跑得最快。
    数量最少和跑得最快在真实的物理世界里,根本不是一回事。
    江临想起陈启明在报告厅留下的最后一句话。
    他的搜索空间里,充满了正確的低效垃圾。
    此刻他更深一层地明白了。
    九个比较器,是按数量排的。
    但在一颗真实的cpu里,决定一段代码快慢的,从来就不只是指令的数量。
    而是这九个比较器之间的数据依赖链:哪些必须排队等前一个算完,哪些可以並排著同时算。
    是它们落在乱序执行引擎的哪几个埠上,会不会挤在一起抢资源。
    是每条min、max、条件传送指令的延迟和吞吐。
    是寄存器够不够用,会不会被逼著往內存里倒腾。
    同样九个比较器,排成一条又细又长的依赖链,和排成五层能並行的浅塔,在流水线上的表现,可能差出一大截。
    比较器数量少,和比较器深度浅,是两个不同的目標。
    陈启明真正想要的目標,那个在型號a的伺服器上榨乾最后一滴性能,把延迟压到最低的最优解,藏在极深极脏的硬体底层里。
    和微架构死死绑在一起。
    换一颗不同代际的cpu,哪怕只是从intel的skylake换到zen 3架构,缓存延迟和指令埠的微小变化,都会导致那个最优解瞬间跌落神坛,变成次优。
    江临盯著那张三台机器的perf表,一下子意识到,要找到那个真正適应物理世界的最优解,单靠数学证明是不够的。
    他必须在他的mps框架里,接入一个代价模型。
    在海量正確的候选网络里,不仅看数量,还要看深度,看並行度。
    甚至到最后,他需要写一个自动化脚本,把筛选出来的,看起来很有希望的几百个变种网络,一个一个编译成机器码,扔进真实的物理伺服器里去实测,打分,筛选。
    每一次实测都伴隨著作业系统调度的噪声、缓存预热的波动。
    他得每个候选跑上一百万次,取中位数,取99分位延迟,做枯燥的统计学对抗。
    根本不是一个晚上,甚至不是现实里的一年半载能够跑完的事。
    意识到纯算法在底层硬体面前的局限性后,江临的脑子反而冷静了下来。
    顺手点开rank5,这个逻辑能不能也用现有的框架碾压过去。
    题面:给五个数,不要求全部排好,只问输入窗口中心位置的那个数,在这五个数里到底排第几。
    几乎是出於惯性,本能地就想把刚写好的零一原理验证脚本套上去。
    不过还好下一刻,他就及时把自己摁住了。
    零一原理管的是排好没有,是全局的单调性。
    可rank5要的根本不是把所有数都乖乖排好,而是要给每个数,或者特定的某个数,贴上一个精確的名次標籤。
    如此,它的正確性判据不再是问最终的输出序列有没有单调递增,而是变成了问:那个原本在输入中心位置的数,它头上顶著的名次,到底是不是正確的名次?
    如果强行把输入全换成0和1会发生什么?
    比如原始输入是【10, 50, 30, 20, 40】,中间那个数是30,它排第三名。
    如果粗暴地二值化为0-1序列,可能会变成【0, 1, 1, 0, 1】。
    在这个二值序列里,有三个1,两个0。
    原本该区分开的绝对名次情形,因为数值维度的坍缩,直接撞成了一团烂泥,根本分不开谁是真正的第三。
    零一原理在这里,不直接成立。
    或者说,它失效了。
    rank5≠排序网络。
    验证rank5,必须回到相对大小关係(如置换群)的层面,而非单纯的0-1输入空间。
    它的底层结构更接近一张由偏序关係构成的,动態更新的两两比较矩阵。
    这又是一个新坑。
    江临的目光扫过最后一个问题:top3_of_8。
    从八个数里,挑出前三大。
    这个问题同样暗藏杀机。
    对比较网络形式的top-k选择,类似的零一检验可以使用。
    八个位置,就是2^8=256个0-1证人。
    但前提是,你必须先和出题人把正確的语义定义好。
    什么叫挑出前三?
    口径a:只要最大的三个数,落进了输出数组的前三个坑位就行,这三个数內部是乱序也无所谓?
    口径b:还是说,不仅最大的三个数要进前三,而且这三个数之间,也必须严格按照从大到小排好?
    如果口径没有定死,那么mps框架搜索出来的就是空中楼阁。
    標准宽泛一分,搜索空间就呈指数级缩小。
    標准严格一分,依赖链就不可避免地加长。
    三个题,三套完全不同的脾气,三种不同的验证体系。
    江临没有急著去写代码解题。
    因为方向比速度重要一万倍。
    他打开一个文本编辑器,像一个在雷区插旗的工兵一样,把每一套题目的逻辑边界、验证难点、硬体耦合点,一字一句地记进文档里。
    天快亮的时候,江临整理出了一份能交给陈启明的东西。
    第一样,是基於零一原理的验证夹具。
    这是一个杀器。
    它能对陈启明团队里任何人写出的任何一段sort5候选代码,给出一份绝对穷举的,与底层cpu架构毫无关係的,在数学上不可辩驳的正確性证明。
    以后,谁要是交上来一段自以为绝妙的位运算代码,不用爭吵,跑一下这个夹具。
    三十两个0-1证人当场表態。
    不过关的直接打回。
    这就是地基。
    第二样,是一份边界说明文档。
    他把陈启明面临的混沌问题,一分为二。
    第一层,是纯组合层面的理论目標。
    比较器的总数能不能更少?
    依赖链的层数能不能更浅?
    对sort5这种规模的小问题,江临承诺,他可以用他的mps框架,把这一层的所有理论最优解完整摸清,连根拔起。
    但他也直言不讳地指出,面对更大的n(比如16、32),哪怕是mps,依然会在算力面前发生组合爆炸。
    必须引入启发式剪枝。
    第二层,是与具体物理硬体绑定的微架构最优。
    也就是在陈启明指定的某一台伺服器上,把延迟压到最低。
    这一层,需要陈启明的实测夹具,需要海量的候选代码去真机里跑统计。
    工作量极大,需要时间。
    这绝非现实世界里加几个通宵的班就能够解决的问题。
    他会把整套mps-kernel的框架,搬进废土里去打磨。
    在那里,他有几十年,去把搜索框架,剪枝策略,代价模型一寸一寸地养肥,调通,跑稳。
    当然,前哨站里那台工作站,是他从现实背进去的特定机器。
    它跑出来的最快,只是那台机器上的最快。
    陈启明的目標,是另外几台型號完全不同的伺服器。
    微架构一换,最优解就不一样。
    所以,废土那几十年,他真正能带回来的,不会是某一段在废土硬体上飞快的代码。
    而是一套被反覆打磨调试,验证过的超级搜索框架,一套成熟的代价建模方法论,外加一个经过数学验证和现实实测双重筛选的候选网络结构库。
    如此计划妥当,江临將打包好的文档和脚本重命名为j_mps_phase1_deliverables.zip。
    窗外天光已经泛白,江城那场憋了一夜的雨,到底没落下来。

添加书签

搜索的提交是按输入法界面上的确定/提交/前进键的
上一章 目录 下一章