最终使读者对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字节的倍数,允许在循环中一致地使用SIMD指令而无需额外的条件检查。 这样可以写出更简单,高效、CPU缓存友好的代码。
选择这个特定的填充长度是因为它可以匹配截至2016年4月可用的最大的已知SIMD指令寄存器(Intel AVX-512)。换句话说,可以将整个64字节缓冲区加载到512位宽的SIMD寄存器中,并在使得64字节缓冲区中的所有列值获得数据级并行性。通过填充还可以让一些编译器直接生成更优化的代码。(例如安全的使用Intel的 -qopt-assume-safe-padding
).
数组的是固定长度的数据结构,在Apache Arrow中最大长度为231 – 1,之所以选择这个长度理由如下:
Null值数量是Apache内存结构的数据结构的属性,在数据结构中会记录。 Null值的数量使用32-bit有符号整数,极端情况下,Null值的数量可能与数组的长度相等,即所有的值都是Null值。
任何相对类型都可能是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是一种嵌套类型,其中每个数组下标位置包含一个可变大小的值序列,它们都具有相同的相对类型(如果想实现不同类型的结构存储在数据中可以通过Union实现,稍后描述)。
List类型的声明如 List
, T
可以是任何相对类型 (原始类型或嵌套类型).
List的表达结构如下:
T
,T
可以是原始类型或嵌套类型Offset偏移量数组中,记录了值数组中每一个元素的起始位置,数组元素的长度,计算方式如下:
slot_position = offsets[j]
slot_length = offsets[j + 1] - offsets[j] // (for 0 <= j < length)
offset偏移数组中的第一个值是0,最后一个元素是values数组的长度。
示例内存结构 List
数组
对于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
>
[[[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`)
* 长度: 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数组具有一个或多个空值,也必须使用自己的Null Bitmap来标记Null值。
物理上,struct类型为每个字段都有一个子数组。 子数组是独立的,不需要在内存中彼此相邻。
例如,下边的Struct(此处显示的字段名称为字符串,用于说明目的)
Struct <
name: String (= List),
age: Int32
>
有两个子数组,一个List 数组 (List内存结构上边介绍过) ,一个4字节的原始数值数组,逻辑类型为Int32。
示例内存结构Struct
, 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`):
* 长度: 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语义上跟Struct是类似的,差别在于Struct在内存结构上是多个数组,Union只有一个包含了不同数据类型的数组。Union中可以有带名字的字段,字段名和字段数据类型,同样是Schema的元数据的一部分,不会体现在内存结构上。
Arrow中定义了两种类型的Union:密集型Union(类似于Struct)和稀疏型Union,分别对应用不同的场景。密集型Union中,数组的每一个值,都包含了额外的5个Byte。
密集型Union内存结构如下:
最重要的一点,密集型Union中的字段如果是Struct类型,且相互不重叠,那么其开销是最小的,例如 (Union
)
示例密集型Union的内存机构
对于
Union
类型,实际值为 [{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没有offset偏移数组。在稀疏型Union中,数据数组的每一个子数组的长度与原始数据的长度是一样的,例如我们有一个List
稀疏型Union可能会比密集型Union占用更多的内存空间,但是也有自己的优点:
示例稀疏型Union内存结构
类型SparseUnion
[{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`)
* 长度: 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
[
['a', 'b'],
['a', 'b'],
['a', 'b'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['c', 'd', 'e'],
['a', 'b']
]
使用字典编码的形式
数据List (字典编码, 字典ID i)
索引数组: [0, 0, 0, 1, 1, 1, 0]
字典i
类型: List
[
['a', 'b'],
['c', 'd', 'e'],
]