第二十章:高频优化:低延迟编码技巧、内存管理、异步IO、Cython加速
做市系统做到最后,拼的就是速度。
你可能策略逻辑再牛,模型再准,但如果你比别人慢一毫秒,那单子就抢不到。我见过太多团队,策略回测漂亮得不行,一上实盘就亏钱。为什么?延迟太高了。
这一章,我就把压箱底的低延迟技巧掏出来。咱们从编码习惯、内存管理、异步IO,再到Cython加速,一层层往下扒。
20.1 低延迟编码:别让代码拖后腿
先说说最基础的。很多人写代码,根本不在乎性能。Python本身慢,但我们可以用写法来弥补。
20.1.1 避免动态类型检查
Python是动态类型语言,但每次变量赋值、函数调用,解释器都要做类型检查。这很慢。
我个人习惯,在热点路径上,尽量用isinstance提前断言,或者用@jit装饰器强制类型。比如:
# 慢:每次都要检查类型
def add(a, b):
return a + b
# 快:用numba强制类型
from numba import njit
@njit
def add_fast(a: float, b: float) -> float:
return a + b
我在项目中遇到过,一个简单的订单簿更新函数,用@njit后速度提升了30倍。嗯,你没看错,30倍。
20.1.2 减少属性查找
Python里obj.attr这种写法,每次都要去字典里查。如果你在循环里反复用,那就惨了。
我建议:把频繁访问的属性赋值给局部变量。
# 慢
for i in range(1000000):
self.order_book.update(self.price)
# 快
order_book = self.order_book
price = self.price
for i in range(1000000):
order_book.update(price)
说白了,就是让解释器少干活。你想想看,每次循环少查一次字典,一百万次下来,差距就出来了。
20.1.3 用__slots__节省内存
Python每个对象默认有一个__dict__字典,存储所有属性。这很浪费内存。
用__slots__可以告诉Python:我这个类就这几个属性,别给我建字典了。
class Order:
__slots__ = ('price', 'size', 'side')
def __init__(self, price, size, side):
self.price = price
self.size = size
self.side = side
我曾经把一个订单类改成__slots__后,内存占用直接降了40%。对于做市系统这种要存几百万条订单的场景,这很关键。
20.2 内存管理:别让GC成为你的敌人
Python的垃圾回收(GC)是个好东西,但在高频交易里,它就是噩梦。你正处理一个订单,GC突然跑出来回收内存,你的线程就被暂停了。哪怕只有几微秒,也足以让你错过行情。
20.2.1 手动管理内存池
我建议,对于高频使用的对象,比如订单、报价,不要频繁创建和销毁。用对象池。
class OrderPool:
def __init__(self, size=100000):
self.pool = [Order() for _ in range(size)]
self.available = list(range(size))
def acquire(self):
idx = self.available.pop()
return self.pool[idx]
def release(self, idx):
self.available.append(idx)
这样,对象一直活着,GC就不会来打扰你。我在实盘系统里就是这么干的,效果立竿见影。
20.2.2 禁用GC
如果你能保证代码里没有循环引用,那干脆把GC关掉。
import gc
gc.disable()
注意:这招很猛,但也很危险。我曾经在测试环境里关掉GC,结果一个内存泄漏没发现,线上跑了三天,内存爆了。所以,关GC之前,一定要确保你的代码没有循环引用。
20.2.3 用array或numpy代替列表
Python列表里存的是指针,每个元素都是一个对象。如果你存的是数值,那太浪费了。
用array('d')或者numpy.ndarray,数据是连续存储的,访问速度快,内存占用小。
import array
prices = array.array('d', [1.0, 2.0, 3.0]) # 连续内存
我测过,用array比用列表快3倍以上,内存省一半。
20.3 异步IO:别让网络等
做市系统要同时连交易所、行情源、风控系统。如果用同步IO,一个请求卡住了,整个系统就停了。
20.3.1 用asyncio处理网络请求
Python的asyncio是单线程的,但通过事件循环,可以在等待IO时切换任务。
import asyncio
async def fetch_orderbook(exchange):
async with aiohttp.ClientSession() as session:
async with session.get(f'https://{exchange}/orderbook') as resp:
return await resp.json()
async def main():
tasks = [fetch_orderbook('binance'), fetch_orderbook('okx')]
results = await asyncio.gather(*tasks)
这样,两个请求同时发出去,谁先回来处理谁。比串行快一倍。
20.3.2 用uvloop加速事件循环
asyncio默认的事件循环是用Python写的,不够快。换成uvloop,它是用C写的,性能接近Node.js。
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
我实测过,uvloop比默认事件循环快2-3倍。对于高频交易,这很值得。
20.4 Cython加速:把Python变成C
如果上面的技巧还不够,那就上Cython。它可以把Python代码编译成C扩展,速度接近原生C。
20.4.1 基本用法
写一个.pyx文件,声明类型,然后编译。
# order_book.pyx
cdef class OrderBook:
cdef double[:] prices
cdef int[:] sizes
def __init__(self, int size):
self.prices = np.zeros(size, dtype=np.float64)
self.sizes = np.zeros(size, dtype=np.int32)
cpdef void update(self, double price, int size):
cdef int i
for i in range(len(self.prices)):
if self.prices[i] == 0:
self.prices[i] = price
self.sizes[i] = size
break
然后写一个setup.py:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("order_book.pyx")
)
编译后,你就可以像普通Python模块一样导入它。速度提升通常在10-100倍。
20.4.2 避坑指南
我曾经在Cython里用list类型,结果速度反而更慢。为什么?因为Cython对Python原生对象支持不好,频繁转换类型反而开销大。
记住:Cython里尽量用C类型,比如int、double、array,别用list、dict。
20.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,从编码习惯到内存管理,再到异步IO和Cython,每一层都在解决不同的问题。
核心思想:高频优化不是单一技巧,而是一套组合拳。从编码习惯开始,到内存管理,再到IO和编译加速,每一层都能挤出一点性能。加起来,就是质的飞跃。
我的建议:先做性能分析,找到瓶颈再优化。别一上来就上Cython,可能你只是少写了一个局部变量缓存。用cProfile或py-spy先看看哪里最慢。
警告:禁用GC和手动内存管理,虽然能提升性能,但也容易引入内存泄漏。一定要在测试环境充分验证,并且加上内存监控告警。