第1章:内存模型基础

做量化回测这些年,我踩过最大的坑就是内存。记得有一次,我写了一个看起来完美的回测引擎,跑沪深300全量数据,结果跑了两个小时,内存直接爆了。从那以后,我养成了一个习惯——写代码之前,先想清楚内存里到底发生了什么。

这一章,我们就从最基础的内存模型讲起。别小看这些基础,它们是后面所有优化的根基。

1.1 Python对象内存模型

Python里的一切都是对象。整数是对象,字符串是对象,函数也是对象。每个对象在内存里都占一块地方。这块地方长什么样呢?

每个Python对象都有三个基本部分:

  • 对象头:包含引用计数和类型指针
  • 数据区:存放实际数据
  • 额外字段:比如字典、弱引用等

我举个例子你就明白了。一个简单的整数 a = 42,在内存里可不是只占4个字节。实际上,它占28个字节(64位系统)。为什么?因为对象头就占了16个字节,再加上数据本身和一些对齐填充。

核心要点:Python对象的内存开销,大头往往不在数据本身,而在对象的管理结构上。

你想想看,如果你有100万个整数,每个多占24个字节,那就是24MB的额外开销。在回测引擎里,这种浪费是致命的。

1.2 引用计数与垃圾回收

Python用引用计数来管理内存。说白了,就是每个对象都记着有多少个变量在引用它。当引用计数降到0,对象就被回收了。

import sys

a = []
print(sys.getrefcount(a))  # 输出2(a本身 + getrefcount的临时引用)

b = a
print(sys.getrefcount(a))  # 输出3

del b
print(sys.getrefcount(a))  # 输出2

这里有个坑,我踩过好几次。循环引用会导致内存泄漏。比如两个对象互相引用,它们的引用计数永远不会降到0。

避坑指南:我曾经在回测引擎里用双向链表存储订单,结果订单取消后内存一直不释放。后来才发现是循环引用的问题。解决方案是用弱引用(weakref)打破循环。

Python的垃圾回收器(GC)主要处理循环引用。它分代回收,对象越老,检查频率越低。这个设计很聪明,因为大部分对象都是朝生暮死的。

触发条件 检查频率
第0代 分配次数 - 回收次数 > 700 最高
第1代 第0代回收10次 中等
第2代 第1代回收10次 最低

我个人习惯在回测引擎里手动控制GC。比如在回测开始前禁用GC,结束后再启用。这样可以避免回测过程中GC暂停导致的性能抖动。

1.3 sys.getsizeof()与内存分析工具

想知道一个对象占多少内存?用 sys.getsizeof()。但这个函数有个坑——它只返回对象本身的大小,不包含引用的对象。

import sys

a = [1, 2, 3]
print(sys.getsizeof(a))  # 输出列表本身的大小,约88字节
# 列表里的整数对象不包含在内

要算完整的内存占用,得用 asizeof 或者自己递归计算。我推荐用 pympler 这个库,它算得准。

from pympler import asizeof

a = [1, 2, 3]
print(asizeof.asizeof(a))  # 输出完整大小,包含所有子对象

实用技巧:在回测引擎里,我经常用 tracemalloc 来追踪内存分配。它能告诉你哪行代码分配了多少内存,对定位内存泄漏特别有用。

其他好用的工具还有:

  • memory_profiler:逐行分析内存使用
  • objgraph:可视化对象引用关系
  • guppy3:堆内存分析

嗯,这里要注意,分析工具本身也会消耗内存。在生产环境慎用,但在开发阶段,它们是救命稻草。

1.4 __slots__优化原理

这是Python里一个被低估的优化手段。默认情况下,每个类实例都有一个 __dict__ 字典来存储属性。字典的开销很大,每个实例光字典就占几百字节。

__slots__ 的原理很简单:告诉Python,这个类的实例只有这些属性,别用字典了,用固定大小的数组来存。

class Trade:
    __slots__ = ('price', 'volume', 'timestamp')
    
    def __init__(self, price, volume, timestamp):
        self.price = price
        self.volume = volume
        self.timestamp = timestamp

效果有多明显?我测试过,一个简单的订单类,用 __slots__ 后内存占用减少了60%以上。在回测引擎里,如果有100万个订单对象,这就是几百MB的差距。

性能对比

属性 普通类 __slots__类
每个实例内存 约400字节 约120字节
属性访问速度 慢(字典查找) 快(索引访问)
灵活性 可动态添加属性 固定属性

__slots__ 也有代价。你不能动态添加新属性了,也不能用 __dict__。如果类需要继承,子类也得定义 __slots__,否则父类的优化就白费了。

避坑指南:我曾经在一个策略基类上用了 __slots__,结果子类忘了加,导致内存占用反而增加了。因为子类既有 __slots__ 的数组,又有 __dict__ 字典,两头都占。

我的建议是:在回测引擎的核心数据结构上(比如K线、订单、持仓),一定要用 __slots__。这些对象数量大,生命周期长,优化收益最明显。

知识体系总览

下面这张图,是我自己画的内存模型知识体系。你可以看到,四个知识点是层层递进的:

Python内存模型知识体系 Python内存模型 对象内存模型 对象头 + 数据区 + 额外字段 引用计数与GC 分代回收 + 循环引用处理 内存分析工具 sys.getsizeof + pympler + tracemalloc __slots__优化 固定属性 + 数组存储 + 减少开销 回测引擎内存优化实战

这张图的核心逻辑是:先理解Python对象在内存里怎么存的(对象模型),再搞懂内存怎么管理(引用计数和GC),然后学会怎么测量和分析(工具),最后用 __slots__ 这样的手段来优化。每一步都是下一步的基础。

好了,这一章的内容就到这里。记住,内存优化不是炫技,而是实打实的性能提升。在回测引擎里,每一字节都值得珍惜。


公众号:蓝海资料掘金营,微信deep3321