当前位置:首页 > 文章列表 > 文章 > python教程 > Python PickleBuffer 怎么减少大数组复制

Python PickleBuffer 怎么减少大数组复制

来源:17golang原创 2026-10-05 00:39:51 0浏览 收藏

pickle.PickleBuffer 减少大数组复制的关键,不是把数组压得更小,而是把连续的大块内存从主 pickle 字节流里分离出来。使用协议 5 时,主流只保存对象结构和对带外缓冲区的引用,真正的数组数据由 buffer_callback 单独收集;反序列化时,再通过 buffers 按原顺序提供给 pickle.loads。

最小可用组合是 protocol=5、buffer_callback=buffers.append 和 pickle.loads(meta, buffers=buffers)。它能避免把支持缓冲协议的大数组再次复制进主 pickle 流,但不会自动替你传输、保存或管理那些缓冲区。

Python 官方文档:https://docs.python.org/3/library/pickle.html#pickle.PickleBuffer

先确认:复制发生在主字节流里

普通 pickle 会把对象图变成一段连续字节。字典、形状、数据类型等元数据通常很小,但数组负载可能有几百 MB。若负载先转成新的 bytes,再写进 pickle 流,序列化端会出现额外分配;反序列化端又要从主流中恢复大块数据。数据越大,这些中间副本对峰值内存和内存带宽的影响越明显。

协议 5 从 Python 3.8 开始提供带外缓冲能力。PickleBuffer 是对缓冲区提供者的包装,本身也实现缓冲协议,可以交给 memoryview 等 API。只有当对象类型的协议 5 归约结果包含 PickleBuffer,并且消费方启用了 buffer_callback,大数据才会真正离开主 pickle 流。

组成保存什么是否包含大数组字节
主 pickle 流对象结构、类型信息、形状、dtype、带外引用标记带外模式下不包含对应缓冲区
PickleBuffer 列表指向数组底层内存的缓冲视图提供真正的大块数据
buffers 参数反序列化端按顺序消费的缓冲区迭代器必须与发送端收集顺序一致

把大数组移出主 pickle 流

NumPy 数组已经支持协议 5。下面代码显式指定 protocol=5,因此即使程序运行在默认协议仍为 4 的 Python 3.8–3.13,也不会因为省略参数而退回旧行为。list.append 返回 None,属于假值,所以 Pickler 会把收到的缓冲区标记为带外数据。

import pickle
import numpy as np

# 构造连续数组;真实业务中它可以来自计算结果或共享内存。
array = np.arange(1_000_000, dtype=np.float64).reshape(1000, 1000)

buffers = []
# append 返回 None,告诉 Pickler:缓冲区由调用方在主流之外处理。
meta = pickle.dumps(
    {"name": "weights", "data": array},
    protocol=5,
    buffer_callback=buffers.append,
)

# 接收端必须按 buffer_callback 触发时的原顺序提供缓冲区。
restored = pickle.loads(meta, buffers=buffers)
restored_array = restored["data"]

# 这里只做结构判断;是否共享底层内存取决于对象类型的重建方式。
assert restored_array.shape == array.shape
assert restored_array.dtype == array.dtype

此时 meta 是主 pickle 流,buffers 是独立的大数据集合。真正的进程间或网络传输需要自己设计两条通道:先传元数据及缓冲区数量,再通过共享内存、分块传输或其他机制交付每个缓冲区。PickleBuffer 提供的是“把数据交给调用方自行搬运”的接口,不是内置网络协议。

协议5主pickle流、PickleBuffer与buffers之间的静态引用关系
图1:协议 5 让主 pickle 流保存对象结构和带外引用,大数组缓冲由 buffer_callback 单独收集,并通过 buffers 按原顺序交给 loads;这是原创静态关系说明图。

怎么判断带外缓冲已经生效

