因子计算变慢,账本不一定记在CPU头上,更大的开销其实是内存带宽被悄悄消耗掉了。业内专家指出,多数量化团队在优化因子时第一时间盯紧CPU使用率,却忽视了一个事实:内存带宽才是多数计算场景下的真正瓶颈,尤其在A股全市场选股的场景下,因子计算涉及数千只股票的逐笔数据扫描,内存搬运的数据量远超实际计算量,这才是卡顿的根源。
内存带宽,到底在为什么买单
一个简单的比喻:仓库和货架
把内存比作仓库,CPU比作货架上的工人,工人本身干活很快,但每次取货都要从仓库搬运,仓库到货架的通道宽度,就是内存带宽,因子计算中,程序每访问一次数据,都是一次“取货”,真正计算只花一个时钟周期,而搬运数据可能花了几百个周期。
内存带宽账本上最扎眼的一项,是数据搬运的浪费,pandas里最常见的df[col].values操作,看似只是取一列,背后可能复制了整块内存,复制本身不产生任何业务价值,但带宽却被占用了,全市场5000只股票、每只2000根日线,一次不经意间的DataFrame切片,搬运几十GB的数据毫无知觉。
带宽被消耗的三个典型场景
- 不必要的副本:
df.copy()、series.reindex()、pd.concat()这些操作,都会在内存中复制一份数据,因子计算中,每多一次副本,带宽就多付一次账。 - 高频次的小数组访问:循环里逐行计算因子,每行都是一次随机内存访问,行与行之间在内存里不连续,缓存命中率极低,相当于每次取货都走一次远程通道。
- 对象类型和字符串硬撑:因子数据如果存成
object类型,内存里存的是指针而非数值,计算时CPU需要先根据指针跳转取数,带宽开销成倍放大。
因子计算前,先检查数据布局
单因子计算为什么慢:真实场景复盘
上海一家私募的量化研究员曾反馈,一个看似简单的动量因子,在全市场回测中跑了将近两小时,排查后发现,原始数据用read_csv读入后默认是float64精度,因子计算中多次用df.rolling(20).mean()创建中间结果,每次滚动窗口都会产生一份新的副本。
优化后的流程简单直接:
- 将
float64降为float32,内存占用直接减半,带宽压力同步减半。 - 用
numpy数组替代DataFrame中间层,因子逻辑通过向量化运算完成。 - 提前聚合数据,只在需要时再展开到日频。
同样一个因子,优化后运行时间缩短到不到原来的十分之一,CPU计算量几乎没有变化,省下的全是内存带宽的开销。
如何判断瓶颈是不是内存带宽
不是所有慢都跟带宽有关,有个简单的验证方法:同时运行两个相同因子的计算任务,如果两个任务的总耗时接近单个任务的一半多一点,说明瓶颈在带宽;如果接近两倍,说明瓶颈在CPU。

另一个更直接的方法是用系统监控工具观察内存带宽利用率,Linux下用perf stat -e offcore_response.demand_data_rd.any查看内存请求量,如果数值远大于实际数据量,说明存在大量无效搬运,Windows下可以用Intel VTune Profiler的Memory Access分析模式,直接查看带宽消耗排名前几的代码行。
因子计算中,最常见的融资融券式操作
rolling、diff、shift是如何吃带宽的
rolling操作是带宽大户,滚动窗口计算天生需要频繁读取窗口内的数据,窗口越大,重复读取的次数越多,行业共识认为,滚动窗口类因子在计算中的带宽消耗是普通点对点运算的3到5倍。
shift操作同样不省心,每次shift都会生成一个新数组,且新旧数组之间存在大量重叠区域,如果连续做多次shift,比如计算5日、10日、20日的收益率,每多一个shift就多一次全量数据搬运。
优化方案是复用中间结果,计算多周期收益率时,先一次性拉出价格序列,用numpy的切片操作替代多次shift调用,切片不复制数据,只是视图,带宽只付一次账。
因子值分布统计的隐形成本
因子计算完成后,往往需要查看分位数、分布区间、异常值比例,常见的df.describe()会触发全列扫描,多个因子一起describe时,每一列都是一次全量内存读取。
更高效的做法是对因子值做有损压缩后统计,将float32数据映射为int16分箱,然后基于分箱数组计算分位数,分箱后数组体积缩小到原来的八分之一,统计过程中的带宽消耗也降到八分之一左右。
数值类型的选择,直接决定带宽账本
float64与float32的真实差距
很多研究员在因子计算中无脑使用float64,理由是精度更高,不容易出错,但量化场景下,绝大多数因子值的有效精度在4到5位小数以内,float32完全够用,以沪深300成分股的日线数据为例,全量因子矩阵用float64存储需要约8GB内存,换成float32只需要4GB。
存储空间减半带来的直接收益,是内存带宽压力同步减半,计算同一批因子时,CPU需要等待的数据量少了,流水线空转的时间也短了,对于分钟级甚至tick级因子,这个差异更明显。
整数化因子也能显著降低带宽消耗
部分因子可以被整数化,尤其是技术面因子,比如均线交叉信号、突破次数统计,把连续的浮点因子值离散化为整数后,数据体积进一步压缩,整数运算在现代CPU上有专门的SIMD指令加速,同样的带宽可以搬运更多有效数据。
实际操作中,可以将float32因子值乘以一个缩放系数后转为int32,一个取值在-5到5之间的因子值,乘以100后转为int32,原本需要32位浮点数表示的精度范围,现在用16位整数就能覆盖。

