簡體   English   中英

與標量相比,為什么 java 向量 API 這么慢?

[英]Why is the java vector API so slow compared to scalar?

我最近決定嘗試使用 Java 的新孵化矢量 API,看看它的速度有多快。 我實現了兩種相當簡單的方法,一種用於解析 int,另一種用於查找字符串中字符的索引。 在這兩種情況下,與它們的標量等效方法相比,我的矢量化方法都非常慢。

這是我的代碼:

public class SIMDParse {

private static IntVector mul = IntVector.fromArray(
        IntVector.SPECIES_512,
        new int[] {0, 0, 0, 0, 0, 0, 1000000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1},
        0
);
private static byte zeroChar = (byte) '0';
private static int width = IntVector.SPECIES_512.length();
private static byte[] filler;

static {
    filler = new byte[16];
    for (int i = 0; i < 16; i++) {
        filler[i] = zeroChar;
    }
}

public static int parseInt(String str) {
    boolean negative = str.charAt(0) == '-';
    byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
    if (negative) {
        bytes[0] = zeroChar;
    }
    bytes = ensureSize(bytes, width);
    ByteVector vec = ByteVector.fromArray(ByteVector.SPECIES_128, bytes, 0);
    vec = vec.sub(zeroChar);
    IntVector ints = (IntVector) vec.castShape(IntVector.SPECIES_512, 0);
    ints = ints.mul(mul);
    return ints.reduceLanes(VectorOperators.ADD) * (negative ? -1 : 1);
}

public static byte[] ensureSize(byte[] arr, int per) {
    int mod = arr.length % per;
    if (mod == 0) {
        return arr;
    }
    int length = arr.length - (mod);
    length += per;
    byte[] newArr = new byte[length];
    System.arraycopy(arr, 0, newArr, per - mod, arr.length);
    System.arraycopy(filler, 0, newArr, 0, per - mod);
    return newArr;
}

public static byte[] ensureSize2(byte[] arr, int per) {
    int mod = arr.length % per;
    if (mod == 0) {
        return arr;
    }
    int length = arr.length - (mod);
    length += per;
    byte[] newArr = new byte[length];
    System.arraycopy(arr, 0, newArr, 0, arr.length);
    return newArr;
}

public static int indexOf(String s, char c) {
    byte[] b = s.getBytes(StandardCharsets.UTF_8);
    int width = ByteVector.SPECIES_MAX.length();
    byte bChar = (byte) c;
    b = ensureSize2(b, width);
    for (int i = 0; i < b.length; i += width) {
        ByteVector vec = ByteVector.fromArray(ByteVector.SPECIES_MAX, b, i);
        int pos = vec.compare(VectorOperators.EQ, bChar).firstTrue();
        if (pos != width) {
            return pos + i;
        }
    }
    return -1;
}

}

我完全預計我的 int 解析會更慢,因為它處理的向量大小永遠不會超過向量大小可以容納的范圍(int 長度永遠不能超過 10 位)。

根據我的基准,將123解析為 int 10k 次對於Integer.parseInt需要 3081 微秒,而對於我的實現則需要 80601 微秒。 在很長的字符串( "____".repeat(4000) + "a" + "----".repeat(193) )中搜索'a'花了 7709 微秒到String#indexOf的 7。

為什么速度如此之慢? 我認為 SIMD 的全部意義在於它比此類任務的標量等價物更快。

您選擇了 SIMD 不擅長 (string->int) 的東西,以及 JVM 非常擅長優化循環外的東西。 如果輸入不是矢量寬度的精確倍數,那么您使用一堆額外的復制工作進行了實現。


我假設您的時間是總數(每次重復 10k 次),而不是每次調用的平均值。

7 us 是不可能的快。

