跳到主要内容
版本:1.6.0

行格式

概述​

Apache Fory 行格式是一种缓存友好的随机访问二进制格式,专为高性能数据处理而设计。传统序列化格式通常需要完整反序列化,而行格式支持:

  • 随机字段访问:无需反序列化整行即可读取单个字段
  • 零拷贝操作:无需转换数据即可直接访问内存
  • 缓存友好布局:通过优化内存布局提高 CPU 缓存效率
  • 跨语言支持:Java、C++、Python 和 Rust 使用一致的二进制格式

Fory 提供两种行格式:

格式语言使用场景
标准格式Java、C++、Python、Rust跨语言兼容
紧凑格式仅 Java提高空间效率,减小行大小

格式对比​

功能标准格式紧凑格式
字段槽位大小固定 8 字节自然宽度(1、2、4 或 8 字节)
null 位图大小按 8 字节对齐按字节对齐,可利用填充空间
null 位图位置位于字段槽位之前位于字段槽位之后(末尾)
定长结构体变长区(偏移量+大小)内联在定长区
字段顺序Schema 定义的顺序按对齐要求排序
所有字段均不可空仍保留位图完全省略位图
对齐严格按 8 字节对齐宽松对齐(2、4 或 8 字节)

标准行格式​

标准格式使用统一的 8 字节字段槽位,优先保证跨语言兼容性和实现简洁性。

设计原则​

  1. 8 字节对齐:所有主要结构均按 8 字节边界对齐,以优化内存访问
  2. 定长字段槽位:每个字段使用 8 字节槽位,从而统一偏移量计算
  3. null 位图:使用位向量紧凑地跟踪 null 值
  4. 相对偏移量:变长数据使用相对偏移量定位子缓冲区

行的二进制布局​

一行结构化数据采用以下布局:

+----------------+------------------+------------------+-----+------------------+------------------+
| Null Bitmap | Field 0 Slot | Field 1 Slot | ... | Field N-1 Slot | Variable Data |
+----------------+------------------+------------------+-----+------------------+------------------+
| B bytes | 8 bytes | 8 bytes | | 8 bytes | Variable size |

null 位图​

null 位图用于标记哪些字段包含 null 值:

  • 大小:((num_fields + 63) / 64) * 8字节(向上取整到最接近的 8 字节字)
  • 编码:每一位对应一个字段索引
    • 位值1= 字段为 null
    • 位值0= 字段非 null
  • 位顺序:第一个字节的第 0 位对应字段 0

示例:对于 10 个字段,位图大小 =((10 + 63) / 64) * 8 = 8字节

字段槽位​

无论实际数据类型如何,每个字段都占用一个固定的 8 字节槽位:

  • 槽位偏移量:bitmap_size + field_index * 8
  • 定长区总大小:bitmap_size + num_fields * 8字节

不同类型的字段槽位内容:

类型类别槽位内容
定长直接存储值(以零填充)
变长编码偏移量+大小(见下文)

变长数据编码​

变长字段(字符串、数组、Map 和嵌套结构体)在槽位中存储一对偏移量和大小。该数据对解释为一个小端序 64 位值:

+---------------------------+---------------------------+
| Relative Offset | Size |
| (32 bits) | (32 bits) |
+---------------------------+---------------------------+
|<-------------- 64-bit field slot value -------------->|
  • 相对偏移量(高 32 位):相对于行基地址的偏移量
  • 大小(低 32 位):变长数据的字节数
  • 物理字节顺序:字节 0-3 存放大小,字节 4-7 存放相对偏移量

编码:

offset_and_size = (relative_offset << 32) | size

解码:

relative_offset = (offset_and_size >> 32) & 0xFFFFFFFF
size = offset_and_size & 0xFFFFFFFF

变长数据区​

变长数据存储在定长区之后:

  • 设置字段时按顺序写入数据
  • 每个变长值填充到 8 字节对齐
  • 填充字节清零,以确保输出具有确定性

