Apache Arrow 0.8.0 中 Java Vector API 的改进
已发布 2017年12月18日
作者 Siddharth Teotia
这篇文章深入介绍了 Java 向量实现的重大改进。自从上次 Arrow 发布以来,我们在过去的 10 周内完成了这项工作。
设计目标
- 提高可维护性和可扩展性
- 改进堆内存使用
- 在热代码路径上没有性能开销
背景
提高可维护性和可扩展性
我们在几个地方使用模板来进行编译时 Java 代码生成,用于不同的向量类、读取器、写入器等。模板很有用,因为开发人员不必编写大量重复代码。
但是,我们意识到随着时间的推移,一些特定的 Java 模板变得非常复杂,包含巨大的 if-else 代码块、糟糕的代码缩进和文档。所有这些都影响了轻松扩展这些模板以添加新功能或改进现有基础设施的能力。
因此,我们评估了模板在编译时代码生成中的使用情况,并决定在某些地方不使用复杂的模板,而是编写少量优雅、文档齐全且可扩展的重复代码。
改进堆使用
我们在 Dremio 中进行了广泛的内存分析,Arrow 在那里大量用于列式数据的内存查询执行。总的结论是,Arrow 的 Java 向量类具有不可忽略的堆开销,并且对象数量过多。在代码中,有些地方我们不必要地创建对象,并使用可以用更好的替代方案代替的结构。
在热代码路径上没有性能开销
Java 向量在整个对象层次结构中大量使用委托和抽象。向量的性能关键的 get/set 方法在执行有意义的工作之前,会在不同对象之间来回进行一系列函数调用。我们还评估了向量 API 中分支的使用,并通过完全避免分支来重新实现其中的一些。
我们从 ArrowBuf
中的 Java 内存代码的工作方式中获得了灵感。对于所有性能关键方法,ArrowBuf
绕过所有 Netty 对象层次结构,获取目标虚拟地址并直接与内存交互。
在某些情况下,可以完全避免分支。
对于可为空的向量,我们进行了多次检查以确认向量中给定位置的值是否为空。
我们的实施方法
- 对于标量,通过为固定宽度和可变宽度标量编写不同的抽象基类来简化继承树。
- 基类包含不同类型的所有通用功能。
- 各个子类实现了固定宽度和可变宽度标量向量的类型特定 API。
- 对于性能关键方法,所有工作都在向量类或相应的 ArrowBuf 中完成。没有委托给任何内部对象。
- 删除了基于 mutator 和 accessor 的对向量 API 的访问。这些对象导致不必要的堆开销并使 API 的使用复杂化。
- 标量和复杂向量都直接与管理偏移量、数据和有效性的底层缓冲区交互。之前我们为每个向量创建不同的内部向量,并将所有功能委托给内部向量。这在内存管理中引入了许多错误,过多的堆开销以及由于委托链导致的性能损失。
- 我们通过删除不可为空的向量来减少向量类的数量。在新实现中,Java 中的所有向量本质上都是可为空的。