为何Protobuf文件在R中反序列化后内存占用大幅降低?
我调用某API下载了一个包含多条消息的大型Protobuf文件(约2.1GB),使用DecodeVarint32函数反序列化后,得到的R对象体积却极小(不足30MB),且所有消息均已正确反序列化,数据完整。反序列化前httr请求内容的大小为2.1GB,反序列化后R对象大小仅1.5MB。
【补充示例】:子集数据可复现该情况:rg对象大小为1.1MB,反序列化后的outputlist(含两条消息)仅1904字节。复现代码及DecodeVarint32函数如下:
复现代码
rg = readRDS("rg.RDS") library(dplyr) library(RProtoBuf) readProtoFiles("message2.proto", package = 'RProtoBuf') alldata = rg$content n = 1 i = 1 outputlist = list() while (n < length(alldata)) { pos = DecodeVarint32(alldata,as.integer(n)) clen = pos[[1]] n = pos[[2]] nend = n + clen - 1 tmp = alldata[n:nend] outputlist[[i]] = read(Spectrum, tmp) n = nend + 1 i = i+1 } object.size(outputlist)
DecodeVarint32函数代码
library(bitops) DecodeVarint32 = function(buffer, pos){ mask = (2 ^ 32 - 1) result = 0 %>% as.raw shift = 0 while(TRUE){ b = buffer[pos] result = bitOr(as.numeric(result), bitShiftL( bitAnd(as.numeric(b), as.numeric(0x7f)), shift)) pos = pos + 1 if(!bitAnd(b,0x80)){ result = bitAnd(result,mask) result = as.integer(result) return(list(result, pos)) } shift = shift + 7 if(shift >= 64){ stop("Too many bytes when decoding varint") } } }
这种巨大的体积差异主要来自Protobuf二进制格式与R内存对象的存储特性差异,具体可以从以下几点分析:
Protobuf二进制的元数据开销:Protobuf的每个字段都需要携带字段编号(varint编码)和类型标识,这些元数据会在每条消息中重复存储。如果响应包含大量结构相同的消息,这些重复的元数据会累积出巨大的体积。而反序列化后的R对象是结构化的列表/对象,字段结构由
.proto文件定义,不需要重复存储字段编号等元信息,只保留实际数据值,因此节省了大量空间。重复数据的内存共享:R的内存管理机制会自动共享重复的数据对象。如果Protobuf响应中存在大量重复的内容(比如多条消息包含相同的字符串、数值数组或子消息),Protobuf的二进制流会为每条消息独立存储这些数据,而R会将重复数据只存储一次,通过引用方式访问,大幅降低内存占用。这也是示例中1.1MB的二进制数据反序列化后仅1.9KB的核心原因之一。
数据编码的存储效率差异:
- Protobuf使用varint编码整数,对于较大的整数(超过2^28),varint需要占用5字节存储,而R中整数通常以固定4字节的原生格式存储,单值占用空间更小;
- 对于重复字段,Protobuf需要为每个元素单独编码(包括字段标识),而R会将重复字段存储为紧凑的向量结构,不需要为每个元素附加元数据,存储效率更高。
无意义填充或冗余数据:如果API返回的Protobuf响应中包含未使用的填充字节、预留字段或重复的空值字段(尽管Protobuf默认不存储默认值,但某些实现可能会额外写入),这些内容会增加二进制体积,但反序列化后不会被保留为有效数据,进一步拉大体积差。
内容的提问来源于stack exchange,提问作者Julien Sade

