Java 19 中的 Vector API
一、簡介
Vector
API 是 Java 生態系統中的孵化器 API,用於在受支持的 CPU 架構上表達 Java 中的向量計算。它旨在為矢量計算提供優於等效標量替代方案的性能增益。
在 Java 19 中,作為 JEP 426 的一部分,提議對Vector
API 進行第四輪孵化。
在本教程中,我們將探討Vector
API、其相關術語以及如何利用該 API。
2. 標量、向量和並行性
在深入研究Vector
API 之前,了解 CPU 運算中標量和向量的概念非常重要。
2.1.處理單元和CPU
CPU 利用一組處理單元來執行操作。處理單元通過操作 一次只能計算一個值。該值稱為標量值,因為它就是一個值。運算可以是對單個操作數進行操作的一元運算,也可以是對兩個操作數進行操作的二元運算。將數字加 1 是一元運算的示例,而將兩個數字相加是二元運算。
處理單元需要一定的時間來執行這些操作。我們以周期來衡量時間。處理單元可能需要 0 個週期來執行一項操作,並需要許多周期來執行另一項操作,例如添加數字。
2.2.並行性
傳統的現代CPU具有多個核心,每個核心容納多個能夠執行操作的處理單元。這提供了同時並行地在這些處理單元上執行操作的能力。我們可以讓多個線程在它們的核心中運行它們的程序,我們可以並行執行操作。
當我們進行大規模計算時,例如從海量數據源中添加大量數字,我們可以將數據分割成更小的數據塊並將它們分佈在多個線程中,希望我們能夠獲得更快的處理速度。這是進行並行計算的方法之一。
2.3. SIMD處理器
我們可以使用 SIMD 處理器以不同的方式進行並行計算。 SIMD 代表單指令多數據。在這些處理器中,沒有多線程的概念。這些SIMD處理器依賴於多個處理單元,並且這些單元在單個CPU週期中(即同時)執行相同的操作。它們共享執行的程序(指令),但不共享底層數據,因此得名。它們具有相同的操作,但對不同的操作數進行操作。
與處理器從內存加載標量值的方式不同,SIMD 機器在操作之前將內存中的整數數組加載到寄存器中。 SIMD 硬件的組織方式使得值數組的加載操作能夠在單個週期內進行。 SIMD 機器允許我們並行地對數組執行計算,而無需實際依賴並發編程。
由於 SIMD 機器將內存視為數組或一系列值,因此我們將其稱為Vector,
並且 SIMD 機器執行的任何操作都成為向量操作。因此,這是一種利用 SIMD 架構原理來執行並行處理任務的非常強大且高效的方法。
3. Vector
API
現在我們知道了什麼是向量,讓我們嘗試了解 Java 提供的Vector
API 的基礎知識。 Java 中的Vector
由抽像類Vector<E>
表示。這裡, E
是以下標量原始整數類型( byte
、 short
、 int,
long)
和浮點類型( float
、 double )的裝箱類型。
3.1.形狀、物種和泳道
我們只有一個預定義的空間來存儲和使用向量,目前範圍為 64 到 512 位。想像一下,如果我們有一個Integer
Vector
,並且有 256 位來存儲它,那麼我們總共將有 8 個分量。這是因為原始 int 值的大小是 32 位。這些組件在Vector
API 的上下文中稱為通道。
向量的形狀是向量的按位大小或位數。 512 位形狀的向量將有 16 個通道,一次可以對 16 個整數進行操作,而 64 位向量只有 4 個。這裡,我們使用術語lane
來表示數據在通道中流動的相似性在 SIMD 機器內。
向量的種類是向量的形狀和數據類型的組合,例如int、float等。它由VectorSpecies<E>.
3.2.向量的車道運算
向量運算大致有兩種類型,分為車道運算和跨車道運算。
顧名思義,逐通道操作一次僅對一個或多個向量的單個通道執行標量操作。這些操作可以將一個向量的一個通道與第二個向量的一個通道組合起來,例如在加法操作期間。
另一方面,跨通道操作可以計算或修改來自向量不同通道的數據。對向量的分量進行排序是跨通道操作的一個示例。跨通道操作可以從源向量產生不同形狀的標量或向量。跨車道操作又可以分為排列操作和歸約操作。
3.3. Vector<E>
API 的層次結構
Vector<E>
類對於六種支持類型中的每一種都有六個抽象子類: ByteVector, ShortVector, IntVector, LongVector, FloatVector
和DoubleVector
。對於 SIMD 機器來說,特定的實現非常重要,這就是為什麼形狀特定的子類進一步擴展了每種類型的這些類。例如Int128Vector
、 Int512Vector,
4. 使用 Vector API 進行計算
最後讓我們看一些Vector
API 代碼。我們將在接下來的部分中討論車道方向和跨車道操作。
4.1 兩個數組相加
我們想要將兩個整數數組相加並將信息存儲在第三個數組中。傳統的標量方法是:
public int[] addTwoScalarArrays(int[] arr1, int[] arr2) {
int[] result = new int[arr1.length];
for(int i = 0; i< arr1.length; i++) {
result[i] = arr1[i] + arr2[i];
}
return result;
}
現在讓我們以向量方式編寫相同的代碼。 Vector API 包可在jdk.incubator.vector
下找到,我們需要將其導入到我們的類中。
由於我們要處理向量,因此我們需要做的第一件事就是從兩個數組創建向量。我們在此步驟中使用 Vector API 的fromArray()
方法。此方法要求我們提供要創建的向量的種類以及開始加載的數組的起始偏移量。
在我們的例子中,偏移量將為 0,因為我們希望從頭開始加載整個數組。我們可以為我們的物種使用默認的SPECIES_PREFERRED
,它使用適合其平台的最大位大小:
static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
var v1 = IntVector.fromArray(SPECIES, arr1, 0);
var v2 = IntVector.fromArray(SPECIES, arr2, 0);
一旦我們從數組中獲得了兩個向量,我們就可以通過傳遞第二個向量來對其中一個向量使用add()
方法:
var result = v1.add(v2);
最後,我們將向量結果轉換為數組並返回:
public int[] addTwoVectorArrays(int[] arr1, int[] arr2) {
var v1 = IntVector.fromArray(SPECIES, arr1, 0);
var v2 = IntVector.fromArray(SPECIES, arr2, 0);
var result = v1.add(v2);
return result.toArray();
}
考慮到上述代碼在 SIMD 機器上運行,加法操作在同一 CPU 週期中將兩個向量的所有通道相加。
4.2. VectorMasks
上面演示的代碼也有其局限性。僅當通道數量與 SIMD 機器可以處理的向量大小相匹配時,它才能正常運行並提供所宣傳的性能。這向我們介紹了使用向量掩碼的想法,由VectorMasks<E>
表示,它就像一個布爾值數組。當我們無法將整個輸入數據填充到向量中時,我們會藉助VectorMasks
。
掩碼選擇要應用操作的通道。如果通道中的相應值為 true,則應用該操作;如果為 false,則執行不同的回退操作。
這些掩碼幫助我們執行獨立於矢量形狀和大小的操作。我們可以使用預定義的length()
方法,它將在運行時返迴向量的形狀。
這是一個稍微修改過的代碼,帶有掩碼,可幫助我們以向量長度的步長迭代輸入數組,然後進行尾部清理:
public int[] addTwoVectorsWithMasks(int[] arr1, int[] arr2) {
int[] finalResult = new int[arr1.length];
int i = 0;
for (; i < SPECIES.loopBound(arr1.length); i += SPECIES.length()) {
var mask = SPECIES.indexInRange(i, arr1.length);
var v1 = IntVector.fromArray(SPECIES, arr1, i, mask);
var v2 = IntVector.fromArray(SPECIES, arr2, i, mask);
var result = v1.add(v2, mask);
result.intoArray(finalResult, i, mask);
}
// tail cleanup loop
for (; i < arr1.length; i++) {
finalResult[i] = arr1[i] + arr2[i];
}
return finalResult;
}
該代碼現在執行起來更加安全,並且獨立於向量的形狀運行。
4.3.計算向量的範數
在本節中,我們將討論另一個簡單的數學計算,即兩個值的法線。範數是我們將兩個值的平方相加,然後對總和求平方根時得到的值。
讓我們先看看標量運算是什麼樣子的:
public float[] scalarNormOfTwoArrays(float[] arr1, float[] arr2) {
float[] finalResult = new float[arr1.length];
for (int i = 0; i < arr1.length; i++) {
finalResult[i] = (arr1[i] * arr1[i] + arr2[i] * arr2[i]) * -1.0f;
}
return finalResult;
}
我們現在將嘗試編寫上述代碼的向量替代方案。
首先,我們獲得FloatVector
類型的首選物種,它在這種情況下是最佳的:
static final VectorSpecies<Float> PREFERRED_SPECIES = FloatVector.SPECIES_PREFERRED;
我們將使用掩碼的概念,正如我們在本示例的上一節中討論的那樣。我們的循環運行直到第一個數組的loopBound
值,並且以species
長度的步幅執行。在每個步驟中,我們將浮點值加載到向量中,並執行與標量版本中相同的數學運算。
最後,我們使用普通標量循環對剩餘元素執行尾部清理。最終的代碼與我們之前的示例非常相似:
public float[] vectorNormalForm(float[] arr1, float[] arr2) {
float[] finalResult = new float[arr1.length];
int i = 0;
int upperBound = SPECIES.loopBound(arr1.length);
for (; i < upperBound; i += SPECIES.length()) {
var va = FloatVector.fromArray(PREFERRED_SPECIES, arr1, i);
var vb = FloatVector.fromArray(PREFERRED_SPECIES, arr2, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(finalResult, i);
}
// tail cleanup
for (; i < arr1.length; i++) {
finalResult[i] = (arr1[i] * arr1[i] + arr2[i] * arr2[i]) * -1.0f;
}
return finalResult;
}
4.4.減少操作
Vector
API 中的歸約操作是指將向量的多個元素組合成單個結果的操作。它允許我們執行計算,例如對向量的元素求和或查找向量內的最大值、最小值和平均值。
Vector
API 提供了可以利用 SIMD 架構機器的多種歸約運算功能。一些常見的 API 包括:
-
reduceLanes()
:此方法接受數學運算,例如 ADD,並將向量的所有元素組合成單個值 -
reduceAll()
:此方法與上面的類似,不同之處在於,它需要一個可以接受兩個值並輸出單個值的二進制歸約操作 - reduceLaneWise():此方法減少特定車道中的元素並生成具有減少的車道值的向量。
我們將看到一個計算向量平均值的示例。
我們可以使用reduceLanes(ADD)
來計算所有元素的總和,然後執行標量除以數組的長度:
public double averageOfaVector(int[] arr) {
double sum = 0;
for (int i = 0; i< arr.length; i += SPECIES.length()) {
var mask = SPECIES.indexInRange(i, arr.length);
var V = IntVector.fromArray(SPECIES, arr, i, mask);
sum += V.reduceLanes(VectorOperators.ADD, mask);
}
return sum / arr.length;
}
5. 與Vector
API 相關的注意事項
雖然我們可以欣賞Vector
API 的好處,但我們應該對它持保留態度。首先,該API仍處於孵化階段。然而,有一個計劃將向量類聲明為原始類。
如上所述, Vector
API 具有硬件依賴性,因為它依賴於 SIMD 指令。許多功能可能在其他平台和架構上不可用。此外,與傳統標量操作相比,維護矢量化操作總是存在開銷。
在不了解底層架構的情況下,也很難在通用硬件上執行向量運算的基準比較。不過,JEP 對此提供了一些指導。
六,結論
儘管需要謹慎使用,但使用Vector
API 的好處是巨大的。性能提升和操作的簡化矢量化為圖形行業、大規模計算等帶來了好處。我們研究了與Vector
API 相關的重要術語。我們還深入研究了一些代碼示例。
與往常一樣,所有代碼示例都可以在 GitHub 上找到.