Apache Arrow 0.8.0 中 Java 向量 API 的改进


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

本文深入介绍了自上次 Arrow 发布以来的 10 周内,Java 向量实现的主要改进。

设计目标

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

背景

改进可维护性和可扩展性

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

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

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

改进堆使用率

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

热点代码路径上无性能开销

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

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

在某些情况下,可以完全避免分支。

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

我们的实现方法

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