不需要编造固定性能数字。先检查两个客观信号:回调是否收到缓冲区,以及主 pickle 流是否只承载较小的结构数据。缓冲区大小可以通过 memoryview 读取,而不必先转换成新的 bytes。

import pickle
import numpy as np

array = np.ones((2048, 2048), dtype=np.float32)
buffers = []

# 显式启用协议 5 和带外回调。
meta = pickle.dumps(array, protocol=5, buffer_callback=buffers.append)

# memoryview 直接观察缓冲区,不调用 bytes(),避免为了统计再复制一份。
out_of_band_bytes = sum(memoryview(buf).nbytes for buf in buffers)
print("主流字节数:", len(meta))
print("带外缓冲数:", len(buffers))
print("带外总字节数:", out_of_band_bytes)

如果 buffers 为空,常见原因有三个:没有使用协议 5、对象类型没有在协议 5 下产生 PickleBuffer,或者回调返回了真值,要求把缓冲区继续序列化到主流中。不要仅凭“用了 pickle.dumps”就认定已经零复制。

自定义数组容器要在 __reduce_ex__ 中暴露缓冲区

自定义类如果只是把 ndarray.tobytes() 塞进归约参数,复制已经在 tobytes() 发生。协议 5 的正确方向是返回 PickleBuffer,让 Pickler 决定走带内还是带外。下面是一个紧凑示例:

import pickle
import numpy as np

class ArrayEnvelope:
    def __init__(self, array):
        # 非连续切片先转成连续布局;这一步可能产生一次必要副本。
        self.array = np.ascontiguousarray(array)

    def __reduce_ex__(self, protocol):
        shape = self.array.shape
        dtype_str = self.array.dtype.str

        if protocol >= 5:
            # 返回缓冲视图,不调用 tobytes() 制造额外大对象。
            payload = pickle.PickleBuffer(self.array)
            return type(self)._restore, (payload, shape, dtype_str)

        # 兼容旧协议时只能退回普通 bytes,复制是预期代价。
        return type(self)._restore, (self.array.tobytes(), shape, dtype_str)

    @classmethod
    def _restore(cls, payload, shape, dtype_str):
        # frombuffer 从现有缓冲区建立数组视图,再恢复原形状。
        array = np.frombuffer(payload, dtype=np.dtype(dtype_str)).reshape(shape)
        obj = cls.__new__(cls)
        obj.array = array
        return obj

source = ArrayEnvelope(np.arange(12, dtype=np.int32).reshape(3, 4))
buffers = []

# 带外缓冲区与主 pickle 流分别保存。
meta = pickle.dumps(source, protocol=5, buffer_callback=buffers.append)
restored = pickle.loads(meta, buffers=buffers)
assert restored.array.shape == (3, 4)

这个写法同时保留了兼容路径:协议 5 走 PickleBuffer,协议 4 及更早版本走 bytes。如果生产系统要跨不同 Python 版本,发送端必须先确认接收端支持协议 5;否则应该协商降级,而不是让旧进程在反序列化时才失败。

复制减少后的三个边界

内存必须适合导出连续视图

PickleBuffer.raw() 返回一维、C 连续、格式为 B 的 memoryview。底层缓冲区既不是 C 连续也不是 Fortran 连续时会抛出 BufferError。NumPy 的步进切片、转置组合或高级索引结果可能不连续;此时 np.ascontiguousarray 会产生一次有目的的副本。PickleBuffer 能减少后续重复复制,但不能让任意离散布局凭空变成连续内存。

带外不等于自动跨进程零复制

如果你把每个 PickleBuffer 立即转成 bytes 再发送,复制仍然发生,只是位置从 pickle 内部移到了调用方。真正受益的场景通常是共享内存、支持散集 I/O 的通道、内存映射文件,或者可以直接消费缓冲协议对象的库。应把“减少 pickle 内部复制”和“整个系统端到端零复制”区分开。

共享可变缓冲区会改变对象隔离