"____".repeat(4000)'a'之前的 16k 字節,我認為這就是您要搜索的內容。 即使是在 4GHz CPU 上以每個時鍾周期 2x 32 字節向量運行的經過良好調整/展開的memchr (又名 indexOf),10k 代表也需要 625 us。 ( 16000B / (64B/c) * 10000 reps / 4000 MHz )。 是的,我希望 JVM 要么調用本機memchr ,要么對常用的核心庫函數(如String#indexOf使用同樣有效的東西。 例如, glibc 的 avx2 memchr非常適合循環展開; 如果您使用的是 Linux,您的 JVM 可能會調用它。

內置 String indexOf也是 JIT“知道”的東西。 當它可以看到您重復使用相同的字符串作為輸入時,它顯然能夠將它從循環中提升出來。 (但是它對剩下的 7 個我們做了什么?我猜做一個不太好的memchr然后以 1/clock 做一個空的 10k 迭代循環可能需要大約 7 微秒,特別是如果你的 CPU 不是' t 快至 4GHz。)

請參閱績效評估的慣用方式? - 如果將重復計數加倍到 20k 並沒有使時間加倍,那么您的基准測試就被破壞了,並且沒有衡量您認為它的作用。

您的手動 SIMD indexOf 不太可能從循環中得到優化。 如果大小不是矢量寬度的精確倍數,它每次都會復制整個數組! (在ensureSize2 )。 正常的技術是回退到最后一個size % width元素的標量,這對於大型數組顯然要好得多。 或者更好的是,對於與以前的工作重疊不成問題的內容,執行在數組末尾結束的未對齊加載(如果總大小 >= 向量寬度)。

現代 x86 上的一個不錯的 memchr(使用像 indexOf 這樣的算法而不展開)應該每 1.5 個時鍾周期大約 1 個向量(16/32/64 字節),數據在 L1d 緩存中是熱的,沒有循環展開或任何東西。 (檢查向量比較和指針綁定作為可能的循環退出條件需要額外的 asm 指令而不是簡單的strlen ,但請參閱此答案以了解假設對齊緩沖區的簡單手寫 strlen 的一些微基准測試)。 可能你的indexOf循環瓶頸在像 Skylake 這樣的 CPU 上的前端吞吐量上,其管道寬度為 4 uops/clock。

因此,讓我們猜測您的實現每 16 字節向量需要 1.5 個周期,如果您使用的是沒有 AVX2 的 CPU? 你沒說。

16kB / 16B = 1000 個向量。 每 1.5 個時鍾 1 個向量,即 1500 個周期。 在 3GHz 機器上,1500 個周期需要 500 ns = 0.5 us 每次調用,或 5000 us 每 10k 代表。 但是,由於 16194 字節不是 16 的倍數,因此每次調用時您都會復制整個內容,因此這會花費更多時間,並且可能占總時間為 7709 us。


SIMD 有什么用

對於這樣的任務。

不,像ints.reduceLanes這樣的“水平”東西是 SIMD 通常很慢的東西。 甚至像如何使用 SIMD 實現 atoi? 使用x86 pmaddwd水平相乘和相加,仍然需要大量工作。

請注意,要使元素足夠寬以乘以位值而不會溢出,您必須解包,這需要進行一些改組。 ints.reduceLanes需要大約 log2(elements) shuffle/add 步驟,如果您從int的 512 位 AVX-512 向量開始,那么這些 shuffle 的前 2 個是車道交叉,3 個周期延遲( https:// agner.org/optimize/ )。 (或者如果你的機器甚至沒有 AVX2,那么 512 位整數向量實際上是 4x 128 位向量。你必須做單獨的工作來解包每個部分。但至少減少會很便宜,只是垂直相加,直到得到一個 128 位向量。)

唔。 我發現這篇文章是因為我在 Vector 性能方面遇到了一些奇怪的事情,因為它表面上應該是理想的 - 將兩個雙精度數組相乘。

  static private void doVector(int iteration, double[] input1, double[] input2, double[] output) {
    Instant start = Instant.now();
    for (int i = 0; i < SPECIES.loopBound(ARRAY_LENGTH); i += SPECIES.length()) {
      DoubleVector va = DoubleVector.fromArray(SPECIES, input1, i);
      DoubleVector vb = DoubleVector.fromArray(SPECIES, input2, i);
      va.mul(vb);
      System.arraycopy(va.mul(vb).toArray(), 0, output, i, SPECIES.length());
    }
    Instant finish = Instant.now();
    System.out.println("vector duration " + iteration + ": " + Duration.between(start, finish).getNano());
  }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM