第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对象在内存里怎么存的(对象模型),再搞懂内存怎么管理(引用计数和GC),然后学会怎么测量和分析(工具),最后用 __slots__ 这样的手段来优化。每一步都是下一步的基础。
好了,这一章的内容就到这里。记住,内存优化不是炫技,而是实打实的性能提升。在回测引擎里,每一字节都值得珍惜。
公众号:蓝海资料掘金营,微信deep3321