PEP 574 明确提醒了可变性和数据共享副作用。反序列化对象可能与原数组共享底层内存,修改一方可能影响另一方。需要独立快照时,应在明确的位置复制,而不是追求绝对零复制。缓冲区提供者必须在消费完成前保持存活;确定不再需要暴露底层缓冲后,可以调用 PickleBuffer.release(),但不能在仍有消费者依赖该内存时提前释放。

PickleBuffer连续布局、生命周期、可变共享与可信输入边界
图2:PickleBuffer 只减少可避免的序列化复制;连续布局、底层所有者存活、可变共享和可信输入仍需要调用方负责。这是原创静态边界说明图。

常见故障怎么定位

现象优先检查处理方式
buffer_callback 被拒绝protocol 是否小于 5 或未显式指定使用 protocol=5
buffers 为空对象是否真的产生 PickleBuffer;回调是否返回真值检查对象的协议 5 归约实现,让带外回调返回假值
反序列化提示缺少缓冲区发送端与接收端缓冲区数量、顺序是否一致把主流与缓冲清单放入同一消息协议管理
BufferError数组或 memoryview 是否连续在边界处显式转成连续数组并接受一次必要复制
还原后修改影响原对象是否共享同一可变底层内存需要隔离时显式 copy()

安全和适用范围

pickle 不是安全的通用数据交换格式。官方文档明确警告:反序列化恶意 pickle 数据可能执行任意代码。PickleBuffer 不会改变这个安全模型;主 pickle 流和所有带外缓冲区都只能来自可信来源,并应作为同一份消息进行完整性校验。面向不可信客户端时,应选择 JSON、明确的二进制协议或经过严格约束的格式。

PickleBuffer 最适合“大块、连续、由 Python 缓冲协议对象提供、且发送端和接收端都由你控制”的数据。小对象、需要跨语言互操作的数据、非连续布局很多的对象,或者最终仍必须复制进单一网络字节串的场景,收益可能有限。先确认复制热点在大数组,再引入协议 5,通常比无差别改造全部 pickle 调用更有效。

相关问题

Python 3.14 已默认协议 5,还要显式写 protocol=5 吗?

跨 Python 3.8–3.13 运行或希望代码意图明确时仍建议显式指定。那些版本的默认协议是 4,省略参数可能让 buffer_callback 无法使用。Python 3.14 起默认协议才是 5。

PickleBuffer 和 memoryview 有什么区别?

memoryview 是通用缓冲视图;PickleBuffer 是 pickle 协议 5 识别的专用包装,同时也是缓冲提供者。它的作用是告诉 Pickler:这块数据有资格作为带外缓冲处理。

buffer_callback 为什么要返回假值?

返回假值表示“调用方会在主流之外保存或传输这块缓冲”;返回真值则要求 Pickler 把它保留在主 pickle 流中。直接使用 buffers.append 很方便,因为该方法正常返回 None。

能把 buffers 打乱后再传给 loads 吗?

不能。buffers 是按 pickle 流遇到带外引用的顺序消费的。缓冲区清单、数量和顺序都必须与发送端一致,否则对象会还原失败,甚至把错误数据绑定到错误字段。

总结

减少大数组复制要抓住三个接口:协议 5 让格式支持带外引用,buffer_callback 把 PickleBuffer 从主流中收集出来,buffers 在反序列化时按相同顺序送回。它减少的是可避免的中间副本,不会替代传输协议、连续性处理、内存生命周期、安全校验和必要的业务隔离。把这些边界一起设计好,PickleBuffer 才能真正降低大数组序列化的内存压力。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go http.Response.Body 为什么必须关闭并尽量读完Go http.Response.Body 为什么必须关闭并尽量读完
上一篇
Go http.Response.Body 为什么必须关闭并尽量读完
Go io.MultiWriter 怎么同步写入多个目标
下一篇
Go io.MultiWriter 怎么同步写入多个目标
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    329次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    387次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    381次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    350次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    175次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码