Apache Arrow官方文档-内存结构

定义/术语

  由于不同的项目使用不同的词语描述各种概念,所以这里有一个小小的术语表来帮助消除歧义。

  • 数组:已知长度具有相同类型的值序列。
  • 槽或数组槽:一些特定数据类型的数组中的单个逻辑值
  • 连续的内存区域:给定长度的顺序虚拟地址空间。任何字节都可以通过小于区域长度的单个指针偏移量来取到。
  • 连续的内存缓冲区:存储Array的多值组件的连续内存区域。有时称为“缓冲区”。
  • 基本类型:占用固定大小的内存槽的数据类型,以位宽或字节宽度指定占用内存大小。
  • 嵌套或参数类型:完整结构依赖于一个或多个其他子对象类型的数据类型。当且仅当子类型相等时,两个完全指定的嵌套类型相等。例如,如果U和V是不同的相对(简单)类型,List<U>与List<V> 也不同。
  • 相对类型或简单类型(不合格):特定的基本类型或完全指定的嵌套类型。当我们说槽时,我们是指相对类型值,不一定是任意物理存储区域。
  • 逻辑类型:使用某些相对(物理)类型实现的数据类型。例如,存储在16个字节中的十进制值可以存储在一个大小为16槽的字节数组中 。类似地,字符串可以存储为 List<1-byte>。
  • 父和子数组:表示嵌套类型结构中物理值数组之间关系的名称。例如,List<T>类型:父类型的数组有一个T型数组作为它的子元素(参见下面的列表)。
  • 叶子节点或叶子:一个原始值数组,可能是也可能不是具有嵌套类型的某些数组的子数组。

    要求,目标和非目标

    基本要求

  • 一种物理内存布局,可在处理平面和嵌套列式数据的各种系统之间进行零反序列化的数据交换,包括Spark,Drill,Impala,Kudu,Ibis,Spark,ODBC协议和利用开源组件的专有系统。
  • 所有数组槽都可以在不间断的时间内访问,复杂性在嵌套级别上呈线性增长
  • 能够表示完全物化和解码/解压缩的Parquet 数据
  • 所有连续的内存缓冲区以64字节边界对齐,并填充到64字节的倍数。
  • 任何相对类型都可以有空槽
  • 数组一旦创建就不可变。实现可以提供API来突变数组,但应用突变将需要构建新的数组数据结构。
  • 数组可重定位(例如,用于RPC /瞬态存储),无需调整指针。另一种方法是连续的内存区域可以迁移到不同的地址空间(例如通过memcpy类型的操作),而不改变它们的内容。

    目标(对于本文档)

  • 描述相对类型,足够的明确的描述实现(物理值类型和一组初始嵌套类型)
  • 每个相对类型的内存布局和随机访问模式
  • 空值表示

    非目标(对于本文档)

  • 枚举或指定可以实现为基本(固定宽度)值类型的逻辑类型。例如:有符号和无符号整数,浮点数,布尔值,精确小数,日期和时间类型,CHAR(K),VARCHAR(K)等。
  • 指定标准化元数据或RPC或临时文件存储的数据布局。
  • 定义选择或屏蔽向量(vector)构造
  • 实现具体细节
  • 用户或开发人员C/C++/Java API的详细信息。
  • 任何由表命名的数组组成的“表”结构,每一个都有自己的类型,或任何构成数组的其他结构。
  • 任何内存管理或引用计数子系统
  • 枚举或指定编码或压缩支持的类型

    字节顺序(Endianness)

      默认情况下,Arrow格式是低位编址的(将低序字节存储在起始地址)。模式元数据有一个指明RecordBatches的字节顺序的字段。通常这是生成RecordBatch的系统的字节顺序。主要用例是在具有相同字节码的系统之间交换RecordBatches。首先,当尝试读取与底层系统不匹配的字节顺序的模式时,将会返回错误。参考实现集中在地位编址,并为此提供测试。最终我们可以通过字节交换来提供自动转换。

    对齐和填充

      如上所述,所有缓冲区都旨在以64字节边界为准对齐内存,并且填充到64字节倍数的长度。对齐要求遵循优化内存访问的最佳做法:

  • 数值数组中的元素将保证通过对齐的访问来读取。
  • 在一些架构上,对齐可以帮助限制部分使用的缓存行。
  • 64字节对齐由英特尔性能向导为超过64个字节的数据结构所推荐的(这将是Arrow格式数组的共同情况)。
      要求填充64个字节的倍数允许在循环中一致地使用SIMD指令,无需额外的条件检查。这样就允许更简单和更有效的代码。
      选择特定的填充长度是因为它与2016年4月可用的最大的已知SIMD指令寄存器匹配(Intel AVX-512)。保证填充也可以允许某些编译器直接生成更优化的代码(例如可以安全地使用英特尔 -qopt-assume-safe-padding)。
      除非另有说明,填充字节不需要具有特定值。

    数组长度

      任何数组具有已知且固定长度,存储为32位有符号整数,因此最多可以存储(2^31 - 1)个元素。我们选择一个有符号的int32有一下2个原因:

  • 增强与Java和客户端语言的兼容性,可能对无符号整数具有不同的支持质量。
  • 为了鼓励开发人员组成较小的数组(每个数组在其叶节点中都包含连续的内存),以创建可能超过(2^31- 1)个元素的更大数组结构,而不是分配非常大的连续内存块。

    空值计数

      空值槽的数量是物理数组的属性,并被认为是数据结构的一部分。空值计数存储为32位有符号整数,因为它可能与数组长度一样大。

    空值位图

      任何相对类型都可以有空值槽,不管是原始类型还是嵌套类型。
      具有空值的数组必须具有连续的内存缓冲区,称为空(或有效)位图,其长度为64字节的倍数(如上所述),并且足够大,以至于每个数组槽至少有1位。
      任何数组槽是否有效(非空)是在该位图的各个位中编码的。索引(设置位)j值为1表示该值不为空,而0(位未设置)表示该值为空。位图被初始化为在分配时间全部未设置(这包括填充)。
    is_valid[j] -> bitmap[j / 8] & (1 << (j % 8))
      我们使用最低有效位(LSB)编号(也称为位编址bit-endianness)。这意味着在一组8个位中,我们从右到左读:

    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

      具有0空值计数的数组可以选择不分配空值位图。实现为了方便可能会选择始终分配一个空值位图,但是在内存被共享时应该注意。
      嵌套类型数组具有自己的空值位图和空值计数,而不管其子数组的空值和空位。

    原始(基本)类型值数组

      基本类型值数组表示固定长度的数组,每个值都具有通常用字节测量的相同的物理槽宽度,尽管规范还提供了位打包类型(例如以位编码的布尔值)。
      在内部,数组包含一个连续的内存缓冲区,其总大小等于槽宽乘以数组长度。对于打包类型,大小将舍入到最接近的字节。
      关联的空值位图被连续分配(如上所述),但不需要在内存中与值缓冲器相邻。

    示例布局:Int32数组

    例如int32的原始数组:

    [1,2,null,4,8]

