Apache Arrow 0.8.0 中 Java Vector API 的改進


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

這篇文章深入探討了 Java 向量實作的主要改進。自上次 Arrow 版本發布以來,我們在過去 10 週內進行了這項工作。

設計目標

  1. 提升可維護性和可擴展性
  2. 改善堆積記憶體使用量
  3. 在熱門程式碼路徑上沒有效能開銷

背景

提升可維護性和可擴展性

我們在多個地方使用模板,以便為不同的向量類別、讀取器、寫入器等進行編譯時 Java 程式碼生成。模板很有用,因為開發人員不必編寫大量重複的程式碼。

然而,我們意識到,隨著時間的推移,某些特定的 Java 模板變得極其複雜,包含龐大的 if-else 區塊、糟糕的程式碼縮排和文件。所有這些都影響了輕鬆擴展這些模板以新增功能或改進現有基礎架構的能力。

因此,我們評估了模板在編譯時程式碼生成中的使用情況,並決定在某些地方不使用複雜的模板,而是編寫少量重複但優雅、文件完善且可擴展的程式碼。

改善堆積使用量

我們在下游的 Dremio 中進行了廣泛的記憶體分析,Arrow 在 Dremio 中被大量用於對柱狀資料進行記憶體內查詢執行。總體結論是,Arrow 的 Java 向量類別具有不可忽略的堆積開銷,並且物件數量過多。在程式碼中,有些地方我們不必要地創建物件,並使用了可以用更好的替代方案替換的結構。

在熱門程式碼路徑上沒有效能開銷

Java 向量在整個物件層次結構中大量使用了委派和抽象。向量效能關鍵的 get/set 方法在執行有意義的工作之前,會在不同物件之間來回經歷一系列的函數呼叫。我們還評估了向量 API 中分支的使用情況,並透過完全避免分支來重新實作了其中一些方法。

我們從 ArrowBuf 中的 Java 記憶體程式碼的工作方式中獲得了啟發。對於所有效能關鍵的方法,ArrowBuf 都繞過了所有 Netty 物件層次結構,獲取目標虛擬位址並直接與記憶體互動。

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

在可空向量的情況下,我們會進行多次檢查以確認向量中給定位置的值是否為空值。

我們的實作方法

  • 對於純量,透過為固定寬度和可變寬度純量編寫不同的抽象基底類別,簡化了繼承樹。
  • 基底類別包含不同類型之間的所有通用功能。
  • 各個子類別為固定寬度和可變寬度純量向量實作了類型特定的 API。
  • 對於效能關鍵的方法,所有工作都在向量類別或對應的 ArrowBuf 中完成。沒有委派給任何內部物件。
  • 已移除基於 mutator 和 accessor 的向量 API 存取方式。這些物件導致了不必要的堆積開銷,並使 API 的使用變得複雜。
  • 純量向量和複雜向量都直接與管理偏移量、資料和有效性的底層緩衝區互動。早期,我們為每個向量創建不同的內部向量,並將所有功能委派給內部向量。這在記憶體管理中引入了許多錯誤、過度的堆積開銷以及由於委派鏈導致的效能損失。
  • 我們透過移除不可空向量來減少向量類別的數量。在新的實作中,Java 中的所有向量本質上都是可空的。