数组的二进制布局​

数组存储同构元素序列:

+------------------+------------------+------------------+
| Element Count | Null Bitmap | Element Data |
+------------------+------------------+------------------+
| 8 bytes | B bytes | Variable size |

数组头部​

字段大小说明
元素数量8 字节元素个数(uint64)
null 位图((count + 63) / 64) * 8字节各元素的 null 标志

头部大小:8 + ((num_elements + 63) / 64) * 8字节

数组元素数据​

元素连续存储在头部之后:

  • 定长元素:按自然宽度存储(1、2、4 或 8 字节)
  • 变长元素:存储为 8 字节的偏移量+大小数据对

元素偏移量:header_size + element_index * element_size

数据区大小:向上取整到最接近的 8 字节边界

数组元素大小​

元素类型元素大小
bool1 字节
int81 字节
int162 字节
int324 字节
int648 字节
float324 字节
float648 字节
string/二进制8 字节(偏移量+大小)
数组/map/结构体8 字节(偏移量+大小)

Map 的二进制布局​

Map 将键值对分别存储在两个数组中:

+------------------+------------------+------------------+
| Keys Array Size | Keys Array | Values Array |
+------------------+------------------+------------------+
| 8 bytes | Variable size | Variable size |

Map 结构​

字段大小说明
键数组大小8 字节键数组的总字节数
键数组变长用于存储键的完整数组结构
值数组变长用于存储值的完整数组结构

键数组偏移量:base_offset + 8 值数组偏移量:base_offset + 8 + keys_array_size

键数组和值数组都遵循标准数组二进制布局,并且必须包含相同数量的元素。

嵌套结构体布局​

嵌套结构体以完整行结构的形式存储在变长数据区中:

  1. 父字段槽位包含指向嵌套行的偏移量+大小
  2. 嵌套行拥有自己的 null 位图和字段槽位
  3. 支持任意嵌套深度
Parent Row:
+----------------+------------------+------------------+
| Null Bitmap | ... Slots ... | Nested Row Data |
+----------------+------------------+------------------+
| ^
| offset+size |
+------------------->+

Nested Row:
+----------------+------------------+------------------+
| Null Bitmap | Field Slots | Variable Data |
+----------------+------------------+------------------+

紧凑行格式(仅 Java)​

紧凑格式通过额外优化提高空间效率,目前仅在 Java 中实现。

说明:紧凑格式仍在开发中,目前可能尚不稳定。

设计原则​

  1. 自然宽度存储:定长字段使用其自然字节宽度,而不是统一使用 8 字节
  2. 按对齐要求排序字段:根据对齐要求排列字段,以尽量减少填充
  3. 条件式 null 位图:所有字段均不可空时省略 null 位图
  4. 内联定长结构体:所有字段均为定长字段的嵌套结构体以内联方式存储

紧凑行的二进制布局​

+------------------+------------------+-----+------------------+----------------+------------------+
| Field 0 Value | Field 1 Value | ... | Field N-1 Value | Null Bitmap | Variable Data |
+------------------+------------------+-----+------------------+----------------+------------------+
| W0 bytes | W1 bytes | | WN-1 bytes | B bytes (opt) | Variable size |

与标准格式的主要区别​

  1. 字段槽位大小:每个字段使用其自然宽度(Wi = 类型宽度;变长字段为 8)
  2. null 位图位置:位于字段槽位之后,可以利用对齐填充空间
  3. 字段顺序:按对齐要求排序(8 字节 → 4 字节 → 2 字节 → 1 字节 → 变长)
  4. 条件式位图:所有字段均不可空时完全省略位图

null 位图(紧凑格式)​

  • 大小:(num_nullable_fields + 7) / 8字节(按字节对齐,而不是按 8 字节对齐)
  • 省略条件:所有字段均为基本类型或不可空字段
  • 位置:位于所有定长字段槽位之后,可以使用对齐填充空间

字段排序算法​

字段按以下方式排序,以尽量减少填充并优化对齐:

