JDK 23 功能預覽:JEP 469 Vector API 向量計算

JEP 469 Vector API

在軟體開發周期中,效能優化一直是重要的課題。Java 作為企業級應用程式開發的主流語言之一,持續不斷地追求更好的執行效能。JDK 23 中的 JEP 469 提出的向量 API(Vector API),正是朝向這個目標邁進的重要一步。

這個新的 API 目的是為了要提供一個簡潔且高效的向量計算介面,讓開發人員能夠更好地利用現代處理器的向量運算能力,從而大幅提升程式的執行效能。由於尚在孵化階段,因此本篇僅簡介功能,待功能正式上線後再詳細介紹。

前言

在傳統的程式開發中,我們常常要處理大量的數值運算,特別是在科學計算、機器學習、圖形處理等領域。這些運算通常會透過迴圈逐一處理每個數值,但是這種方式並沒有充分利用到處理器的特性。

目前的處理器都具有向量運算的能力,也就是所謂的 SIMD(Single Instruction Multiple Data)指令集。這些指令可以在一個時鐘週期內同時處理多個數值,理論上可以大幅提升運算效能。然而,在 Java 中不太容易直接利用這項能力。

為什麼我們需要向量計算 API?

向量計算由一系列的向量操作所組成。向量通常包含一組固定的標量值(scalar value)序列,其中標量值對應於硬體定義的向量通道數(vector lane)。作用於兩個相同通道數向量的二元運算,將會對每個通道進行等效的標量運算,對應至每個向量的兩個標量值。這通常被稱為單指令多資料(即前段提到的 SIMD)。

向量操作有著一定程度的並行性,使得我們可以在一個 CPU 周期內完成更多工作以提高性能。例如,給定兩個各包含八個整數序列的向量(即八個通道),我們可以使用單一硬體指令將這兩個向量相加。也就是說,一次向量加法指令會操作 16 個整數並執行 8 次整數加法。一般情況下,對兩個整數進行操作需要執行一次整數加法。

HotSpot 已經支持自動向量化,它可以將標量操作轉換為超字操作(superword operation),並映射到向量指令。然而,並非所有的標量操作集都能夠被轉換。此外它也無法涵蓋到所有的程式碼寫法與流程,從而限制了生成程式碼的性能。

如果開發人員想要撰寫可將標量操作有效轉換成超字操作的程式碼,他們需要深入了解 HotSpot 的自動向量化演算法及其局限性,才能夠實現可靠且可持續的性能優化。並且,在某些情況下可能無法編寫可轉換的標量操作程式碼。例如,HotSpot 自動向量化不會轉換用於計算陣列雜湊碼的簡單標量操作(Arrays::hashCode 方法),也不能按字典順序比較兩個陣列的程式碼(因此有一個用於字典順序比較的內部函數 )。

為了改善上述情況,向量 API 提供了複雜向量演算法的撰寫能力,其背後使用 HotSpot 自動向量化器的功能且更有可預測性和穩健性。手動撰寫出的向量迴圈可以達成高效能演算法,例如向量化 hashCode 或專門的陣列比較,而自動向量化器可能永遠不會轉換這些演算法。許多領域都可以從這個顯式的向量 API 中受益,包括機器學習、線性代數、密碼學、金融以及 JDK 本身中的程式碼。

JEP 469 概觀

開發人員可以使用向量 API 去描述與進行複雜的向量計算。在有指令支援的 CPU 架構上,這些計算於運行時能夠可靠且有效地編譯成最佳向量指令,從而實現優於等效標量計算的性能。

向量 API 預計將一直處於孵化階段(最多只進行少量更改),直到 Project Valhalla 中的必要功能(基於值的類別和物件)成為預覽功能並正式釋出。一旦釋出後,我們將會調整向量 API 和它的實作方式,並將向量 API 從孵化階段提升到預覽階段。

  • 引入 API 來清晰簡潔地表達各種向量計算,包括在迴圈或控制流程中的向量操作序列
  • API 獨立於 CPU 架構,並能夠在支援向量指令的多個架構上可靠地將向量計算編譯為最佳向量指令
  • 提供了一組類別和方法,用於建立、操作和訪問向量
  • 支援各種向量操作,例如:加減乘除、比較、邏輯運算等

