菜单

内存管理

相关源文件

Protocol Buffers 提供了高效的内存管理机制,可以在保持简单 API 的同时优化性能和资源使用。本页面将介绍 Protocol Buffers 如何管理内存,重点关注 Arena 分配系统、字段内存布局以及库中使用的优化策略。

Arena 分配系统

Arena 分配系统是 Protocol Buffers 内存管理的核心,它通过从预分配的内存块中分配对象并允许一次性释放所有对象,从而带来显著的性能优势。

架构

Arena 系统主要包含三个组件:

  • Arena:提供分配方法的公共 API 类。
  • ThreadSafeArena:提供线程安全性的内部实现。
  • SerialArena:管理内存块的内部每个线程的分配器。

创建 Arena 对象时,它会在内部创建一个 ThreadSafeArena,该 ThreadSafeArena 管理一个或多个 SerialArena 实例(每个访问 Arena 的线程一个)。每个 SerialArena 以递增大小的块分配内存,从一个小的初始块开始,并增长到可配置的最大块大小。

Arena 的工作原理

  1. 块分配:Arena 以越来越大的块分配内存。
  2. 对象放置:对象从当前块分配。
  3. 块增长:当一个块已满时,会分配一个大小加倍的新块(最高到最大值)。
  4. 批量释放:当 Arena 被销毁时,所有分配的对象都会一次性释放。

Arena API 和用法

在 Arena 上创建对象

  • Arena::Create<T>():在 Arena 上创建类型为 T 的对象。
  • Arena::CreateMessage<T>():在 Arena 上创建类型为 T 的消息。
  • Arena::Own():将现有对象的拥有权转移给 Arena。

优点

  1. 性能:显著减少分配/释放开销。
  2. 内存局部性:一起分配的对象会聚集在内存中,改善缓存行为。
  3. 简单性:无需跟踪和手动释放单个对象。

来源:src/google/protobuf/arena.h156-773 src/google/protobuf/arena.h38-105 src/google/protobuf/generated_message_reflection.cc95-102

代码到内存的映射

下图展示了 Protocol Buffers 代码实体如何映射到内存管理概念。

此映射显示了公共 API 与管理内存的内部实现类之间的关系。

来源:src/google/protobuf/arena.h166-195 src/google/protobuf/serial_arena.h src/google/protobuf/thread_safe_arena.h

Repeated Field 内存管理

Protocol Buffers 使用专门的容器来处理重复字段,并针对内存使用和性能进行了优化。

RepeatedField 和 RepeatedPtrField

处理重复字段的两个主要类:

  • RepeatedField<T>:用于基本类型(int32、float 等)。
  • RepeatedPtrField<T>:用于消息类型和字符串。

小型对象优化 (SOO)

对于小的重复字段,Protocol Buffers 使用小型对象优化来避免堆分配。

SOO 优化:

  • 将小型数组直接存储在 RepeatedField 对象中。
  • 对于短数组(通常为 1-3 个元素)避免堆分配。
  • 在需要时自动过渡到堆存储。

增长策略

当重复字段需要增长时:

  1. 分配一个新的、更大的数组(通常是大小的两倍)。
  2. 复制现有元素。
  3. 释放旧数组。
  4. 增加容量。

增长策略旨在平衡:

  • 内存效率(不过多分配过多的容量)。
  • 性能(尽量减少重分配次数)。

RepeatedField 的内存布局

来源:src/google/protobuf/repeated_field.h237-866 src/google/protobuf/repeated_ptr_field.h137-902

String 内存管理

Protocol Buffers 提供了多种字符串存储策略,以优化不同场景。

字符串存储选项

字符串存储类比较

最佳用途内存特性权衡
ArenaStringPtr通用用法字符串指针简单,灵活
InlinedStringField小型字符串字符串内容内联到消息中为小型字符串避免间接引用
MicroString极小型字符串高度优化的字符串对于小于 15 字节的字符串速度最快
absl::Cord大型字符串树状结构对大型字符串高效,对小型字符串开销较大

实现细节

ArenaStringPtr 是标准的字符串表示。

InlinedStringField 将小型字符串直接存储在消息中。

来源:src/google/protobuf/arenastring.h src/google/protobuf/inlined_string_field.h src/google/protobuf/micro_string.h

Message 内存布局