Priority order (highest to lowest):
1. Fields with 8-byte alignment (int64, float64, variable-width)
2. Fields with 4-byte alignment (int32, float32)
3. Fields with 2-byte alignment (int16)
4. Fields with 1-byte alignment (int8, bool)

在每个对齐分组内,较大的字段排在前面。

定长结构体内联​

所有字段均为定长字段的嵌套结构体以内联方式存储在父行中:

标准格式(包含 2 个 int32 字段的嵌套结构体):

Parent slot: [offset (4 bytes) | size (4 bytes)] → Points to nested row (8+ bytes elsewhere)

紧凑格式(同一个嵌套结构体):

Parent slot: [int32 field 0 | int32 field 1] → 8 bytes total, inline

这样可以消除定长嵌套结构的偏移量+大小间接寻址。

定长宽度计算​

字段的定长宽度通过递归方式确定:

  • 基本类型:自然字节宽度(1、2、4 或 8)
  • 结构体类型:所有子字段定长宽度之和(仅当所有子字段均为定长字段)
  • 变长类型(string、数组、map):返回 -1(使用 8 字节偏移量+大小槽位)
fixed_width(field) =
if primitive: type_width
if struct and all_children_fixed: header_bytes + sum(fixed_width(child) for each child)
else: -1 (variable, uses 8-byte slot)

紧凑数组的二进制布局​

+------------------+------------------+------------------+
| Element Count | Null Bitmap | Element Data |
+------------------+------------------+------------------+
| 4 bytes | B bytes (opt) | Variable size |

紧凑数组头部​

字段大小说明
元素数量4 字节元素个数(int32)
null 位图(count + 7) / 8字节(可选)各元素的 null 标志

头部大小计算:

header_size = 4 + (element_nullable ? (num_elements + 7) / 8 : 0)

// Round to 8-byte boundary only if element width is 8-byte aligned
if (fixed_width % 8 == 0):
header_size = round_to_8_bytes(header_size)

与标准数组的主要区别​

  1. 元素数量:使用 4 字节,而不是 8 字节
  2. null 位图:按字节对齐;元素不可空时省略
  3. 定长结构体:定长结构体元素以内联方式存储

通用规范​

以下规范同时适用于标准格式和紧凑格式。

类型编码​

基本类型​

类型宽度编码
bool1 字节0x00(false)或0x01(true)
int81 字节二进制补码
int162 字节二进制补码,小端序
int324 字节二进制补码,小端序
int648 字节二进制补码,小端序
float324 字节IEEE 754 单精度
float648 字节IEEE 754 双精度

时间类型​

类型宽度编码
timestamp8 字节自 Unix epoch 起的微秒数(int64)
date324 字节自 Unix epoch 起的天数(int32)
duration8 字节以微秒表示的时长(int64)

字符串与二进制数据​

  • 编码:字符串使用 UTF-8,二进制数据使用原始字节
  • 存储:字段槽位存储偏移量+大小数据对,数据存储在变长区
  • 填充:标准格式将数据填充到 8 字节对齐,紧凑格式按自然宽度对齐

null 处理​

行中的 null 处理​

  • null 字段在 null 位图中的对应位设置为 1
  • null 字段的槽位内容在标准格式中未定义,在紧凑格式中清零
  • 读取 null 字段会返回 null/空值标志

数组中的 null 处理​

  • null 元素在数组 null 位图中的对应位设置为 1
  • null 元素的元素数据未定义
  • 紧凑格式:元素不可空时省略位图

变长数据的 null 语义​

从 null 字段读取变长数据时:

  • 返回大小 -1 或等价的 null 标志
  • 不执行数据访问

对齐与填充​

标准格式对齐​

  1. null 位图:大小向上取整到 8 字节边界
  2. 字段槽位:每个槽位始终为 8 字节
  3. 变长数据:每个值填充到 8 字节边界
  4. 数组数据:整个数据区填充到 8 字节边界

