多种回测配置。
每一天,每一个行业,每一个状態標记、每一个数据版本,都要切出一层层的横截面进行分组排名。
於是,这些微小的小动作被嵌套在庞大的循环网里,被反覆调用。
江临把调用栈里耗时最高的那个函数次数列印了出来
七千八百四十二万一千九百零六次。
看著这个天文数字,他沉默了十几秒,然后在项目的audit_log.md里,他敲下了这样一段话。
真正拖慢整个复杂系统的,从来都不是偶尔出现的大山,而是每天必须被搬运七千万次的小石头。
写完这句话,他停顿了一下。
为了让將来可能接手这份审计报告的平庸工程师也能看懂,他又补了一段更通俗的解释。
一次给五张试卷按分数从高到低排序,对任何人都不难。
难的是,系统要求你一天之內,把给五张试卷排序这个动作,重复七千万次。
標准库里的排序算法在计算机科学上被证明是非常优秀的。
但它们优秀的前提是通用。
標准库就像是一套占地几万平方米的大型自动化物流分拣中心。
它可以处理一万张试卷。
可以处理一百万个包裹。
可以处理带有各种奇怪对象的复杂数据结构。
它强大,通用,绝对可靠。
但如果你的流水线上,每次送过来的永远只有五个小包裹,而且每天要送七千万次。
那么,你每一次都去启动那套耗电巨大的大型物流中心,让传送带空转,让机械臂寻址,去执行庞大的分拣逻辑,
这就是不可饶恕的浪费。
在底层代码的视角里,这种浪费体现为,为了通用性而保留的复杂的函数调用开销。
为了处理多態而进行的动態类型检查。
为了兼容不同数组长度,標准流程里保留了大量条件分支。
而这些分支一旦在热点循环里反覆触发,就会拖慢现代cpu最依赖的指令流水线。
事实上,並不是系统不会排。
而是流程太重了。
重到cpu的每一个时钟周期都在被无意义的管理逻辑消耗。
江临现在要做的,不是去推翻高德纳在《电脑程式设计艺术》里写下的经典排序理论,也不是发明什么震惊世界的新算法。
他只是需要一套固定手势。
五张试卷。
看第一张和第二张。
谁大谁在前面,该换就换。
再看第三张和第四张。
该换就换。
几步极其固定的比较之后,顺序自然就出来了。
不问多余的类型问题。
不打开多余的內存分配流程。
不为那根本不存在的一百万张试卷准备任何冗余的边界检查工具。
没有数据相关的循环,没有运行时临时选择路径。
比较顺序在编译前就被钉死,剩下的只是固定位置之间的比较与交换。
只处理这五个位置的数字。
这就是在高性能计算领域里,针对极其明確边界的小规模数据,进行优化的核心奥义。
凌晨一点二十,万籟俱寂,江临在新建的c语言扩展文件里,写下了第一版函数的签名。
函数名很丑,甚至不像一个优雅算法库里的东西。
rank5_fixed_v0。
它不试图排序世界上一切数组,只处理五个float64因子暴露值,五个有效性標记,以及五个原始位置编號。
输出的也不是一个漂亮的新数组,而是一组业务排名和一组mask。
它就像一把在废土车间里,为了拧某种特定型號引擎底盘上的特定螺丝,而被强行把手柄焊弯的怪异扳手。
但江临现在需要的,正是这种专一暴力的扳手。
第一版写完,江临並没有急著直接替换到python 审计主流程里去。
作为在废土里见识过一个小数点错误导致整个证明功亏一簣的倖存者,他对替换底层逻辑有著病態的严谨。
他先做baseline。
原流程输出什么,新函数就必须输出一模一样的东西。
在量化金融的数据里,现实永远比理论骯脏。
无重复值(理想状態)。
重复值(两只股票因子得分完全一样)。
缺失值(nan,某只股票当天停牌没有数据)。
极端值(infinity)。
负值。
相等值且需要保持原相对顺序(稳定排序要求)。
每一种情况,都必须对齐。
有重复值时,原来在数组里谁在前面,现在排序后也必须谁在前面。
遇到nan缺失值时,量化的规则不是数学上的把它当最大或者把它当最小,而是必须按照项目预设的规则,將其单独剔除並打上mask_nan標籤,剩下的数继续排。
这並非纯粹数学定义上的排序,带著强烈业务属性的金融审计项目里的排名规则。
两者绝不能混为一谈。
凌晨两点半,江临揉了揉发酸的眼睛。
第一版v0跑过了包含两万个边缘测试用例的单元测试。
结果全对。
速度有提升,但並不大,大概只快了15%。
第一版只是为了验证这个强耦合的方向是走得通的。
江临並不意外。
他重新打开c 代码。
开始真正的榨乾性能。
刪掉所有不必要的状態判断。
把仅有的一点循环彻底展开,变成直线型的直线代码。
把可能会出现的各种数据类型的可能性彻底焊死,限定在这个项目里真正会输入进来的內存布局。
凌晨三点十八,第二版出来了。
他没有把这个函数暴露成一个给python循环逐次调用的小玩具。
那样七千万次跨语言调用本身就会成为新的灾难。
他真正写的是一个批处理入口。
一次性接收连续內存里的数百万个五元组,在c层內部跑完整个固定排序网络,再把排名矩阵吐回给python。
再次跑测试。
结果一致。
速度提升到了30%。
但这还不够。
江临的眉头微微皱起,他感觉到代码里还有多余的脂肪。
从抽屉里拿出一张白纸和一支笔。
纸上,他画了五个圆圈,標上序號:0,1,2,3,4。
然后开始在圆圈之间连线。
他现在写的不是代码,而是动作。
在底层的汇编指令里,比较並交换是一个极其廉价的动作。
只要没有if分支造成的预测失败,指令就可以像水一样顺畅地通过 cpu流水线。
比较0和1(大的去右边)。
比较3和4。
比较2和4。
比较2和3。
比较1和4。
比较0和3。
……
每一步,都像是在进行一次精密的机械手工调整。
五个数排好序,根本不需要程序在每一次运行的时候去思考接下来该怎么办。
路线是可以提前定死的。
就像水流经过预先挖好的迷宫沟渠,无论水势大小,最终都会从既定的出口按照大小顺序流出。
只要这套网格路线上,所有可能的 $5! = 120$ 种初始排列,最后都能被正確地疏导成有序状態,就足够了。
这就是计算机科学中极其冷门但极其硬核的概念。
排序网络。
它一点也不聪明。
面对1000个数它毫无办法。
但它非常稳定。
它不通用,但它是为高频,小规模任务量身定製的终极杀器。
写到这里,江临停下了笔。
忽然想起了上午在b304里,那个放在桌角的磁性几何魔方,想起了那道阿里数学竞赛的盲盒题。
他记得自己对尹航说过的话,先別被图案牵著走,先找上界,再找取等构造。
优化排序底层的逻辑也一样。
先別被排序这个在教科书里被讲烂了的大词嚇住。
他现在要处理的,根本不是排序学,只是五个內存位置之间有限次比较交换组合的最小充分集。
这是一个很小的世界。
小到它的所有状態都可以被数学穷尽。
小到它是可以被严格证明的。
但它重要到,只要这个动作被重复七千万次,它就会变成拖垮庞大金融系统的结构性瓶颈。
凌晨四点十分,窗外的天际已经隱隱有了一丝青灰色。
江临敲下最后一组指令,第三版完成。