数据结构选型:Python内置数据结构的内存开销分析
做量化交易系统,说白了就是在跟延迟和内存较劲。我见过太多团队,代码写得花里胡哨,结果一跑回测,内存直接爆了。嗯,今天咱们就来聊聊Python内置数据结构的内存开销,看看怎么选型才能让系统跑得又快又稳。
Python对象的内存模型
先得搞清楚一件事:Python里每个对象都有额外开销。我刚开始优化内存时,以为一个整数就占4个字节,结果一查,好家伙,28个字节!
为什么会这样?因为Python的对象头里存了引用计数、类型指针这些东西。你想想看,每个对象都带着这么个「身份证」,内存能不大吗?
核心结论:Python对象的内存开销 = 对象头开销 + 实际数据开销。对象头通常占16-56字节,具体看类型。
常见数据结构的内存开销对比
我在项目中做过一个测试,把100万个整数分别存到不同结构里,结果让我挺意外的。咱们直接看数据:
| 数据结构 | 100万整数内存占用 | 每个元素平均开销 | 适用场景 |
|---|---|---|---|
| list | ~36 MB | 36 字节 | 通用序列操作 |
| tuple | ~28 MB | 28 字节 | 固定数据、哈希键 |
| array('i') | ~4 MB | 4 字节 | 同类型数值序列 |
| numpy.ndarray | ~4 MB | 4 字节 | 数值计算、矩阵操作 |
| set | ~48 MB | 48 字节 | 快速去重、成员检查 |
| dict | ~64 MB | 64 字节 | 键值映射、快速查找 |
看到没?list和tuple差了将近8个字节。别小看这点差距,在量化系统里,你存的可能是几千万条tick数据,这差距就大了去了。
list vs tuple:选谁?
我个人习惯是:能固定长度的数据,坚决用tuple。比如存储订单的字段,价格、数量、时间戳这些,用tuple比list省内存,还更安全。
# 不推荐:用list存固定结构
order = ['buy', 100.5, 1000, 1623456789.0]
# 推荐:用tuple存固定结构
order = ('buy', 100.5, 1000, 1623456789.0)
# 更推荐:用namedtuple,可读性更好
from collections import namedtuple
Order = namedtuple('Order', ['side', 'price', 'qty', 'timestamp'])
order = Order('buy', 100.5, 1000, 1623456789.0)
避坑指南:我曾经在回测系统里用list存了500万条K线数据,结果内存飙到2GB。换成tuple后,直接降到1.5GB。虽然只省了500MB,但系统稳定性提升了不少。
array模块:被低估的利器
很多人不知道Python自带的array模块。它存的是C语言的原生类型,内存效率极高。我处理tick数据时,经常用array('d')来存价格序列。
import array
# 存储double类型的价格数据
prices = array.array('d', [100.5, 100.6, 100.4, 100.7])
# 类型码说明:
# 'i' - 有符号整数 (4字节)
# 'l' - 有符号长整数 (8字节)
# 'f' - 浮点数 (4字节)
# 'd' - 双精度浮点数 (8字节)
# 性能对比:array比list快30%左右
# 内存对比:array比list省80%左右
但要注意,array只能存同类型数据。如果你需要混合类型,那还是得用list或tuple。
dict的内存陷阱
dict是Python里最灵活的数据结构,但也是内存大户。我见过有人把每个tick数据都存成dict,结果内存直接爆炸。
# 不推荐:每个tick都存成dict
tick = {'symbol': 'BTCUSDT', 'price': 50000.0, 'qty': 0.1, 'timestamp': 1623456789.0}
# 推荐:用tuple或namedtuple
tick = ('BTCUSDT', 50000.0, 0.1, 1623456789.0)
# 或者用__slots__自定义类
class Tick:
__slots__ = ('symbol', 'price', 'qty', 'timestamp')
def __init__(self, symbol, price, qty, timestamp):
self.symbol = symbol
self.price = price
self.qty = qty
self.timestamp = timestamp
警告:dict的哈希表实现会预留大量空槽位。当dict容量达到2/3时,Python会触发扩容,内存直接翻倍。如果你要存大量键值对,考虑用__slots__类或者pandas的DataFrame。
set vs list:去重场景
做量化交易时,经常需要去重。比如从多个数据源合并订单簿,要去掉重复的订单。这时候用set比list快得多,但内存开销也大。
# 小数据量(< 1万):list去重更快
unique_orders = list(set(orders))
# 大数据量(> 10万):set内存开销明显
# 建议用bloom filter或者分批处理
# 我常用的技巧:用set做临时去重,用list存最终结果
seen = set()
result = []
for order in orders:
if order.id not in seen:
seen.add(order.id)
result.append(order)
实战选型建议
根据我多年的经验,总结了几条选型原则:
- 存储tick数据:用array或numpy,别用list。我处理过每秒10万条tick的系统,用array内存占用不到list的1/8。
- 存储订单簿:用__slots__类或namedtuple。每个订单节省30-40字节,100万订单就是30-40MB。
- 存储策略参数:用dict没问题,参数数量通常不多。但别把历史数据也塞进去。
- 存储回测结果:用pandas DataFrame,它底层是numpy,内存效率高,还方便分析。
- 存储中间计算结果:能用局部变量就别用全局变量。函数执行完,局部变量自动释放内存。
知识体系总览
下面这张图是我整理的数据结构选型决策流程,你可以照着这个思路来:
总结一下
数据结构选型没有银弹。你得根据实际场景来权衡。我个人习惯是:
- 先评估数据量级,小于10万条,用啥都行
- 大于100万条,必须用array或numpy
- 大于1000万条,考虑用数据库或文件存储
记住一点:内存优化不是一蹴而就的事。先跑起来,再优化。别一开始就追求极致的内存效率,把代码搞得太复杂,反而容易出bug。
核心原则:能用tuple不用list,能用array不用tuple,能用numpy不用array。越底层的数据结构,内存效率越高,但灵活性越低。找到平衡点,才是高手。