紧凑格式对齐​

  1. 字段槽位:使用自然宽度(1、2、4 或 8 字节)
  2. null 位图:按字节对齐,位于字段之后
  3. 变长数据:仅在需要时填充到 8 字节边界
  4. 头部:可以使用更宽松的对齐方式以降低开销

填充字节​

  • 所有填充字节都必须设置为零
  • 确保序列化输出具有确定性
  • 防止未初始化内存造成信息泄露

大小计算​

标准行大小​

row_size = bitmap_size + num_fields * 8 + variable_data_size

where:
bitmap_size = ((num_fields + 63) / 64) * 8
variable_data_size = sum of (padded_size for each variable field)
padded_size = ((size + 7) / 8) * 8

紧凑行大小​

row_size = fixed_region_size + bitmap_size + variable_data_size

where:
fixed_region_size = sum of (fixed_width(field) or 8 for each field)
bitmap_size = all_non_nullable ? 0 : (num_nullable_fields + 7) / 8
// May be rounded to 8-byte boundary if has variable fields

标准数组大小​

array_size = header_size + fixed_data_size + variable_data_size

where:
header_size = 8 + ((num_elements + 63) / 64) * 8
element_slot_size = natural width for fixed-width elements, otherwise 8
fixed_data_size = ((num_elements * element_slot_size + 7) / 8) * 8
variable_data_size = sum of (padded_size for each non-null variable-width element)
padded_size = ((size + 7) / 8) * 8

紧凑数组大小​

array_size = header_size + data_size

where:
header_size = 4 + (element_nullable ? (num_elements + 7) / 8 : 0)
// header_size rounded to 8 if element_width % 8 == 0
data_size = num_elements * element_width

Map 大小​

map_size = 8 + keys_array_size + values_array_size

汇总表​

布局汇总​

组成部分标准格式紧凑格式
行头部((N + 63) / 64) * 8字节0 或(N + 7) / 8字节(位于末尾)
行字段槽位N * 8字节sum(field_widths)字节
数组头部8 + ((E + 63) / 64) * 8字节4 + (E + 7) / 8字节(元素可空时)
数组元素按 8 字节对齐的槽位以及变长主体E * element_width
Map 头部8 字节8 字节
偏移量+大小数据对8 字节u64:(offset << 32) | size8 字节(相同)

其中,N = 字段数,E = 元素数。

类型宽度汇总​

类别存储宽度标准槽位紧凑槽位
bool1 字节8 字节1 字节
int81 字节8 字节1 字节
int162 字节8 字节2 字节
int324 字节8 字节4 字节
int648 字节8 字节8 字节
float324 字节8 字节4 字节
float648 字节8 字节8 字节
string/二进制变长8 字节8 字节
数组变长8 字节8 字节
map变长8 字节8 字节
结构体变长8 字节内联或 8 字节

实现说明​

字节序​

  • 所有多字节整数均以小端序格式存储
  • 浮点值使用 IEEE 754 位表示形式,并按小端字节序存储

内存安全​

  • 写入器必须将填充字节清零,以防止信息泄露
  • 读取器必须先校验偏移量和大小,再访问数据
  • 对于不可信输入,必须检查缓冲区边界

性能注意事项​

标准格式:

  • 固定的 8 字节槽位只需简单的算术运算即可实现 O(1) 字段访问
  • 8 字节对齐可以优化 CPU 缓存行的使用
  • 最适合跨语言互操作

紧凑格式:

  • 更小的行可以降低内存带宽占用
  • 字段排序可尽量减少填充浪费
  • 内联结构体消除了指针追踪
  • 在某些架构上,宽松对齐可能产生少量 CPU 开销

各格式的适用场景​

场景推荐格式
跨语言数据交换标准格式
仅使用 Java 且内存受限紧凑格式
包含许多小型基本类型字段紧凑格式
包含许多嵌套定长结构体紧凑格式
要求最高读取性能标准格式
与 Java/C++/Python/Rust 互操作标准格式