Apache Arrow 0.8.0 中 Java 向量 API 的改进


发布日期 2017年12月18日
作者 Siddharth Teotia

这篇博文深入探讨了向量 Java 实现中的主要改进。自上一个 Arrow 版本发布以来的过去 10 周里,我们开展了这项工作。

设计目标

  1. 提高可维护性和可扩展性
  2. 改进堆内存使用
  3. 热代码路径上无性能开销

背景

提高可维护性和可扩展性

我们在多个地方使用模板为不同的向量类、读取器、写入器等进行编译时 Java 代码生成。模板很有用,因为开发人员不必编写大量重复代码。

然而,我们意识到,随着时间的推移,一些特定的 Java 模板变得极其复杂,包含巨大的 if-else 块、糟糕的代码缩进和文档。所有这些都影响了轻松扩展这些模板以添加新功能或改进现有基础设施的能力。

因此,我们评估了模板在编译时代码生成中的使用,并决定在某些地方不使用复杂模板,而是编写少量优雅、文档完善且可扩展的重复代码。

改进堆使用

我们在 Dremio 中进行了广泛的下游内存分析,其中 Arrow 被大量用于列式数据上的内存中查询执行。普遍的结论是 Arrow 的 Java 向量类具有不可忽略的堆开销,并且对象数量过高。代码中有些地方我们不必要地创建对象,并使用了可以用更好的替代方案替换的结构。

热代码路径上无性能开销

Java 向量在整个对象层次结构中大量使用了委托和抽象。向量的性能关键型 get/set 方法在执行有意义的工作之前,会经历一系列不同对象之间来回的函数调用链。我们还评估了向量 API 中分支的使用,并通过完全避免分支重新实现了一些方法。

我们从 ArrowBuf 中 Java 内存代码的工作方式中获得了启发。对于所有性能关键型方法,ArrowBuf 绕过了所有 netty 对象层次结构,获取目标虚拟地址并直接与内存交互。

有些情况下可以完全避免分支。

在可空向量的情况下,我们进行了多次检查以确认向量中给定位置的值是否为空。

我们的实现方法

  • 对于标量,通过为固定宽度和可变宽度标量编写不同的抽象基类来简化继承树。
  • 基类包含不同类型之间的所有共同功能。
  • 各个子类实现了固定宽度和可变宽度标量向量的类型特定 API。
  • 对于性能关键型方法,所有工作都在向量类或相应的 ArrowBuf 中完成。没有委托给任何内部对象。
  • 删除了基于修改器和访问器的向量 API 访问。这些对象导致不必要的堆开销并使 API 的使用复杂化。
  • 标量和复杂向量都直接与管理偏移量、数据和有效性的底层缓冲区交互。以前,我们为每个向量创建不同的内部向量,并将所有功能委托给内部向量。这在内存管理中引入了许多错误,由于委托链导致了过多的堆开销和性能损失。
  • 我们通过删除不可空向量来减少向量类的数量。在新实现中,Java 中的所有向量本质上都是可空的。