Protocol Buffers 消息具有基于字段类型和选项的优化内存布局。

基本消息布局

字段存在性跟踪

Protocol Buffers 使用位字段(_has_bits_)来高效跟踪哪些可选字段已被设置。

  • 每个可选字段在 _has_bits_ 数组中占有一位。
  • 设置字段会设置其相应的位。
  • 清除字段会清除其位。
  • 必需字段和重复字段不使用 has bits。

按字段类型的内存布局

字段类型存储内存模式
标量(int32、bool)直接内联存储在消息对象中。
字符串间接指向字符串内容(带优化)。
消息间接指向子消息对象。
重复间接指向 RepeatedField 或 RepeatedPtrField。
枚举(Enum)直接存储为整数值。
Map间接指向专门的哈希映射实现。

延迟字段解析

对于可能不会被访问的大型嵌套消息,Protocol Buffers 支持延迟解析。

  • 存储序列化数据而非已解析的消息。
  • 仅在访问字段时进行解析。
  • 减少未使用的消息的内存使用。

来源:src/google/protobuf/generated_message_reflection.cc392-547 src/google/protobuf/message.h241-411 src/google/protobuf/message_lite.h44-958

Extensions 内存管理

Extensions 允许在不修改消息定义的情况下向消息添加字段,这需要特殊的内存管理考虑。

ExtensionSet

Extensions 存储在 ExtensionSet 结构中。

主要特性

  • 仅在实际使用扩展时分配。
  • 高效管理不同类型的扩展字段。
  • 支持 Arena 分配。
  • 内部使用类似 map 的结构进行查找。

Extension 内存优化

Extension 内存通过以下方式优化:

  1. 延迟分配 - 仅在需要时创建。
  2. 基于字段类型的有效存储。
  3. 与 Arena 兼容的设计以获得更好的性能。
  4. 为重复扩展使用专用容器。

来源:src/google/protobuf/extension_set.h177-210 src/google/protobuf/extension_set.cc101-146

Arena 与 Message 类的集成

Protocol Buffers 消息设计为与 Arena 分配系统高效协同工作。

Arena 构造函数特性

Protocol Buffers 使用特性来确定如何分配和释放对象。

这些特性支持:

  • 在 Arena 上直接构造对象。
  • 在销毁 Arena 时跳过平凡类型的析构函数。
  • 与 Arena 优化的移动语义。

在消息中使用 Arena

生成的消息通过以下方式与 Arena 集成:

  1. Arena 感知的构造函数。
  2. 解析方法中的特殊处理。
  3. 考虑 Arena 分配的优化序列化。
  4. 用于 Arena 分配的自定义工厂方法。

来源: src/google/protobuf/arena.h196-226 src/google/protobuf/message_lite.h236-280

内存优化策略

除了 Arena 之外,Protocol Buffers 还采用了多种内存优化策略。

缓存大小

每个消息都维护一个缓存的大小,以避免重复计算序列化大小。

这项优化

  • 避免了重复计算序列化大小
  • 仅在消息更改时更新
  • 使用原子操作保证线程安全

解析优化

内存高效的解析技术包括:

  • 在可能的情况下进行零拷贝解析
  • 为已知字段大小预先分配空间
  • 针对常见消息模式的专用解析路径
  • 使用解析表的尾递归优化

扩展管理

通过以下方式优化扩展:

  • 仅在需要时分配扩展存储
  • 对不同类型使用紧凑的表示
  • 支持 Arena 分配以处理扩展
  • 专门的重复扩展处理

来源: src/google/protobuf/message_lite.cc175-243 src/google/protobuf/parse_context.h66-102

内存管理最佳实践

为了优化 Protocol Buffers 的内存使用:

  1. 使用 Arena 分配:处理大量消息时,使用 Arena 分配可以降低分配开销。

  2. 考虑字符串存储选项:对于包含许多小字符串的消息,请考虑字符串内联或 MicroString 选项。

  3. 批量处理:使用单个 Arena 批量处理消息,以提高局部性并减少碎片。

  4. 使用移动语义:在适当的情况下,使用移动语义以避免复制。

  5. 监控内存使用:使用 SpaceUsed() 方法来跟踪内存消耗。

来源: src/google/protobuf/arena.h282-290 src/google/protobuf/message.h314-327