本文的目标
- 清楚的描述Apach相对类型(原始类型和初始嵌套类型集),到达可以实现的程度
- 每一种相对类型的内存结构和随机访问的模式
- Null值的表达
最终使读者对Apache Arrow的底层有一个大致清晰的了解,如果能够从中了解到一点硬件级性能优化的概念,那么便是超出了期望。
字节顺序
Apache Arrow默认使用Little-Endian,在Apache Arrow的Schema元数据中有一个endianness 字段来表示是Little-Endian还是Big-Endian,edianness的典型值是生成Arrow数据的系统的所使用的的字节顺序,也就是说具有相同字节顺序的平台可以交换数据。在最开始的实现中,如果是不同的endianness,则直接返回错误。在本文中Apache Arrow主要考虑Little-Endian,所有的测试也是围绕着Little-Endian。
最终也许会考虑实现在Little-Endian和Big-Endian无缝转换,但不是现在的重点。
字节对齐和填充
如上所述,所有缓冲区必须在8字节边界的内存中对齐,并填充为8字节倍数的长度。 对齐要求遵循优化内存访问的最佳实践:
- 数字数组中的元素将保证通过对齐访问来检索。
- 在某些体系结构上,对齐可以帮助限制部分使用的缓存行。
- 对于超过64字节的数据结构,英特尔性能指南建议使用64字节对齐(这将是箭头阵列的常见情况)。
建议填充为64字节的倍数,允许在循环中一致地使用SIMD指令而无需额外的条件检查。 这样可以写出更简单,高效、CPU缓存友好的代码。
选择这个特定的填充长度是因为它可以匹配截至2016年4月可用的最大的已知SIMD指令寄存器(Intel AVX-512)。换句话说,可以将整个64字节缓冲区加载到512位宽的SIMD寄存器中,并在使得64字节缓冲区中的所有列值获得数据级并行性。通过填充还可以让一些编译器直接生成更优化的代码。(例如安全的使用Intel的 -qopt-assume-safe-padding
).
数组长度
数组的是固定长度的数据结构,在Apache Arrow中最大长度为231 – 1,之所以选择这个长度理由如下:
- 保证Java和其他语言之间的兼容性,不同的语言对无符号整数的支持不同。
- 鼓励开发人员使用更小的数组来组成更大的数组型的数据结构(超过231 – 1),而不是申请非常大的连续内存块。
Null值数量
Null值数量是Apache内存结构的数据结构的属性,在数据结构中会记录。 Null值的数量使用32-bit有符号整数,极端情况下,Null值的数量可能与数组的长度相等,即所有的值都是Null值。
Null bitmaps
任何相对类型都可能是Null值,包括原子类型和嵌套类型。
包含Null值的数据必需包含一个Null Bitmap,用来记录数组中的每个数组下标的位置是否是Null值,Bitmap的长度至少要等于数组的长度,并且为64字节的倍数(上边讨论过为什么是64字节的倍数)。
数组的每一个index的值是否为Null值全部记录在bitmap中,1表示是该位置的值不是Null值,0表示该位置的值是空值。Bitmap在最开始申请完初始化完毕之后,所有的值都是0(Bitmap默认全是0),同时也进行了内存对齐和填充。
判断一个是否为null值:
is_valid[j] -> bitmap[j / 8] & (1 << (j % 8))
在Apache Arrow中使用最低有效位(LSB),在使用bit的时候,,从右向左,读取字节中每一个bit。如下所示
values = [0, 1, null, 2, null, 3]
bitmap
j mod 8 7 6 5 4 3 2 1 0
0 0 1 0 1 0 1 1
当数组中不包含Null值的时候,也可以在数据结构中不分配Null Bitmap内存区块。为方便起见,在语言中具体实现的时候可以选择总是分配,但是在共享内存时应该注意这一点。
包含嵌套类型的数组,数组中的每一个子元素都有自己的Null Bitmap,所以在当前数组Null Bitmap中并不用考虑数组子元素是否有Null值。
原始类型数组
示例1:包含Null值的Int32数组
[1, null, 2, 4, 8]内存结构如下:
* 长度: 5, Null 数量: 1
* Null bitmap buffer:
|Byte 0 (validity bitmap) | Bytes 1-63 |
|-------------------------|-----------------------|
| 00011101 | 0 (padding) |
* 值Buffer:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-19 | Bytes 20-63 |
|------------|------------|-------------|-------------|-------------|-------------|
| 1 | unspecified | 2 | 4 | 8 | unspecified |
示例2:不包含Null值的Int32数组
[1, 2, 3, 4, 8] 存在两种可能的内存结构:
包含Null Bitmap内存结构:
* 长度: 5, Null 数量: 0
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-----------------------|
| 00011111 | 0 (padding) |
* 数据数组:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | bytes 12-15 | bytes 16-19 | Bytes 20-63 |
|------------|-------------|-------------|-------------|-------------|-------------|
| 1 | 2 | 3 | 4 | 8 | unspecified |
省略Null bitmap的结构:
* 长度5, Null值数量: 0
* Null bitmap buffer: 可省略
* 数据数组:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | bytes 12-15 | bytes 16-19 | Bytes 20-63 |
|------------|-------------|-------------|-------------|-------------|-------------|
| 1 | 2 | 3 | 4 | 8 | unspecified |
List类型
List是一种嵌套类型,其中每个数组下标位置包含一个可变大小的值序列,它们都具有相同的相对类型(如果想实现不同类型的结构存储在数据中可以通过Union实现,稍后描述)。
List类型的声明如 List<T>
, T
可以是任何相对类型 (原始类型或嵌套类型).
List的表达结构如下:
- 一个包含值的数组,数组元素的类型为
T
,T
可以是原始类型或嵌套类型 - 一个offset偏移量数组,长度为 ,正好等于上层数组的长度+1,也就意味着值数组的长度为-1
Offset偏移量数组中,记录了值数组中每一个元素的起始位置,数组元素的长度,计算方式如下:
slot_position = offsets[j]
slot_length = offsets[j + 1] - offsets[j] // (for 0 <= j < length)
offset偏移数组中的第一个值是0,最后一个元素是values数组的长度。
示例内存结构 List<Char>
数组
对于List [[‘j’, ‘o’, ‘e’], null, [‘m’, ‘a’, ‘r’, ‘k’], []],内存结构如下:
* 长度: 4, Null 数量: 1
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-----------------------|
| 00001101 | 0 (padding) |
* Offsets偏移量数组(int32)
| Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-19 | Bytes 20-63 |
|------------|-------------|-------------|-------------|-------------|-------------|
| 0 | 3 | 3 | 7 | 7 | unspecified |
* 数据数组 (char 数组):
* 长度: 7, Null 数量: 0
* Null bitmap buffer: 可省略
| Bytes 0-6 | Bytes 7-63 |
|------------|-------------|
| joemark | unspecified |
示例内存结构List<List<byte>>
[[[1, 2], [3, 4]], [[5, 6, 7], null, [8]], [[9, 10]]]内存结构如下:
* 长度3
* Nulls 数量: 0
* Null bitmap buffer: 可省略
* Offsets偏移量数组(int32)
| Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-63 |
|------------|------------|------------|-------------|-------------|
| 0 | 2 | 5 | 6 | unspecified |
* 数据数组 (子元素为`List<byte>`)
* 长度: 6, Null 数量: 1
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-------------|
| 00110111 | 0 (padding) |
* Offsets偏移量buffer(int32)
| Bytes 0-27 | Bytes 28-63 |
|----------------------|-------------|
| 0, 2, 4, 7, 7, 8, 10 | unspecified |
* 数据数组 (子元素为bytes):
* 长度: 10, Null 数量: 0
* Null bitmap buffer: 可省略
| Bytes 0-9 | Bytes 10-63 |
|-------------------------------|-------------|
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 | unspecified |
Struct类型
Struct是一种嵌套类型,包含了一个或者多个字段。通常,字段具有名称,但字段名称及其类型是类型元数据的一部分,不包含在物理内存结构中。
struct数组的值没有任何额外的已分配物理存储。 如果Struct数组具有一个或多个空值,也必须使用自己的Null Bitmap来标记Null值。
物理上,struct类型为每个字段都有一个子数组。 子数组是独立的,不需要在内存中彼此相邻。
例如,下边的Struct(此处显示的字段名称为字符串,用于说明目的)
Struct <
name: String (= List<char>),
age: Int32
>
有两个子数组,一个List 数组 (List内存结构上边介绍过) ,一个4字节的原始数值数组,逻辑类型为Int32。
示例内存结构Struct<List<char>, Int32>
[{‘joe’, 1}, {null, 2}, null, {‘mark’, 4}]的结构如下:
* 长度: 4, Null值数量: 1
* Null bitmap buffer:
|Byte 0 (validity bitmap) | Bytes 1-63 |
|-------------------------|-----------------------|
| 00001011 | 0 (padding) |
* 数据数组:
* field-0 数组(`List<char>`):
* 长度: 4, Null 数量: 2
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-----------------------|
| 00001001 | 0 (padding) |
* Offsets偏移量 buffer:
| Bytes 0-19 |
|----------------|
| 0, 3, 3, 3, 7 |
* 数据数组:
* 长度: 7, Null 数量: 0
* Null bitmap buffer: 可省略
* 值buffer:
| Bytes 0-6 |
|----------------|
| joemark |
* field-1 数组(int32 数组):
* 长度: 4, Null 数量: 1
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-----------------------|
| 00001011 | 0 (padding) |
* Value Buffer:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-63 |
|------------|-------------|-------------|-------------|-------------|
| 1 | 2 | unspecified | 4 | unspecified |
密集型Union类型
密集型Union语义上跟Struct是类似的,差别在于Struct在内存结构上是多个数组,Union只有一个包含了不同数据类型的数组。Union中可以有带名字的字段,字段名和字段数据类型,同样是Schema的元数据的一部分,不会体现在内存结构上。
Arrow中定义了两种类型的Union:密集型Union(类似于Struct)和稀疏型Union,分别对应用不同的场景。密集型Union中,数组的每一个值,都包含了额外的5个Byte。
密集型Union内存结构如下:
- 每个相对类型对应一个子数组
- Types 类型Buffer:类型为8-bit有符号整数的一段buffer,从0开始枚举Union中每一个类型组合,总共可以枚举127个类型,对于可能超过127个类型组合的Union,可以通过设计成Union嵌套Union来绕过127的相对类型的限制。
- Offset偏移Buffer:类型为32-bit有符号整数的一段buffer,表示types类型buffer中每一个类型对应的数据数组中的index。
最重要的一点,密集型Union中的字段如果是Struct类型,且相互不重叠,那么其开销是最小的,例如 (Union<s1: Struct1, s2: Struct2, s3: Struct3, ...>
)
示例密集型Union的内存机构
对于
Union<f: float, i: int32>
类型,实际值为 [{f=1.2}, null, {f=3.4}, {i=5}]
*
长度: 4, Null 数量: 1
* Null bitmap buffer:
|Byte 0 (validity bitmap) | Bytes 1-63 |
|-------------------------|-----------------------|
|00001101 | 0 (padding) |
* Types类型buffer:
|Byte 0 | Byte 1 | Byte 2 | Byte 3 | Bytes 4-63 |
|---------|-------------|----------|----------|-------------|
| 0 | unspecified | 0 | 1 | unspecified |
* Offset偏移量 buffer:
|Byte 0-3 | Byte 4-7 | Byte 8-11 | Byte 12-15 | Bytes 16-63 |
|---------|-------------|-----------|------------|-------------|
| 0 | unspecified | 1 | 0 | unspecified |
* 数据数组:
* Field-0 array (f: float):
* 长度: 2, Null值: 0
* Null bitmap buffer: 可省略
* Value Buffer:
| Bytes 0-7 | Bytes 8-63 |
|-----------|-------------|
| 1.2, 3.4 | unspecified |
* Field-1 array (i: int32):
* 长度: 1, Null值数量: 0
* Null bitmap buffer: 可省略
* Value Buffer:
| Bytes 0-3 | Bytes 4-63 |
|-----------|-------------|
| 5 | unspecified |
稀疏型Union类型
稀疏型Union跟密集型Union的结构几乎是一样了,除了一点,稀疏型Union没有offset偏移数组。在稀疏型Union中,数据数组的每一个子数组的长度与原始数据的长度是一样的,例如我们有一个List<Union>,list的长度=数据数组中每一个子数组的长度,在下边的示例中可以看到。
稀疏型Union可能会比密集型Union占用更多的内存空间,但是也有自己的优点:
- 在某些情况下,对向量运算更加友好
- 因为数据数组的长度与原始数据的数组长度是相等的,所以可以省略Offset 偏移量Buffer的定义
示例稀疏型Union内存结构
类型SparseUnion<u0: Int32, u1: Float, u2: List<Char>>,
[{u0=5}, {u1=1.2}, {u2=’joe’}, {u1=3.4}, {u0=4}, {u2=’mark’}],union数组的长度=6,数据数组中的子数组的长度=6,内存结构如下:
* 长度: 6, Null 值数量: 0
* Null bitmap buffer: 可省略
* Types类型buffer:
| Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Bytes 6-63 |
|------------|-------------|-------------|-------------|-------------|--------------|-----------------------|
| 0 | 1 | 2 | 1 | 0 | 2 | unspecified (padding) |
* 数据数组:
* u0 (Int32):
* 长度: 6, Null值数量: 4
* Null bitmap buffer:
|Byte 0 (validity bitmap) | Bytes 1-63 |
|-------------------------|-----------------------|
|00010001 | 0 (padding) |
* Value buffer:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-19 | Bytes 20-23 | Bytes 24-63 |
|------------|-------------|-------------|-------------|-------------|--------------|-----------------------|
| 5 | unspecified | unspecified | unspecified | 4 | unspecified | unspecified (padding) |
* u1 (float):
* 长度: 6, Null 数量: 4
* Null bitmap buffer:
|Byte 0 (validity bitmap) | Bytes 1-63 |
|-------------------------|-----------------------|
| 00001010 | 0 (padding) |
* Value buffer:
|Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-19 | Bytes 20-23 | Bytes 24-63 |
|-------------|-------------|-------------|-------------|-------------|--------------|-----------------------|
| unspecified | 1.2 | unspecified | 3.4 | unspecified | unspecified | unspecified (padding) |
* u2 (`List<char>`)
* 长度: 6, Null 数量: 4
* Null bitmap buffer:
| Byte 0 (validity bitmap) | Bytes 1-63 |
|--------------------------|-----------------------|
| 00100100 | 0 (padding) |
* Offsets偏移 buffer (int32)
| Bytes 0-3 | Bytes 4-7 | Bytes 8-11 | Bytes 12-15 | Bytes 16-19 | Bytes 20-23 | Bytes 24-27 | Bytes 28-63 |
|------------|-------------|-------------|-------------|-------------|-------------|-------------|-------------|
| 0 | 0 | 0 | 3 | 3 | 3 | 7 | unspecified |
* 数据数组(char array):
* 长度: 7, Null 数量: 0
* Null bitmap buffer:不可省略
| Bytes 0-7 | Bytes 8-63 |
|------------|-----------------------|
| joemark | unspecified (padding) |
字典编码
当字段使用了字典编码的时候,值使用Int32数组表示,数组中的元素(Int32)表示的是数值在字典中的索引。
例如我们有一个数据如下
类型: List<String>
[
['a', 'b'],
['a', 'b'],
['a', 'b'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['a', 'b']
]
使用字典编码的形式
数据List<String> (字典编码, 字典ID i)
索引数组: [0, 0, 0, 1, 1, 1, 0]
字典i
类型: List<String>
[
['a', 'b'],
['c', 'd', 'e'],
]