会像:

* Length: 5, Null count: 1
* Null bitmap buffer:

  |Byte 0 (validity bitmap) \| Bytes 1-63           |
  |-------------------------|-----------------------|
  |00011011                 | 0 (padding)          |

* Value Buffer:

  |Bytes 0-3 | Bytes 4-7 | Bytes 8-11| Bytes 12-15| Bytes 16-19 | Bytes 20-63 |
  |----------|-----------|-----------|-----------|-------------|-------------|
  | 1        | 2         | unspecified| 4        | 8           | unspecified |

示例布局:非空int32数组

[1,2,3,4,8]有两种可能的布局:

* Length: 5, Null count: 0
* Null bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00011111                 | 0 (padding)           |

* Value 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 |
或者位图消失:
* Length 5, Null count: 0
* Null bitmap buffer: Not required
* Value 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<T>,这里的T是任何相对类型(原始或嵌套)。
  列表数组由以下组合表示:

  • 值数组,T类型的子数组,T也可以是嵌套类型。
  • 一个包含长度等于顶级数组长度加1的32位有符号整数的偏移缓冲区。请注意,这将数组的大小限制为(2^31 -1)。
      偏移数组对值数组中的起始位置进行编码,并且使用与偏移数组中的下一个元素的第一个差异来计算每个槽中的值的长度。例如。槽j的位置和长度计算为:
    slot_position = offsets[j]
    slot_length = offsets[j + 1] - offsets[j]  // (for 0 <= j < length)

    偏移数组中的第一个值为0,最后一个元素是值数组的长度。

猜你喜欢

转载自blog.51cto.com/1196740/2160722