4. 数据结构选型:list vs tuple vs array vs deque,以及 __slots__ 优化内存布局

在低延迟因子评分接口中,每一纳秒的访问延迟、每一个字节的内存占用都可能成为瓶颈。Python 内置的多种序列型数据结构——list、tuple、array、deque——虽然都支持“按索引访问”,但它们在内存布局、缓存友好度、修改开销和线程安全方面存在显著差异。选错数据结构,轻则增加 GC 压力,重则导致评分流水线整体吞吐下降 30% 以上。

本章从底层实现出发,对比四种数据结构的性能特征,并引入 __slots__ 机制来进一步压缩因子对象的内存占用。

4.1 四种序列型数据结构的内存与性能对比

下表从 存储粒度、修改开销、迭代速度 和 适用场景 四个维度进行横向对比:

特性 list tuple array('d') deque
底层存储 PyObject* 数组(指针数组) PyObject* 数组(不可变) C 类型连续内存(如 double) 双向链表(块状数组)
元素类型 任意 Python 对象 任意 Python 对象 单一 C 原生类型 任意 Python 对象
内存开销/元素 8 字节指针 + 对象头 8 字节指针 + 对象头 8 字节(double) 8 字节指针 + 块开销
索引访问 O(1) 指针解引用 O(1) 指针解引用 O(1) 直接内存偏移 O(1) 但需块内计算
尾部追加 均摊 O(1) 不支持 均摊 O(1) O(1)
头部弹出 O(n) 整体移动 不支持 O(n) 整体移动 O(1)
迭代速度 中等(需引用计数) 中等(同 list) 最快(无引用计数) 较慢(链表跳跃)
缓存友好度 中等(指针间接) 中等 极高(连续内存) 低(离散块)
核心结论: 对于纯数值型因子序列(如收益率、波动率),array('d') 在迭代和内存上全面优于 list;tuple 适合作为不可变的因子快照;deque 仅在需要频繁双端操作时使用。

4.2 选型决策流程

在因子评分接口中,数据结构的选型应遵循以下决策树:

  1. 数据是否在创建后完全不变? → 是 → 使用 tuple(享元模式,可哈希)。
  2. 数据是否为纯数值类型(float/int)? → 是 → 使用 array('d') 或 array('f')。
  3. 是否需要频繁在头部插入/弹出? → 是 → 使用 deque。
  4. 以上都不满足,且需要通用对象存储? → 使用 list,但配合 __slots__ 优化元素对象。
低延迟实践: 在因子评分循环中,将因子值预加载为 array('d'),评分时直接通过内存视图(memoryview)批量计算,避免 Python 对象开销。

4.3 内存布局对比:指针 vs 连续内存

下图展示了 list 与 array('d') 在内存中的实际布局差异:

list vs array('d') 内存布局对比 list 对象 指针数组 指针0 指针1 指针2 ... PyFloat PyFloat PyFloat 每个元素:8 字节指针 + 32 字节 PyObject 头 + 8 字节 double 总开销 ≈ 48 字节/元素,且内存不连续,缓存命中率低 array('d') 对象 C 数组 double0 double1 double2 ... 每个元素:8 字节 double,连续内存,无 Python 对象头 总开销 ≈ 8 字节/元素,顺序访问时 CPU 预取效率极高
性能陷阱: 使用 list 存储 100 万个浮点数时,内存占用约 48 MB;改用 array('d') 后仅需 8 MB,且迭代速度提升 2-3 倍。在因子评分循环中,这种差异直接决定能否在 1 秒内完成全市场扫描。

4.4 __slots__:压缩因子对象内存布局

在因子评分接口中,每个因子通常被建模为一个 Python 对象,包含名称、权重、当前值、历史窗口等属性。默认情况下,每个对象都维护一个 __dict__ 字典来存储实例属性,这带来了巨大的内存开销(约 56 字节/属性 + 字典哈希表)。

__slots__ 通过声明固定属性列表,让 Python 为每个实例分配紧凑的 描述符数组,彻底消除 __dict__ 和 __weakref__ 的开销。

4.4.1 使用前后对比

指标 普通类(有 __dict__) __slots__ 类 节省比例
每个实例基础开销 56 字节(对象头) + 字典指针 48 字节(对象头) ~15%
属性存储方式 哈希表(dict) 固定偏移数组 —
属性访问速度 哈希查找(O(1) 均摊) 直接偏移(O(1) 常数) 快 20-40%
100 万因子对象内存 ~120 MB ~56 MB ~53%

4.4.2 代码示例

# 未使用 __slots__:每个因子对象约 120 字节
class Factor:
    def __init__(self, name: str, weight: float, value: float):
        self.name = name
        self.weight = weight
        self.value = value

# 使用 __slots__:每个因子对象约 56 字节
class FactorSlots:
    __slots__ = ('name', 'weight', 'value')
    def __init__(self, name: str, weight: float, value: float):
        self.name = name
        self.weight = weight
        self.value = value
低延迟最佳实践: 在因子评分接口中,所有因子对象、评分结果行、中间缓存结构都应使用 __slots__。配合 array('d') 存储数值序列,可将整体内存占用降低 60% 以上,同时减少 GC 触发频率。

4.5 综合选型策略:因子评分流水线中的数据结构组合

下图展示了一个典型的因子评分流水线中,不同阶段应选用的最优数据结构:

因子评分流水线数据结构选型 原始行情接收 array('d') 因子计算 __slots__ 因子对象 评分聚合 tuple(不可变快照) 结果输出 array('d') 选型要点 1. 原始行情:使用 array('d') 连续存储,支持向量化计算 2. 因子对象:使用 __slots__ 压缩内存,避免 dict 开销 3. 评分聚合:使用 tuple 创建不可变快照,支持缓存和哈希 4. 结果输出:使用 array('d') 批量写入,减少 Python 对象创建

4.6 避坑清单

  • 不要对 array 进行频繁的 append 操作:虽然均摊 O(1),但每次扩容都会触发内存重分配和拷贝。预分配长度(array('d', [0.0]) * n)可避免此开销。
  • 不要将 deque 用于随机访问:虽然索引访问是 O(1),但实际需要遍历块内偏移,比 list 慢 3-5 倍。
  • 不要忘记 __slots__ 的继承限制:子类必须重新定义 __slots__,否则会重新启用 __dict__。
  • 不要对 tuple 使用 __slots__:tuple 本身已是不可变序列,无法定义 __slots__。
  • 不要在 __slots__ 类中定义 __dict__:如果需要在运行时动态添加属性,应使用普通类或显式声明 __dict__ 在 __slots__ 中。
性能验证: 在 Intel Xeon 3.0GHz 上,使用 array('d') 迭代 1000 万次耗时约 0.12 秒,而 list 耗时约 0.35 秒。配合 __slots__ 因子对象,整体评分接口延迟可从 50ms 降至 20ms 以下。