優點

  • 提供清晰且簡潔的 API 去直接表示向量運算,而無需編寫組合語言或原生函數
  • 可靠地編譯為最佳的硬體向量指令以實現比等效標量計算更好的效能
  • 具備跨平台的可攜性,支援不同的 CPU 架構並提供一致的行為
  • 提供優雅的降級機制,在不支援特定向量指令的平台上仍能正常運作

缺點

  • 目前仍處於孵化階段,API 可能在未來版本中有所變動
  • 需要 CPU 支援向量指令才能發揮最佳效能。在不支援向量指令的 CPU 上,它的性能可能不如標量計算
  • 對於不熟悉向量運算概念的開發人員來說仍然會有一定的學習曲線

向量 API 簡介

向量 API 的核心概念是向量(Vector)和向量種類(Species)。

向量代表一個固定長度的標量值序列,由抽象類別 Vector<E> 表示。類型變數 E 被實例化為向量所涵蓋的標量原始整數或浮點元素類型的包裝類型,支援的型別為 ByteShortIntegerLongFloatDouble,分別對應於標量基礎型別 byteshortintlongfloatdouble,並根據其類型變數而延伸出下列子類別:ByteVector, DoubleVector, FloatVector, IntVector, LongVector, ShortVector

種類定義了向量的形狀,意即向量的 bit 大小。當 HotSpot C2 編譯器編譯向量計算時,向量的形狀決定了 Vector<E> 的實例如何映射到硬體向量暫存器。向量的長度,即通道或元素的數量,是向量大小除以元素大小。支援的形狀集對應於 64、128、256 和 512 位元的向量大小,以及 max 位元。 512 位元的形狀可以將位元組打包成 64 個通道,或者將整數打包成 16 個通道,並且這種形狀的向量可以一次對 64 個位元組或 16 個整數進行操作。 max-bits 形狀支援當前架構的最大向量大小。這使得能夠支援 ARM SVE 平台,其中平台實作可以支援從 128 到 2048 位元的任何固定大小,增量為 128 位元。

匯入孵化器模組

當我們想要使用 jshell 測試向量 API 功能並匯入模組時,會出現如下的錯誤:

這是因為我們在啟動 jshell 時並沒有明確指定要加入該模組,因此會在 import 時出現「unnamed module does not read: jdk.incubator.vector」的錯誤訊息。我們必須要在啟動 jshell 時明確指定需要加入該模組。使用下列語法啟動 jshell:

jshell --enable-preview --add-modules jdk.incubator.vector

如同上圖所示,我們就可以開始測試向量 API 功能了。

範例程式碼

以下是一個簡單的向量運算範例:

static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_PREFERRED;

void vectorComputation(double[] a, double[] b, double[] c) {
    for (int i = 0; i < a.length; i += SPECIES.length()) {
        var va = DoubleVector.fromArray(SPECIES, a, i);
        var vb = DoubleVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                  .add(vb.mul(vb))
                  .neg();
        vc.intoArray(c, i);
    }
}

上述方法需要傳入三個等長的 double 向量陣列,它會將前兩個向量的每個元素循序進行 -(x ^ 2 + y ^ 2) 的運算後,存入第三個向量。我們可先初始化三個 double 陣列並傳入後得到運算結果:

JEP 469 test

首先,我們從 DoubleVector 中獲取一個形狀對於當前架構最佳的偏好 SPECIES,並儲存在 static final 欄位中,以便運行時編譯器將該值視為常量,從而可以更好地優化向量計算。然後,主迴圈以向量長度尋訪輸入陣列。它從陣列 ab 中的對應索引處載入給定 species 的 double 向量,流暢地執行算術運算,然後將結果儲存到陣列 c 中。

這個範例展示了如何使用向量 API 來進行向量運算。相較於傳統的標量運算,這種方式能夠更有效地利用處理器的向量運算能力,在支援的硬體上可以達到顯著的效能提升。

下列程式碼是等效的標量運算,但無法享受到處理器的優秀能力:

void scalarComputation(double[] a, double[] b, double[] c) {
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}

結語

向量 API 代表了 Java 平台在高效能運算領域的重要進展。透過提供直接且有效的向量運算介面,使得開發人員能夠更好地利用現代處理器的特性,為需要向量計算的應用程式帶來可觀的效能提升。

雖然目前仍處於孵化階段,但隨著未來的發展和完善,向量 API 很可能會成為 Java 平台中不可或缺的一部分,特別是在科學計算、機器學習等領域的應用。我們應該密切關注這個 API 的發展,並在適合的場景中開始嘗試使用它。

本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

12 + 15 =

返回頂端