Apache Arrow的内存结构

本文的目标

  1. 清楚的描述Apach相对类型(原始类型和初始嵌套类型集),到达可以实现的程度
  2. 每一种相对类型的内存结构和随机访问的模式
  3. 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-EndianBig-Endian无缝转换,但不是现在的重点。

字节对齐和填充

如上所述,所有缓冲区必须在8字节边界的内存中对齐,并填充为8字节倍数的长度。 对齐要求遵循优化内存访问的最佳实践:

  1. 数字数组中的元素将保证通过对齐访问来检索。
  2. 在某些体系结构上,对齐可以帮助限制部分使用的缓存行。
  3. 对于超过64字节的数据结构,英特尔性能指南建议使用64字节对齐(这将是箭头阵列的常见情况)。

建议填充为64字节的倍数,允许在循环中一致地使用SIMD指令而无需额外的条件检查。 这样可以写出更简单,高效、CPU缓存友好的代码。

选择这个特定的填充长度是因为它可以匹配截至2016年4月可用的最大的已知SIMD指令寄存器(Intel AVX-512)。换句话说,可以将整个64字节缓冲区加载到512位宽的SIMD寄存器中,并在使得64字节缓冲区中的所有列值获得数据级并行性。通过填充还可以让一些编译器直接生成更优化的代码。(例如安全的使用Intel -qopt-assume-safe-padding).

数组长度

数组的是固定长度的数据结构,在Apache Arrow中最大长度为231 – 1,之所以选择这个长度理由如下:

  1. 保证Java和其他语言之间的兼容性,不同的语言对无符号整数的支持不同。
  2. 鼓励开发人员使用更小的数组来组成更大的数组型的数据结构(超过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 可以是任何相对类型 (原始类型或嵌套类型).

List的表达结构如下:

  1. 一个包含值的数组,数组元素的类型为TT可以是原始类型或嵌套类型
  2. 一个offset偏移量数组,长度为2^{31} ,正好等于上层数组的长度+1,也就意味着值数组的长度为2^{31}-1

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数组的值没有任何额外的已分配物理存储。 如果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类型

密集型Union语义上跟Struct是类似的,差别在于Struct在内存结构上是多个数组,Union只有一个包含了不同数据类型的数组。Union中可以有带名字的字段,字段名和字段数据类型,同样是Schema的元数据的一部分,不会体现在内存结构上。

Arrow中定义了两种类型的Union:密集型Union(类似于Struct)和稀疏型Union,分别对应用不同的场景。密集型Union中,数组的每一个值,都包含了额外的5个Byte。

密集型Union内存结构如下:

  1. 每个相对类型对应一个子数组
  2. Types 类型Buffer:类型为8-bit有符号整数的一段buffer,从0开始枚举Union中每一个类型组合,总共可以枚举127个类型,对于可能超过127个类型组合的Union,可以通过设计成Union嵌套Union来绕过127的相对类型的限制。
  3. Offset偏移Buffer:类型为32-bit有符号整数的一段buffer,表示types类型buffer中每一个类型对应的数据数组中的index。

最重要的一点,密集型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的结构几乎是一样了,除了一点,稀疏型Union没有offset偏移数组。在稀疏型Union中,数据数组的每一个子数组的长度与原始数据的长度是一样的,例如我们有一个List,list的长度=数据数组中每一个子数组的长度,在下边的示例中可以看到。

稀疏型Union可能会比密集型Union占用更多的内存空间,但是也有自己的优点:

  1. 在某些情况下,对向量运算更加友好
  2. 因为数据数组的长度与原始数据的数组长度是相等的,所以可以省略Offset 偏移量Buffer的定义

示例稀疏型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'],
]

 

你可能感兴趣的:(大数据,Apache,Arrow)