Python生成器yield/send传输大体积数据的开销疑问
关于生成器send/yield的性能问题解答
核心底层逻辑说明
Python 中generator.send()和yield的交互仅传递对象的引用,不会对传输的结构做任何深拷贝操作,本身的执行耗时是纳秒级,和传输对象的大小完全无关,你的初始认知是正确的。
你观测到的接近1秒的间隔,本质是send调用完成到生成器被调度执行到logger.info("got data")之间的所有前置操作的总耗时,和send/yield本身无关。
耗时的常见原因
你遇到的高延迟大概率来自两个核心方向:
- 大对象的批量GC开销:你传输的百万键值对字典树,所有节点的引用计数初始为1,当发送端的作用域销毁该字典时,会触发整个对象树的引用计数批量减1,大量节点引用计数归零后会被即时回收,整个遍历销毁的过程会阻塞事件循环,刚好落在你两个日志的时间区间内。
- 协程调度延迟:如果你的事件循环队列中存在大量待执行的前置任务,
send将生成器标记为就绪后,需要等待所有前置任务执行完成才会执行到接收端的日志打印逻辑。加上30MiB Protobuf数据的反序列化本身就有数百毫秒的开销,累加后就会出现接近1秒的延迟。
验证方法
你可以新增埋点确认send本身的耗时:
# 发送端埋点 import time data = huge_big_dictionary_of_dictionaries logger.info("sending to generator") start = time.perf_counter() generator.send(data) end = time.perf_counter() logger.info(f"send operation itself cost: {end - start:.6f}s") # 接收端埋点 data = yield(...) logger.info("got data right after yield")
实测后你会发现send本身的耗时稳定在微秒级,和字典大小没有关联。
优化方案(实现常数时间传输)
- 做对象池复用:如果这类大字典是高频传输的固定结构,用完后放回对象池复用,不要直接交给GC回收,避免批量销毁的开销。
- 调整引用持有逻辑:发送端完成传输后不要立即释放大对象的引用,等接收端处理完成后再统一销毁,把回收开销转移到非关键路径。
- 优化大对象的存储结构:不要一次性将30MiB的Protobuf全量反序列化为Python字典,Python字典的内存开销是序列化后数据的5~10倍,操作本身就很慢。可以按需反序列化需要访问的节点,或者用更紧凑的结构体(比如
dataclass+slots、第三方的array类结构)存储字典树,大幅降低内存和操作开销。 - 拆分长耗时任务:把大对象的处理逻辑拆分为多个小任务分步执行,避免单个任务阻塞事件循环超过10ms,降低调度延迟。
内容的提问来源于stack exchange,提问作者Goswin von Brederlow
相关产品推荐
相关产品推荐

