簡體   English   中英

AVX512:如何將前 8 個字節轉換為 8 個 64 位整數?

[英]AVX512: How to convert first 8 bytes into 8 64-bit integers?

我有一個__m512i inputVector,其中 64 個字節中的每一個都包含一些偏移量。 接下來,我需要將前 8 個字節偏移量添加到存儲在另一個__m512i變量( base )中的 8 個 64 位值中。 (為了處理所有 64 字節的偏移量,我將下面的代碼重復 4 次)。 然而,在我可以對 8 個打包的 64 位整數進行向量加法之前,我需要將前 8 個字節轉換為 8 個 64 位整數。 目前我的代碼使用兩個_mm512_cvtepu*_epi*()內在函數來實現這一點:

  // Convert first 16 bytes from inputVector into 16 32-bit values
  __m512i v16 = _mm512_cvtepu8_epi32(_mm512_extracti32x4_epi32(inputVector, 0));
  // Convert first 8 32-bit values into 8 64-bit values
  __m512i v8 = _mm512_cvtepu32_epi64(_mm512_extracti32x8_epi32(v16, 0));
  
  // Finally do the addition
  res = _mm512_add_epi64(base, v8);

有沒有更好的方法來實現這一目標? 我的解決方案感覺很笨拙,我對 AVX 沒有太多經驗。

_mm512_cvtepu8_epi64對低 8 個字節完全符合您的要求。 它只查看輸入__m128i的低 8 個字節; 你不需要做任何特別的事情來讓你的 8 個元素填充一個__m128i https://felixcloutier.com/x86/pmovzx

您可以只_mm512_castsi512_si128以防某些編譯器無法將extract(v,0)優化為零 asm 指令。


對於更高的塊,您可以首先從 memory 加載 8 字節塊,這樣您就可以直接提供vpmovzxbq指令,而不是加載 64 字節並且必須將高 qwords 洗牌到底部。

__m512i pmovzxbq(const char *p) {
               // yes loadl takes a __m128i* pointer but only actually loads the low 8 bytes of it.  Pretty poor API design in Intel's early SSE/SSE2 intrinsics
  __m128i bytes = _mm_loadl_epi64((const __m128i*)p);        // intrinsic for vmovq, but should optimize away into a memory source for pmovzx
  __m512i qwords = _mm512_cvtepu8_epi64(bytes);
  return qwords;
}

這與 GCC9 和 clang5.0 及更高版本( https://godbolt.org/z/vdxxGssoz )很好地編譯。 早期版本無法將負載折疊到 memory 源操作數中,執行單獨的vmovq (盡管內存源版本無論如何都不會在當前 Intel 上使用 YMM 或更廣泛的目標對負載進行微融合。)

pmovzxbq:
        vpmovzxbq       zmm0, QWORD PTR [rdi]
        ret

改組更寬向量的高部分

如果您確實想要進行更廣泛的負載,或者出於其他原因(例如某些計算的結果)在 SIMD 寄存器中包含壓縮字節,您有 2 個選項:

  • vpmovzxbq的 8 字節塊隨機播放到底部
  • 手動進行洗牌,從它們所在的位置(而不是底部)獲取字節,並將它們放在目標向量的 8 字節塊的底部,其他元素為零

第一個可以使用valignq來右移/旋轉矢量以將您想要的部分放到底部。 (像_mm512_extracti32x4_epi32這樣的即時控制洗牌只能在 16 字節的塊中工作;而像vpermq這樣的一般洗牌只允許高達 YMM 的即時控制操作數,除此之外,只有一個帶有 ZMM 控制向量的版本。)

  // normally worse than just doing 8-byte loads unless v is in a register already
  __m512i chunk3 = _mm512_alignr_epi64(v,v, 3);      // rotate right by 3 qwords
  __m512i v3unpacked = _mm512_cvtepu8_epi64(_mm512_castsi512_si128(chunk3));

(GCC11 編譯為書面;clang13 悲觀到vextracti128 / vpshufd : https://godbolt.org/z/v1hfa7Ph3

這避免了需要加載任何常量或設置任何掩碼寄存器,並且valignq在 Intel CPU 上只是具有 3c 延遲的單 uop,並且從 AVX-512F(即 Skylake-AVX512)開始支持。 https://uops.info/

但這是循環內的額外指令,就像額外的加載 uop 一樣,但更糟糕的是,它會在端口 5 上與vpmovzxbq競爭洗牌單元,除非周圍的代碼使用其他端口做大量工作,否則會損害吞吐量。

在循環內,使用 AVX-512VBMI vpermb

這需要 Ice Lake 或更高版本的 AVX512 VBMI ( https://en.wikipedia.org/wiki/AVX-512#CPUs_with_AVX-512 )。 如果可用,您可以使用帶零掩碼的vpermb將每個字節發送到需要的位置,並將所有 rest 歸零。 即從源向量的 8 字節塊中對字節進行零擴展,所有這些都具有一個字節混洗,在支持它的 CPU 上作為單個 uop 運行。

 __m512i vin;
 
 const __m512i shuf1 = _mm512_setr_epi64(8,9,10,11,12,13,14,15);  // hopefully a compiler implements this with `vpmovzxbq` from memory, but probably will spend a full 64 bytes
 const __m512i shuf2 = _mm512_add_epi64(shuf1, _mm512_set1_epi64(8));  // probably compilers will load each of these separately instead of generating on the fly...
 const __m512i shuf3 = _mm512_add_epi64(shuf2, _mm512_set1_epi64(8));
 const __mmask64 zextmask = 0x0101010101010101;    // zero except for low byte of each qword.

// all the above outside a loop, or hopefully compilers can hoist them

 __m512i v0unpacked = _mm512_cvtepu8_epi64(vin);      // special case
 __m512i v1unpacked = _mm512_maskz_permutexvar_epi8(zextmask, shuf1, vin);
 __m512i v2unpacked = _mm512_maskz_permutexvar_epi8(zextmask, shuf2, vin);
  ...

(eg https://godbolt.org/z/czdjTeK41 - GCC loads 64-byte vector constants from memory, clang's shuffle optimizer turns it into memory-source vpmovzx instructions like vpmovzxbq zmm2, qword ptr [rsi + rax + 16] !)

因此,這在一個循環中很好,它證明了所有設置常量的工作是合理的,在循環內為每個 output 向量節省 1 個負載或 shuffle uop。

否則,一次只加載 8 個字節而不是 64 個字節,就像 clang 已經做的那樣,如果你在_mm512_loadu_si512的結果上使用它。

根據您的評論,我能夠將我的代碼改進為:

  __m128i bytes16 = _mm512_castsi512_si128(inputVector);
  __m512i res = _mm512_cvtepu8_epi64(bytes16);
  res = _mm512_add_epi64(base, res);

對於高 8 字節,它似乎稍微困難一些。 我嘗試的兩種方法都使用了比低 8 字節代碼更多的指令。 就性能而言,這兩種方法在我的測試中運行得同樣快:

第一個解決方案:

  // Select bytes 8-15
  __m512i high8Bytes = _mm512_maskz_compress_epi8(0x000000000000ff00ull, inputVector);
  res = _mm512_cvtepu8_epi64(_mm512_castsi512_si128(high8Bytes));
  res = _mm512_add_epi64(base, res);

第二個解決方案(我的原始代碼):

  __m512i words32 = _mm512_cvtepu8_epi32(_mm512_castsi512_si128(inputVector));
  __m512i res = _mm512_cvtepu32_epi64(_mm512_extracti32x8_epi32(words32, 1));
  res = _mm512_add_epi64(base, res);

暫無
暫無

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

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