并行的账,怎么算才不亏
多进程与内存带宽的暧昧关系
很多团队为了加速因子计算,一上来就用multiprocessing开满所有核,在计算密集型任务中,这么做确实有效,但在内存带宽受限的任务中,多核并行反而可能导致总耗时增加。
原因是多进程各自持有数据副本,内存带宽被多个进程同时抢占,每个进程都在搬运自己那份数据,总带宽需求成倍上涨,当带宽成为瓶颈时,8个进程跑赢不了4个进程,某些极端情况下甚至更慢。
判断是否能并行,要看计算强度(算术运算量与数据搬运量的比值),如果计算强度低于1,即搬运1字节数据只做一次算术运算,那么并行几乎无益,高于10时并行效果明显,接近理论加速比。
推荐的因子计算并行策略
- 按股票分组并行,而非按时间切片,股票之间的数据天然独立,分组后各进程只读自己的数据块,带宽访问更连续。
- 共享只读内存,使用
multiprocessing.shared_memory将原始行情数据放在共享内存中,各进程只读取不复制,带宽只付一次账。 - 控制并行度,先用小规模测试找到带宽拐点,然后采用拐点附近的进程数,而不是无脑铺满CPU核数。
因子缓存:给带宽账本做个预算
哪些中间结果值得缓存
因子计算往往有重复环节,同一个谈素材,标准化之前和之后的形态都会被后续模块使用,如果不加缓存,每次使用时重新计算,带宽账本又多了一笔重复支出。
值得缓存的中间结果包括:
- 标准化前的原始因子值矩阵
- 去极值后的因子值
- 行业中性化所需的行业哑变量矩阵
- 市值因子、估值因子等基准因子向量
缓存格式优先选择npy或parquet,不要用pickle。pickle序列化后的数据体积大,反序列化时内存占用高,加载过程中带宽消耗较大。parquet有列式压缩,读取时只加载需要的列,带宽效率更高。
缓存的更新策略
量化研究的日常流程中,数据每天更新,因子也需要增量计算,采用按日期追加的增量更新方式,每次仅计算新交易日的因子值,而不是全量重算。
判断更新范围的简单规则:如果只追加了新数据,保留历史因子结果,只计算新增部分;如果因子定义发生变化,才触发全量重算,通过定义文件记录因子版本号,缓存命中时检查版本号一致,避免误用旧缓存。
因子计算中,数据源与格式的取舍
从数据库读取时的带宽控制
从数据库批量读取因子原始数据时,常见的低效操作是SELECT 拉取全部字段,一个包含50个字段的行情表,因子计算只需要其中5个字段,但数据库传输了全部数据,内存带宽在导入阶段就被浪费了。
正确做法是在SQL层面只查询所需字段,更进一步,如果数据库支持列式存储引擎(如ClickHouse、Doris),按列读取天然适合因子计算场景,带宽利用效率远高于行式存储的MySQL。

使用内存映射文件减少复制
当因子数据量超过内存容量时,使用mmap内存映射方式读取文件,可以跳过显式加载到内存的过程,操作系统的页面缓存机制让文件内容按需进入内存,因子计算过程中访问到的数据才占用带宽,未被访问的部分不产生任何消耗。
Python中直接使用np.memmap即可实现,对于5000只股票的历史日线数据,映射后的数组可以像普通numpy数组一样参与计算,而不需要一次性载入全部数据。
常见的难过内存缓存错误
查看中间结果的代价
调试因子时,很多研究员习惯在jupyter notebook里查看每步计算的中间结果,每次显示DataFrame时,前端都会复制一份数据用于渲染,如果数据量大,这个复制操作会消耗较大带宽。
更高效的调试方式是使用df.head(10)或df.dtypes这样只触达少量数据的检查命令,避免自动渲染全量数据。
过度使用apply函数的隐患
df.apply(func, axis=1)在因子计算中极为常见,但也是带宽消耗最高的操作之一,逐行调用Python函数,每次函数调用都是一次独立的内存访问,CPU缓存完全失效,数据量越大,带宽浪费越严重。
替代方案是apply换成向量化表达式,比如要计算最高价和最低价的比值,直接df['high'] / df['low']即可,不要在DataFrame的每一行上调用自定义函数。
Q&A
因子计算内存占用高,首先要检查什么?
检查数据副本的数量,用sys.getsizeof对比原始数据与每个中间变量的内存大小,找出复制次数最多的环节,通常首先排查的是DataFrame的rolling和shift操作,这两步最容易产生不必要的副本。
内存带宽和CPU使用率有什么关系?
CPU使用率高说明计算逻辑在干活,内存带宽消耗高说明数据搬运在排队,两个指标同时高,说明算力和带宽都接近极限,需要从算法层面减少运算量和数据访问量,两个指标一个高一个低,优先优化高的那一个。
提升内存带宽利用效率后,因子计算能快多少?
北京某量化团队的经验是,对现有因子库做一轮基于带宽优化的改造后,整体因子计算速度提升约4倍,部分依赖海量历史数据的因子,提速超过10倍,这个结果基于数据布局调整、类型压缩、中间结果复用三项基础优化,不需要调整服务器配置。
优化因子计算性能的核心,始终围绕控制内存带宽的消耗展开,每一份不必要的副本、每一个多余的数据类型转换、每一次无缓存的重算,都是带宽账本上的一笔浪费,在评估内存账本需要用时,先用监控工具度量带宽,再用本文提到的方法逐项消除,运行效率提升的空间通常远超预期。