病。
但这不是江临的第一反应。
在他的脑海深处,那个属于【废土世界】的倒计时始终存在。
在资源枯竭的废土里,永远没有再买一台的选项。
在那里,机器慢,不能先要资源,必须先问。为什么慢?是什么在吃掉算力?
江临果断按下了 ctrl+c,停掉了后续的扫描任务。
打开python的性能分析工具,将刚才那段跑得极慢的代码用cprofile 重新包起来,然后接入SnakeViz进行可视化分析。
二十分钟后,一份详细的调用栈火焰图出现在屏幕上。
真正的瓶颈浮出水面。
在调用栈里吞掉最多墙钟时间的怪物,不是那些几百万行的大矩阵乘法。
不是复杂的hdF5文件读取,不是前端渲染的图表生成,也不是某个玄学的机器学习复杂模型。
而是几个小得近乎不起眼的动作:排序、排名、分桶。
在量化回测中,经常需要在特定的行业特定的市值区间内,对股票的因子暴露值进行中性化和排序。
每次参与排序的股票可能不多,只有五个、八个、或者十六个。
单独看,给五个数排序,不管用什么算法,每一次都快得像没有成本,连一毫秒都不需要。
但问题在于乘数效应。
qF-oLdLIb-001里有四百多个因子。
三年历史版本。
多个动态调整的样本池。
多种回测配置。
每一天,每一个行业,每一个状态标记、每一个数据版本,都要切出一层层的横截面进行分组排名。
于是,这些微小的小动作被嵌套在庞大的循环网里,被反复调用。
江临把调用栈里耗时最高的那个函数次数打印了出来
七千八百四十二万一千九百零六次。
看着这个天文数字,他沉默了十几秒,然后在项目的audit_log.里,他敲下了这样一段话。
真正拖慢整个复杂系统的,从来都不是偶尔出现的大山,而是每天必须被搬运七千万次的小石头。
写完这句话,他停顿了一下。
为了让将来可能接手这份审计报告的平庸工程师也能看懂,他又补了一段更通俗的解释。
一次给五张试卷按分数从高到低排序,对任何人都不难。
难的是,系统要求你一天之内,把给五张试卷排序这个动作,重复七千万次。
标准库里的排序算法在计算机科学上被证明是非常优秀的。
但它们优秀的前提是通用。
标准库就像是一套占地几万平方米的大型自动化物流分拣中心。
它可以处理一万张试卷。
可以处理一百万个包裹。
可以处理带有各种奇怪对象的复杂数据结构。
它强大,通用,绝对可靠。
但如果你的流水线上,每次送过来的永远只有五个小包裹,而且每天要送七千万次。
那么,你每一次都去启动那套耗电巨大的大型物流中心,让传送带空转,让机械臂寻址,去执行庞大的分拣逻辑,
这就是不可饶恕的浪费。
在底层代码的视角里,这种浪费体现为,为了通用性而保留的复杂的函数调用开销。
为了处理多态而进行的动态类型检查。
为了兼容不同数组长度,标准流程里保留了大量条件分支。
而这些分支一旦在热点循环里反复触发,就会拖慢现代cpU最依赖的指令流水线。
事实上,并不是系统不会排。
而是流程太重了。
重到cpU的每一个时钟周期都在被无意义的管理逻辑消耗。
江临现在要做的,不是去推翻高德纳在《计算机程序设计艺术》里写下的经典排序理论,也不是发明什么震惊世界的新算法。
他只是需要一套固定手势。
五张试卷。
看第一张和第二张。
谁大谁在前面,该换就换。
再看第三张和第四张。
该换就换。
几步极其固定的比较之后,顺序自然就出来了。
不问多余的类型问题。
不打开多余的内存分配流程。
不为那根本不存在的一百万张试卷准备任何冗余的边界检查工具。
没有数据相关的循环,没有运行时临时选择路径。
比较顺序在编译前就被钉死,剩下的只是固定位置之间的比较与交换。
只处理这五个位置的数字。
这就是在高性能计算领域里,针对极其明确边界的小规模数据,进行优化的核心奥义。
凌晨一点二十,万籁俱寂,江临在新建的c语言扩展文件里,写下了第一版函数的签名。
函数名很丑,甚至不像一个优雅算法库里的东西。
rank5_fixed_v0。
它不试图排序世界上一切数组,只处理五个float64因子暴露值,五个有效性标记,以及五个原始位置编号。
输出的也不是一个漂亮的新数组,而是一组业务排名和一组sk。
它就像一把在废土车间里,为了拧某种特定型号引擎底盘上的特定螺丝,而被强行把手柄焊弯的怪异扳手。
但江临现在需要的,正是这种专一暴力的扳手。
第一版写完,江临并没有急着直接替换到python 审计主流程里去。
作为在废土里见识过一个小数点错误导致整个证明功亏一篑的幸存者,他对替换底层逻辑有着病态的严谨。
他先做baseline。
原流程输出什么,新函数就必须输出一模一样的东西。
在量化金融的数据里,现实永远比理论肮脏。
无重复值(理想状态)。
重复值(两只股票因子得分完全一样)。
缺失值(NaN,某只股票当天停牌没有数据)。
极端值(Infinity)。
负值。
相等值且需要保持原相对顺序(稳定排序要求)。
每一种情况,都必须对齐。
有重复值时,原来在数组里谁在前面,现在排序后也必须谁在前面。
遇到NaN缺失值时,量化的规则不是数学上的把它当最大或者把它当最小,而是必须按照项目预设的规则,将其单独剔除并打上SK_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个数它毫无办法。
但这不是江临的第一反应。
在他的脑海深处,那个属于【废土世界】的倒计时始终存在。
在资源枯竭的废土里,永远没有再买一台的选项。
在那里,机器慢,不能先要资源,必须先问。为什么慢?是什么在吃掉算力?
江临果断按下了 ctrl+c,停掉了后续的扫描任务。
打开python的性能分析工具,将刚才那段跑得极慢的代码用cprofile 重新包起来,然后接入SnakeViz进行可视化分析。
二十分钟后,一份详细的调用栈火焰图出现在屏幕上。
真正的瓶颈浮出水面。
在调用栈里吞掉最多墙钟时间的怪物,不是那些几百万行的大矩阵乘法。
不是复杂的hdF5文件读取,不是前端渲染的图表生成,也不是某个玄学的机器学习复杂模型。
而是几个小得近乎不起眼的动作:排序、排名、分桶。
在量化回测中,经常需要在特定的行业特定的市值区间内,对股票的因子暴露值进行中性化和排序。
每次参与排序的股票可能不多,只有五个、八个、或者十六个。
单独看,给五个数排序,不管用什么算法,每一次都快得像没有成本,连一毫秒都不需要。
但问题在于乘数效应。
qF-oLdLIb-001里有四百多个因子。
三年历史版本。
多个动态调整的样本池。
多种回测配置。
每一天,每一个行业,每一个状态标记、每一个数据版本,都要切出一层层的横截面进行分组排名。
于是,这些微小的小动作被嵌套在庞大的循环网里,被反复调用。
江临把调用栈里耗时最高的那个函数次数打印了出来
七千八百四十二万一千九百零六次。
看着这个天文数字,他沉默了十几秒,然后在项目的audit_log.里,他敲下了这样一段话。
真正拖慢整个复杂系统的,从来都不是偶尔出现的大山,而是每天必须被搬运七千万次的小石头。
写完这句话,他停顿了一下。
为了让将来可能接手这份审计报告的平庸工程师也能看懂,他又补了一段更通俗的解释。
一次给五张试卷按分数从高到低排序,对任何人都不难。
难的是,系统要求你一天之内,把给五张试卷排序这个动作,重复七千万次。
标准库里的排序算法在计算机科学上被证明是非常优秀的。
但它们优秀的前提是通用。
标准库就像是一套占地几万平方米的大型自动化物流分拣中心。
它可以处理一万张试卷。
可以处理一百万个包裹。
可以处理带有各种奇怪对象的复杂数据结构。
它强大,通用,绝对可靠。
但如果你的流水线上,每次送过来的永远只有五个小包裹,而且每天要送七千万次。
那么,你每一次都去启动那套耗电巨大的大型物流中心,让传送带空转,让机械臂寻址,去执行庞大的分拣逻辑,
这就是不可饶恕的浪费。
在底层代码的视角里,这种浪费体现为,为了通用性而保留的复杂的函数调用开销。
为了处理多态而进行的动态类型检查。
为了兼容不同数组长度,标准流程里保留了大量条件分支。
而这些分支一旦在热点循环里反复触发,就会拖慢现代cpU最依赖的指令流水线。
事实上,并不是系统不会排。
而是流程太重了。
重到cpU的每一个时钟周期都在被无意义的管理逻辑消耗。
江临现在要做的,不是去推翻高德纳在《计算机程序设计艺术》里写下的经典排序理论,也不是发明什么震惊世界的新算法。
他只是需要一套固定手势。
五张试卷。
看第一张和第二张。
谁大谁在前面,该换就换。
再看第三张和第四张。
该换就换。
几步极其固定的比较之后,顺序自然就出来了。
不问多余的类型问题。
不打开多余的内存分配流程。
不为那根本不存在的一百万张试卷准备任何冗余的边界检查工具。
没有数据相关的循环,没有运行时临时选择路径。
比较顺序在编译前就被钉死,剩下的只是固定位置之间的比较与交换。
只处理这五个位置的数字。
这就是在高性能计算领域里,针对极其明确边界的小规模数据,进行优化的核心奥义。
凌晨一点二十,万籁俱寂,江临在新建的c语言扩展文件里,写下了第一版函数的签名。
函数名很丑,甚至不像一个优雅算法库里的东西。
rank5_fixed_v0。
它不试图排序世界上一切数组,只处理五个float64因子暴露值,五个有效性标记,以及五个原始位置编号。
输出的也不是一个漂亮的新数组,而是一组业务排名和一组sk。
它就像一把在废土车间里,为了拧某种特定型号引擎底盘上的特定螺丝,而被强行把手柄焊弯的怪异扳手。
但江临现在需要的,正是这种专一暴力的扳手。
第一版写完,江临并没有急着直接替换到python 审计主流程里去。
作为在废土里见识过一个小数点错误导致整个证明功亏一篑的幸存者,他对替换底层逻辑有着病态的严谨。
他先做baseline。
原流程输出什么,新函数就必须输出一模一样的东西。
在量化金融的数据里,现实永远比理论肮脏。
无重复值(理想状态)。
重复值(两只股票因子得分完全一样)。
缺失值(NaN,某只股票当天停牌没有数据)。
极端值(Infinity)。
负值。
相等值且需要保持原相对顺序(稳定排序要求)。
每一种情况,都必须对齐。
有重复值时,原来在数组里谁在前面,现在排序后也必须谁在前面。
遇到NaN缺失值时,量化的规则不是数学上的把它当最大或者把它当最小,而是必须按照项目预设的规则,将其单独剔除并打上SK_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个数它毫无办法。