<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      zyl910

      優化技巧、硬件體系、圖像處理、圖形學、游戲編程、國際化與文本信息處理。

        博客園 :: 首頁 :: 博問 :: 閃存 :: 新隨筆 :: 聯系 :: 訂閱 訂閱 :: 管理 ::

      上一篇文章里,給大家講解了24位圖像水平翻轉(FlipX)算法,其中用到了一個關鍵方法——YShuffleX3Kernel。一些讀者對它背后的原理感興趣——為什么它在跨平臺時運行也能獲得SIMD硬件加速, 各種向量指令集的情況下具體怎樣實現的?于是本文便詳細解答一下。

      一、為什么它在跨平臺時運行也能獲得SIMD硬件加速

      1.1 歷史

      最初,只有匯編語言能使用SIMD(Single Instruction Multiple Data,單指令多數據流)指令,故只能用它來編寫向量化算法。匯編語言的編程難度是很大的,且不同的CPU架構得專門去編碼,可移植性為零。

      后來,C/C++ 等編程語言增加了內在函數(Intrinsics Functions)機制,能通過內在函數調用SIMD硬件加速指令,使編程門檻大為降低。此時遇到了可移植性的瓶頸——雖然用 C/C++ 能開發跨平臺程序,但一旦使用了SIMD指令,因它與本機指令集密切相關,就難以跨平臺了。

      像 simde、vectorclass、xsimd 等向量算法庫,為 C/C++提高了向量算法的可移植性。使用它們,可實現源代碼級別的可移植性——同一份源代碼,拿到不同的平臺上時,只要調整好編譯參數,便能編譯出該平臺的能執行的向量算法。

      但上述辦法,還是需要去每個平臺編譯一遍,使用繁瑣。

      最理想的情景是——程序只需編譯一遍,隨后在各個平臺上不僅能正常運行,且能夠自動使用最佳的SIMD指令。

      C/C++里有一個辦法來盡可能逼近這個理想——在程序運行時檢測本機支持哪些指令集,然后切換為對應指令的算法。但該方法工作量大、編碼難度大,且分支過多也會影響程序性能。而且該辦法有一個致命弱點,它頂多只能實現同一種CPU架構時的自動使用最佳SIMD指令,CPU架構不同時還是得重新編譯。

      .NET Core 3.0 增加了對內在函數的支持,給這個理想帶來了一線曙光。因為 .NET 程序不是一次性編譯的,而是先編譯為IL(中間語言)代碼,隨后程序在目標平臺上運行時,才會被JIT(即時編譯器)編譯為本地代碼并執行。關鍵點在于,是JIT支持內在函數,于是在不同平臺運行時,JIT可使用該平臺的SIMD指令集。

      只是 .NET 更關注底層能力,有自己的發展目標,對于向量算法的關注還不夠多。目前Vector等向量類型所提供的方法,方法數量還很少,缺少很多向量算法所需的方法。且部分方法長期使用標量回退算法,導致它們沒有SIMD硬件加速(如 Vector128.Shuffle)。

      為了解決這些缺點,我開發了VectorTraits庫,為向量類型補充了不少方法,且這些方法在多個平臺都是有SIMD硬件加速的。從而實現了這個理想——程序只需編譯一遍,隨后在各個平臺上不僅能正常運行,且能夠自動使用最佳的SIMD指令。

      1.2 .NET 中如何使用各種指令集的內在函數

      對于固定大小的向量,它位于這個命名空間。

      • System.Runtime.Intrinsics:用于提供各種位寬的向量類型,如 只讀結構體 Vector64<T>Vector128<T>Vector256<T>Vector512<T>,及輔助的靜態類 Vector64、Vector128、Vector256、Vector512。官方文檔說明:包含用于創建和傳遞各種大小和格式的寄存器狀態的類型,用于指令集擴展。有關操作這些寄存器的說明,請參閱 System.Runtime.Intrinsics.X86 和 System.Runtime.Intrinsics.Arm。

      對于各種架構的指令集,位于下面這些命名空間。

      • System.Runtime.Intrinsics.X86:用于提供x86架構各種指令集的類,如Avx等。官方文檔說明:公開 x86 和 x64 系統的 select 指令集擴展。 對于每個擴展,這些指令集表示為單獨的類。 可以通過查詢相應類型上的 IsSupported 屬性來確定是否支持當前環境中的任何擴展。
      • System.Runtime.Intrinsics.Arm:用于提供Arm架構各種指令集的類,如AdvSimd 等。官方文檔說明:公開 ARM 系統的 select 指令集擴展。 對于每個擴展,這些指令集表示為單獨的類。 可以通過查詢相應類型上的 IsSupported 屬性來確定是否支持當前環境中的任何擴展。
      • System.Runtime.Intrinsics.Wasm:用于提供Wasm架構各種指令集的類,如PackedSimd 等。官方文檔說明:公開 Wasm 系統的 select 指令集擴展。 對于每個擴展,這些指令集表示為單獨的類。 可以通過查詢相應類型上的 IsSupported 屬性來確定是否支持當前環境中的任何擴展。

      簡單來說,“System.Runtime.Intrinsics”用于定義通用的向量類型,隨后它的各種子命名空間,以CPU架構來命名。子命名空間里,包含各個內在函數類,每個類對應一套指令集。類中的各個靜態方法就是內在函數,對應指令集內的各條指令。

      對于每一個內在函數類,都提供靜態屬性 IsSupported,用于檢查當前運行環境是否支持該指令集。例如“Avx.IsSupported”,是用于檢測是否支持Avx指令集。

      觀察子命名空間里的內在函數類,發現有些類的后綴是“64”(如Avx.X64,及Arm里的AdvSimd.Arm64),這些是64位模式下特有的指令集,它們的指令一般比較少。平時應盡量使用后綴不是“64”的類,因為這些它們是 32位或64位 環境都能工作的類。

      1.3 判斷指令集

      上面提到了每一個內在函數類,都提供靜態屬性 IsSupported,用于檢查當前運行環境是否支持該指令集。

      于是可以用if語句寫分支代碼,先檢測該指令集的 IsSupported 屬性是否為 true,隨后在分支內使用該指令集。例如。

      if (Avx.IsSupported) {
        c = Avx.Add(a, b);
        ...
      }
      

      等一等,以前很多資料上說了分支語句會影響性能嗎?它造成CPU流水線失效啊。

      其實呢,雖然表面上這里仍是用 if關鍵字,但它與常規的分支語句不同。由于是JIT負責將程序編譯為目標平臺的本地代碼并執行,此時本機支持的指令集是已經確定的,于是對應類的IsSupported屬性其實是運行時的常量。于是JIT在編譯這個if語句時,僅會對有效的分支進行編譯,而其他分支會被忽略。也就說,在JIT生成的本地代碼里,并沒有“分支跳轉指令”,只存在有效分支內的代碼。

      這種工作機制,類似 C++ 2017 標準里增加的 “constexpr if”機制。用于在編譯時根據常量表達式的值,選擇執行對應的代碼分支。它允許在編譯時進行條件編譯,從而提高代碼的靈活性和性能。

      1.4 使用內聯來避免函數調用開銷

      若方法內的代碼比較短小時,此時函數調用開銷會非常突出。函數調用在執行時,首先要在棧中為形參和局部變量分配存儲空間,然后還要將實參的值復制給形參,接下來還要將函數的返回地址(該地址指明了函數執行結束后,程序應該回到哪里繼續執行)放入棧中,最后才跳轉到函數內部執行。這個過程是要耗費時間的。另外,函數執行 return 語句返回時,需要從棧中回收形參和局部變量占用的存儲空間,然后從棧中取出返回地址,再跳轉到該地址繼續執行,這個過程也要耗費時間。

      對于向量類型,函數調用開銷會更加嚴重。不僅是因為向量類型的字節數比較多,而且還要做清空向量寄存器等操作。

      于是對于短小的方法,應標記為“內聯”的。這樣JIT會將該方法內的代碼,盡量與調用者的代碼內聯在一起進行編譯。從而避免了函數調用開銷。

      具體辦法是給方法增加MethodImpl特性,標記 AggressiveInlining。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      

      1.6 使用自動大小向量Vector

      在 X86架構上,通過Sse系列指令集可以使用128位向量,通過Avx系列指令集可以使用256位向量等。這些向量長度,對應了 .NET 中的 Vector128、 Vector256類型。

      在 Arm架構上,通過AdvSimd(NEON)系列指令集可以使用128位向量。

      為了能夠使向量算法的代碼能夠跨平臺,一種辦法是使用 各架構的最小集,即Vector128。但這個辦法存在以下缺點:

      1. 沒能發揮CPU的全部潛力。X86處理器如今已經普及Avx2指令了,能夠完善的處理256位向量。若使用Vector128,就強制降級了。
      2. .NET 版本要求高。.NET Core 3.0才提供 Vector128類型。而很多應用程序還需使用 .NET Framework
      3. .NET 7.0之前的固定大小向量(Vector128等)還不完善。例如自動大小向量(Vector)早就支持的函數,固定大小向量(Vector128等)很晚才支持 。

      于是,更好的辦法是使用 自動大小向量 Vector。

      1. Vector 類型的大小不是固定的。一般來說,它是本機CPU的最大向量大小。例如是X86架構且具有Avx2指令集時,它是256位;否則它為128位。鑒于Avx512尚未普及,這個位寬是合適的。
      2. .NET 版本需求低。自 .NET Core 1.0 起,便原生支持該類型。使用 nuget 安裝了 System.Numerics.Vectors 包后,從 .NET Framework 4.5開始便能使用自動大小向量 Vector。

      固定大小向量(Vector128等)都提供了一個擴展方法 AsVector,可以將它們重新解釋為 自動大小向量 (Vector)。同樣的,自動大小向量具有AsVector128、AsVector256 擴展方法 ,可以重新解釋為固定大小向量。

      例如VectorTraits的源代碼中,YShuffleX3Kernel是這樣將Vector128重新解釋為Vector的。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector<byte> YShuffleX3Kernel(Vector<byte> vector0, Vector<byte> vector1, Vector<byte> vector2, Vector<byte> indices) {
          return WStatics.YShuffleX3Kernel(vector0.AsVector128(), vector1.AsVector128(), vector2.AsVector128(), indices.AsVector128()).AsVector();
      }
      

      注意,它是重新解釋,而不是類型轉換,所以沒有類型轉換的開銷。但是需要注意,只有向量位長相同時,才能安全的進行轉換。具體來說,通過觀察程序運行時匯編代碼,會發現無論是原來的Vector128,還是重新解釋后的Vector,仍是使用同一個向量寄存器,沒有任何其他操作。AsVector等擴展方法,只是為了能通過 C# 的語法檢查。

      于是我們一般是這樣使用的:

      1. 首先使用固定大小向量(Vector128等),通過 Sse、Avx、AdvSimd等指令集,編寫好算法實現。
      2. 隨后為了方便外層代碼的調用,將固定大小向量重新解釋為自動大小向量 (Vector)。
      3. 最后對于算法的公共方法,會檢測向量位長、指令集支持性等信息,選擇最適合的“算法實現”進行調用。

      1.5 小結

      VectorTraits 就是使用了上述辦法,從而實現了跨平臺時運行也能獲得SIMD硬件加速。

      Vectors等類所提供的方法,就是算法公共方法。這些公共方法,分別有著Sse、Avx、AdvSimd等指令集編寫的算法實現。且 Vector128s、Vector256s也提供了同樣的向量方法,用于需使用固定大小向量的場合。

      跨平臺庫的使用起來很簡單方便,可要將它開發出來,就沒那么輕松了。需要為不同處理器架構的各種指令集,分別編寫算法實現。工作繁重,是一個體力活。

      接下來的章節,會詳細講解Byte類型的YShuffleX3Kernel方法,在各個平臺的各種指令集上是如何實現的。

      其實 YShuffleX3Kernel 方法不僅支持 Byte 類型,它的重載方法還支持 Int16、Int32、Int64 等類型。這就使不同位寬的數據,也能按照同樣的辦法去處理。為了避免文章篇幅過長,于是文本僅講解了 Byte 類型。有興趣的讀者可以參考本文,查看源代碼里的其他數據類型是怎么處理。

      二、X86架構

      X86架構提供了 shuffle(換位) 指令,可以用它來實現向量內的換位。

      接下來先介紹用Sse等指令集操作128位向量,隨后介紹用Avx2指令集操作256位向量。最后介紹如何使用Avx512系列指令集做優化。

      2.1 用Sse等指令集操作128位向量

      2.1.1 實現單向量換位(YShuffleKernel)

      Ssse3指令集,提供了Byte的shuffle指令。它對應了 Ssse3.Shuffle 方法。該方法的定義如下。

      // https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.intrinsics.x86.ssse3.shuffle?view=net-8.0
      // __m128i _mm_shuffle_epi8 (__m128i a, __m128i b)
      // PSHUFB xmm, xmm/m128
      public static Vector128<byte> Shuffle (Vector128<byte> value, Vector128<byte> mask);
      

      使用該方法,便能實現單向量換位的方法 YShuffleKernel。源碼在 WVectorTraits128Sse.YS.cs

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleKernel(Vector128<byte> vector, Vector128<byte> indices) {
          if (Ssse3.IsSupported) {
              return Ssse3.Shuffle(vector, indices);
          } else {
              return SuperStatics.YShuffleKernel(vector, indices);
          }
      }
      

      Ssse3.IsSupported 為true時,使用 Ssse3.Shuffle;當不支持該指令集時,便回退為SuperStatics的標量算法。

      2.1.2 實現2向量換位(YShuffleX2Kernel)

      組合使用2個單向量的Shuffle方法,就能實現2向量換位的方法 YShuffleX2Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX2Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> indices) {
          if (!Ssse3.IsSupported) VectorMessageFormats.ThrowNewUnsupported("Ssse3");
          Vector128<byte> vCount = Vector128.Create((byte)Vector128<byte>.Count);
          Vector128<byte> indices1 = Sse2.Subtract(indices, vCount);
          Vector128<byte> rt0 = Ssse3.Shuffle(vector0, indices);
          Vector128<byte> mask = Sse2.CompareGreaterThan(vCount.AsSByte(), indices.AsSByte()).AsByte(); // vCount[i]>indices[i] ==> indices[i]<vCount[i].
          Vector128<byte> rt1 = Ssse3.Shuffle(vector1, indices1);
          Vector128<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1);
          return rt;
      }
      

      方法內的第1行,是判斷指令集指令集是否支持,若不支持便會調用ThrowNewUnsupported拋出異常。

      隨后2次調用 Shuffle方法,傳遞了不同的索引。

      1. 第1次的索引為 indices 的原始值,使 i <Count 的元素做好換位。
      2. 第2次的索引(indices1)為 (indices-Count) 的值,使 i >= Count 的元素做好換位。

      然后計算好掩碼 mask,便能使用條件選擇(ConditionalSelect_Relaxed),將這2次Shuffle的結果進行合并。ConditionalSelect_Relaxed的用途,與 Vector128.ConditionalSelect 是相同的,它還會利用 Sse41指令集進行優化(Sse41提供了條件選擇的單條指令 Sse41.BlendVariable)。

      由于 YShuffleX2Kernel 這樣帶Kernel后綴的方法要求索引必須在范圍內,故在滿足這個前提的情況下,上面的代碼是正常工作的。若索引超過范圍,上面的代碼的結果會不正確。此時應該改為使用 YShuffleX2或YShuffleX2Insert 方法,它們會判斷索引范圍而進行相應的清零或插入的處理,故運算量會多一些。

      2.1.3 實現3向量換位(YShuffleX3Kernel)

      組合使用3個單向量的Shuffle方法,就能實現3向量換位的方法 YShuffleX2Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX3Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> vector2, Vector128<byte> indices) {
          if (!Ssse3.IsSupported) VectorMessageFormats.ThrowNewUnsupported("Ssse3");
          Vector128<byte> vCount2 = Vector128.Create((byte)(Vector128<byte>.Count * 2));
          Vector128<byte> indices1 = Sse2.Subtract(indices, vCount2);
          Vector128<byte> rt0 = YShuffleX2Kernel_Combine(vector0, vector1, indices);
          Vector128<byte> mask = Sse2.CompareGreaterThan(vCount2.AsSByte(), indices.AsSByte()).AsByte(); // vCount2[i]>indices[i] ==> indices[i]<vCount2[i].
          Vector128<byte> rt1 = Ssse3.Shuffle(vector2, indices1);
          Vector128<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1);
          return rt;
      }
      

      它調用了上面的 YShuffleX2Kernel_Combine來簡化代碼,用 YShuffleX2Kernel_Combine 來處理 i < Count*2 范圍內的索引。

      隨后該方法自己用Shuffle指令來處理 i >= Count*2 范圍內的索引,最后將結果合并。

      2.2 用Avx2指令集操作256位向量

      2.2.1 實現單向量換位(YShuffleKernel)

      2.2.1.1 指令介紹

      Avx2指令集,提供了Byte的shuffle指令。它對應了 Avx2.Shuffle 方法。該方法的定義如下。

      // https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.intrinsics.x86.avx2?view=net-8.0
      // __m256i _mm256_shuffle_epi8 (__m256i a, __m256i b)
      // VPSHUFB ymm, ymm, ymm/m256
      public static Vector256<byte> Shuffle (Vector256<byte> value, Vector256<byte> mask);
      

      它看上去與 Ssse3.Shuffle差不多,僅是向量大小變為了256位。但該指令的索引(第2個參數mask)的定義域并沒有擴展到256位,而仍然是128位。它可以看作是2個128位的Shuffle指令組合而成。下面摘錄了《Intel? Intrinsics Guide》手冊里該指令的介紹。

      __m256i _mm256_shuffle_epi8 (__m256i a, __m256i b)
      #include <immintrin.h>
      Instruction: vpshufb ymm, ymm, ymm
      CPUID Flags: AVX2
      Description
      Shuffle 8-bit integers in a within 128-bit lanes according to shuffle control mask in the corresponding 8-bit element of b, and store the results in dst.
      Operation
      FOR j := 0 to 15
      	i := j*8
      	IF b[i+7] == 1
      		dst[i+7:i] := 0
      	ELSE
      		index[3:0] := b[i+3:i]
      		dst[i+7:i] := a[index*8+7:index*8]
      	FI
      	IF b[128+i+7] == 1
      		dst[128+i+7:128+i] := 0
      	ELSE
      		index[3:0] := b[128+i+3:128+i]
      		dst[128+i+7:128+i] := a[128+index*8+7:128+index*8]
      	FI
      ENDFOR
      dst[MAX:256] := 0
      

      注意描述(Description)中提到的“128-bit lanes”,表示它是按每個128位小道,分別處理的。在Avx、Avx2 等256位指令集中,有不少指令是這樣從128位擴展到256的,導致用起來比較困難,這一類困難一般被簡稱為“跨lane難題”(跨128位小道的難題)。

      另外還可以發現,若索引向量里元素的最高位為1,則結果向量里對應元素的值會被清零。這個特性很有用,下面的章節將會用到。

      2.2.1.2 解決跨lane難題

      借助permute(重排)和blend(條件選擇)指令,可以解決跨lane難題。

      這里介紹一下,X86向量指令的名稱一般是這樣約定的——shuffle(換位)是128位lane內的操作,而permute(重排)是跨lane的操作。由于lane內操作可以按每128位并行處理,指令性能是非常高的,推薦使用。而permute等跨lane的操作會相對慢一些,應僅在必須使用時才用。

      借助permute和blend指令的幫忙,全256位索引的換位有了思路——因AVX2的shuffle指令的索引是128位的,要想實現全256位索引的換位,需要調用2次shuffle指令。第一次對低128位索引進行換位;另一次先使用permute指令將高、低128位進行交換,再執行shuffle指令對另外128位索引進行換位。最后計算好掩碼,使用blend指令對2次shuffle指令的結果進行合并。

      上述思路確實能夠工作,只是步驟比較多,拖累了性能。有沒有更好的辦法呢?

      在stackoverflow網站上,ErmIg給出了一種辦法,效率更高。這段代碼是C語言的。

      // https://stackoverflow.com/questions/30669556/shuffle-elements-of-m256i-vector
      const __m256i K0 = _mm256_setr_epi8(
          0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70,
          0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0);
      
      const __m256i K1 = _mm256_setr_epi8(
          0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
          0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70);
      
      inline const __m256i Shuffle(const __m256i & value, const __m256i & shuffle)
      {
          return _mm256_or_si256(_mm256_shuffle_epi8(value, _mm256_add_epi8(shuffle, K0)), 
              _mm256_shuffle_epi8(_mm256_permute4x64_epi64(value, 0x4E), _mm256_add_epi8(shuffle, K1)));
      }
      

      該辦法很巧妙,利用了加法對索引里的值進行轉換,使其能很好利用shuffle指令的“最高位為1時清零”特性。

      • 加上K0,能使低128位數據里 低128范圍的索引有效(最高位不為0),而高128位范圍的索引因最高位為1,會被清零。且高128位數據的效果反之。
      • 加上K1,能使低128位數據里 高128范圍的索引有效(最高位不為0),而低128位范圍的索引因最高位為1,會被清零。且高128位數據的效果反之。

      再利用permute指令對源值進行重排,以及使用 or 指令將2個shuffle的結果進行合并,于是完成了全256位索引的換位。它不需要blend指令,并節省了掩碼計算的開銷,效率非常高。

      將上述辦法翻譯為 C# 語言的代碼,便能實現單向量換位的方法 YShuffleKernel。源碼在 WVectorTraits256Avx2.YS.cs

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleKernel_ByteAdd(Vector256<byte> vector, Vector256<byte> indices) {
          return Avx2.Or(
              Avx2.Shuffle(Avx2.Permute4x64(vector.AsInt64(), (byte)ShuffleControlG4.ZWXY).AsByte(), Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K1))
              , Avx2.Shuffle(vector, Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K0))
          );
      }
      

      先前的C語言代碼在使用permute指令進行重排時,使用了一個魔法數字 0x4E。魔法數字會造成理解困難,于是我改為使用枚舉類型ShuffleControlG4來描述。它的成員的命名規則,參考了 HLSL(High-level shader language。DirectX里的著色語言)/GLSL(OpenGL Shading Language。OpenGL里的著色語言)里swizzle語句的寫法,用 X/Y/Z/W 這4字母來代表偏移量0~3,隨后“4個字母的組合”表達了“4個元素的偏移量”。

      ShuffleControlG4.ZWXY 相當于HLSL(或GLSL)里的 result = source.zwxy。即permute指令使用該常數時,會將 “[X, Y, Z, W]”給重排為“[Z, W, X, Y]”,于是實現了 高128位與低128位的互換。

      K0、K1這樣的常數由于經常使用,于是將它們放進了 Vector256Constants 這個靜態類中。

      2.2.1.3 進一步優化

      上面的代碼雖然有效,但是早期的 .NET JIT 在編譯本機代碼時,不會做指令排序等優化,導致性能沒達到預期。于是可以手動對各個向量指令調整順序,盡可能提高這段代碼的運行時效率。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleKernel_ByteAdd2(Vector256<byte> vector, Vector256<byte> indices) {
          // Format: Code; //Latency, Throughput(references IceLake)
          Vector256<byte> vector1 = Avx2.Permute4x64(vector.AsInt64(), (byte)ShuffleControlG4.ZWXY).AsByte(); // 3,1
          Vector256<byte> indices0 = Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K0); // 1,0.33
          Vector256<byte> indices1 = Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K1); // 1,0.33
          Vector256<byte> v0 = Avx2.Shuffle(vector, indices0); // 1,0.5
          Vector256<byte> v1 = Avx2.Shuffle(vector1, indices1); // 1,0.5
          Vector256<byte> rt = Avx2.Or(v0, v1); // 1,0.33
          return rt; //total latency: 8, total throughput CPI: 3
      }
      

      上面這段代碼,還參考了 Intel的手冊里IceLake的指標,估算了一下延遲與吞吐率。總延遲(total latency)為8個時鐘周期,總吞吐率(total throughput CPI)為3。

      2.2.2 實現2向量換位(YShuffleX2Kernel)

      根據先前128位時的處理經驗,組合使用2個單向量的Shuffle方法,就能實現2向量換位的方法 YShuffleX2Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX2Kernel_Combine(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> indices) {
          Vector256<byte> vCount = Vector256.Create((byte)Vector256<byte>.Count);
          Vector256<byte> indices1 = Avx2.Subtract(indices, vCount);
          Vector256<byte> rt0 = YShuffleKernel_ByteAdd2(vector0, indices);
          Vector256<byte> mask = Avx2.CompareGreaterThan(vCount.AsSByte(), indices.AsSByte()).AsByte(); // vCount[i]>indices[i] ==> indices[i]<vCount[i].
          Vector256<byte> rt1 = YShuffleKernel_ByteAdd2(vector1, indices1);
          Vector256<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1);
          return rt;
      }
      

      同樣的,可以手動對各個向量指令調整順序,盡可能提高這段代碼的運行時效率。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX2Kernel_Combine3(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> indices) {
          Vector256<byte> vCount = Vector256.Create((byte)Vector256<byte>.Count);
          // Format: Code; //Latency, Throughput(references IceLake)
          Vector256<byte> vector0B = Avx2.Permute4x64(vector0.AsInt64(), (byte)ShuffleControlG4.ZWXY).AsByte(); // 3,1
          Vector256<byte> vector1B = Avx2.Permute4x64(vector1.AsInt64(), (byte)ShuffleControlG4.ZWXY).AsByte(); // 3,1
          Vector256<byte> indices1 = Avx2.Subtract(indices, vCount); // 1,0.33
          Vector256<byte> indices0A = Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K0); // 1,0.33
          Vector256<byte> indices0B = Avx2.Add(indices, Vector256Constants.Shuffle_Byte_LaneAdd_K1); // 1,0.33
          Vector256<byte> indices1A = Avx2.Add(indices1, Vector256Constants.Shuffle_Byte_LaneAdd_K0); // 1,0.33
          Vector256<byte> indices1B = Avx2.Add(indices1, Vector256Constants.Shuffle_Byte_LaneAdd_K1); // 1,0.33
          Vector256<byte> rt0A = Avx2.Shuffle(vector0, indices0A); // 1,0.5
          Vector256<byte> rt0B = Avx2.Shuffle(vector0B, indices0B); // 1,0.5
          Vector256<byte> rt1A = Avx2.Shuffle(vector1, indices1A); // 1,0.5
          Vector256<byte> rt1B = Avx2.Shuffle(vector1B, indices1B); // 1,0.5
          Vector256<byte> mask = Avx2.CompareGreaterThan(vCount.AsSByte(), indices.AsSByte()).AsByte(); // 1,0.5. vCount[i]>indices[i] ==> indices[i]<vCount[i].
          Vector256<byte> rt0 = Avx2.Or(rt0A, rt0B); // 1,0.33
          Vector256<byte> rt1 = Avx2.Or(rt1A, rt1B); // 1,0.33
          Vector256<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1); // 3,1
          return rt; //total latency: 21, total throughput CPI: 7.83
      }
      

      上面這段代碼,還參考了 Intel的手冊里IceLake的指標,估算了一下延遲與吞吐率。總延遲(total latency)為21個時鐘周期,總吞吐率(total throughput CPI)為7.83。

      2.2.3 實現3向量換位(YShuffleX3Kernel)

      根據先前128位時的處理經驗,組合使用3個單向量的Shuffle方法,就能實現3向量換位的方法 YShuffleX2Kernel。源代碼如下。

      /// <inheritdoc cref="IWVectorTraits256.YShuffleX3Kernel(Vector256{byte}, Vector256{byte}, Vector256{byte}, Vector256{byte})"/>
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX3Kernel_Combine(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> vector2, Vector256<byte> indices) {
          Vector256<byte> vCount2 = Vector256.Create((byte)(Vector256<byte>.Count * 2));
          Vector256<byte> indices1 = Avx2.Subtract(indices, vCount2);
          Vector256<byte> rt0 = YShuffleX2Kernel_Combine3(vector0, vector1, indices);
          Vector256<byte> mask = Avx2.CompareGreaterThan(vCount2.AsSByte(), indices.AsSByte()).AsByte(); // vCount2[i]>indices[i] ==> indices[i]<vCount2[i].
          Vector256<byte> rt1 = YShuffleKernel_ByteAdd2(vector2, indices1);
          Vector256<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1);
          return rt;
      }
      

      2.3 用Avx512系列指令集改進256位向量的操作

      .NET 8.0 新增了對 Avx512系列指令集的支持。Avx512不僅提供了“跨小道重排指令”(_mm_permutexvar_epi8),且提供了“2向量的跨小道重排指令”(_mm_permutex2var_epi8)。這些指令能有效的改進多向量換位方法。

      Avx512系列指令集有多個子集,其中Avx512VL就是負責處理128~256位向量的指令集。隨后它可以與Avx512BW、Avx512DQ、Avx512F、Avx512Vbmi 等子集進行配合,能處理元素大小為 8~64位的數據。例如 Avx512Vbmi 里含有上面所說的 “跨小道重排指令”,且提供了“2向量的跨小道重排指令”。于是 .NET中可以通過 Avx512Vbmi.VL類 來處理 128~256位向量的這些重排指令。

      其實 VectorTraits 也支持 512位向量的多向量換位,源代碼詳見 WVectorTraits512Avx512.YS.cs,有興趣的讀者可以自行翻閱。由于目前自動大小向量Vertor最大為256位,于是本文將重點放在用Avx512系列指令集改進256位向量的操作上。

      2.3.1 實現單向量換位(YShuffleKernel)

      使用Avx512Vbmi所提供 “跨小道重排指令”(_mm256_permutexvar_epi8),可以直接實現全256位的換位。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleKernel(Vector256<byte> vector, Vector256<byte> indices) {
      #if NET8_0_OR_GREATER
          if (Avx512Vbmi.VL.IsSupported) {
              return Avx512Vbmi.VL.PermuteVar32x8(vector, indices);
              //__m256i _mm256_permutexvar_epi8 (__m256i idx, __m256i a)
              //#include <immintrin.h>
              //Instruction: vpermb ymm, ymm, ymm
              //CPUID Flags: AVX512_VBMI + AVX512VL
              //Latency and Throughput
              //Architecture	Latency	Throughput (CPI)
              //Icelake Intel Core	-	1
              //Icelake Xeon	3	1
              //Sapphire Rapids	5	1
          }
      #endif // NET8_0_OR_GREATER
          return YShuffleKernel_ByteAdd2(vector, indices);
      }
      
      • 若當前CPU支持Avx512系列指令集,便使用它提供的高效指令。保障了性能。從上面代碼中的注釋可以看出,Sapphire Rapids時該指令的延遲為5個時鐘周期,吞吐率為1。
      • 若當前CPU不支持Avx512系列指令集,便回退為Avx2的實現。保障了兼容性。YShuffleKernel_ByteAdd2 方法的總延遲為8個時鐘周期,總吞吐率為3。

      這樣便實現了“自動使用當前處理器最佳指令”的效果。

      2.3.2 實現2向量換位(YShuffleX2Kernel)

      使用Avx512Vbmi所提供 “2向量的跨小道重排指令”(_mm256_permutex2var_epi8),可以方便的實現 YShuffleX2Kernel方法。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX2Kernel(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> indices) {
      #if NET8_0_OR_GREATER
          if (Avx512Vbmi.VL.IsSupported) {
              return Avx512Vbmi.VL.PermuteVar32x8x2(vector0, indices, vector1);
          }
      #endif // NET8_0_OR_GREATER
          return YShuffleX2Kernel_Combine3(vector0, vector1, indices);
      }
      

      摘錄一下 Intel手冊對 _mm256_permutex2var_epi8 指令的說明。

      __m256i _mm256_permutex2var_epi8 (__m256i a, __m256i idx, __m256i b)
      #include <immintrin.h>
      Instruction: vpermi2b ymm, ymm, ymm
      CPUID Flags: AVX512_VBMI + AVX512VL
      Description
      Shuffle 8-bit integers in a and b across lanes using the corresponding selector and index in idx, and store the results in dst.
      
      Latency and Throughput
      Architecture	Latency	Throughput (CPI)
      Icelake Intel Core	-	2
      Icelake Xeon	-	2
      Sapphire Rapids	4	2
      
      • 若當前CPU支持Avx512系列指令集,便使用它提供的高效指令。保障了性能。從上面代碼中的注釋可以看出,Sapphire Rapids時該指令的延遲為4個時鐘周期,吞吐率為2。
      • 若當前CPU不支持Avx512系列指令集,便回退為Avx2的實現。保障了兼容性。YShuffleX2Kernel_Combine3 方法的總延遲為21個時鐘周期,總吞吐率為7.83。

      2.3.3 實現3向量換位(YShuffleX3Kernel)

      使用Avx512Vbmi所提供 “跨小道重排指令”、“2向量的跨小道重排指令”,可以方便的實現 YShuffleX3Kernel方法。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX3Kernel_Permute(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> vector2, Vector256<byte> indices) {
          if (!Avx512Vbmi.VL.IsSupported) VectorMessageFormats.ThrowNewUnsupported("Avx512Vbmi, Avx512VL");
          Vector256<byte> vCount2 = Vector256.Create((byte)(Vector256<byte>.Count * 2));
          Vector256<byte> indices1 = Avx2.Subtract(indices, vCount2);
          Vector256<byte> mask = Avx2.CompareGreaterThan(vCount2.AsSByte(), indices.AsSByte()).AsByte(); // vCount2[i]>indices[i] ==> indices[i]<vCount2[i].
          Vector256<byte> rt0 = Avx512Vbmi.VL.PermuteVar32x8x2(vector0, indices, vector1);
          Vector256<byte> rt1 = Avx512Vbmi.VL.PermuteVar32x8(vector2, indices1);
          Vector256<byte> rt = ConditionalSelect_Relaxed(mask, rt0, rt1);
          return rt;
      }
      

      2.3.4 利用512位向量,進一步優化3向量換位(YShuffleX3Kernel)

      還有沒有辦法進一步優化呢?

      有辦法,就是利用512位向量的長度。

      既然已經支持 Avx512Vbmi了,那么應該是支持512位向量的。1個512位向量,可以放下2個256位向量。于是對1個512位向量進行重排,就相當于對2個256位進行“2向量換位”。

      且對 2個512位向量進行重排,相當于對4個256位進行“4向量換位”。目前我們僅需3個256位就行了,于是可以減少一個輸入參數。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector256<byte> YShuffleX3Kernel_PermuteLonger(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> vector2, Vector256<byte> indices) {
          if (!Avx512Vbmi.IsSupported) VectorMessageFormats.ThrowNewUnsupported("Avx512Vbmi");
          Vector512<byte> l = vector0.ToVector512Unsafe().WithUpper(vector1);
          Vector512<byte> u = vector2.ToVector512Unsafe();
          return Avx512Vbmi.PermuteVar64x8x2(l, indices.ToVector512Unsafe(), u).GetLower();
      }
      

      ToVector512Unsafe().WithUpper,就是將2個256位向量,組合成1個512位向量。

      而單獨使用 ToVector512Unsafe,是將 1個256位向量,重新解釋成1個512位向量,高256位不管。它利用了Avx512指令集中“zmm的低256位就是ymm寄存器”的特點,實際上不用生成額外的轉換指令。最后的 GetLower 也利用了這一點。

      僅 WithUpper 需靠 _mm512_inserti64x4(vinserti64x4)指令來實現。

      隨后為了能“自動使用當前處理器最佳指令”,故也寫上指令集判斷。

      public static Vector256<byte> YShuffleX3Kernel(Vector256<byte> vector0, Vector256<byte> vector1, Vector256<byte> vector2, Vector256<byte> indices) {
      #if NET8_0_OR_GREATER
          if (Shuffle_Use_Longer && Avx512Vbmi.IsSupported) {
              return YShuffleX3Kernel_PermuteLonger(vector0, vector1, vector2, indices);
          } else if (Avx512Vbmi.VL.IsSupported) {
              return YShuffleX3Kernel_Permute(vector0, vector1, vector2, indices);
          }
      #endif // NET8_0_OR_GREATER
          return YShuffleX3Kernel_Combine(vector0, vector1, vector2, indices);
      }
      

      Shuffle_Use_Longer 是我定義的一個常量(const),用于對比各種辦法的性能。由于它是常量,于是JIT編譯時會自動剪裁的,不會帶來額外的分支跳轉開銷。

      最后摘錄一下 Intel手冊對 _mm512_permutex2var_epi8 指令的說明。

      __m512i _mm512_permutex2var_epi8 (__m512i a, __m512i idx, __m512i b)
      #include <immintrin.h>
      Instruction: vpermi2b zmm, zmm, zmm
      CPUID Flags: AVX512_VBMI
      
      Latency and Throughput
      Architecture	Latency	Throughput (CPI)
      Icelake Intel Core	-	2
      Icelake Xeon	-	2
      Sapphire Rapids	6	2
      

      從信息里可以看出,Sapphire Rapids時該指令的延遲為6個時鐘周期,吞吐率為2。它比起先前組合實現的辦法,性能又能提升很多。

      2.4 對運行時JIT編譯的本機代碼進行反匯編查看

      本節探討更專業的內容,適合熟悉向量指令匯編代碼的專業人士。若你覺得過于艱深,可以跳過此節。

      觀察運行時的匯編代碼,能夠更清晰的分析代碼的性能瓶頸。

      2.4.1 反匯編查看的辦法

      若想對運行時JIT編譯的本機代碼進行反匯編查看,最簡單的辦法是使用Visual Studio的“Disassembly”功能。詳細說明見 C# 使用SIMD向量類型加速浮點數組求和運算(5):如何查看Release程序運行時匯編代碼,本節簡單說明一下。

      具體辦法是:在Visual Studio里打開程序的解決方案,并設置斷點。按“F5”運行程序直至遇到斷點,然后點擊菜單欄里的“DEBUG”(調試)->“Windows”(窗口)->“Disassembly”(反匯編),便會打開“Disassembly”(反匯編)窗口。

      此時需注意“分層編譯”的影響。為了提高程序的啟動速度,.NET 推出了“分層編譯”技術。即在程序啟動時,幾乎不進行編譯優化,而是以最簡單、最快的辦法進行即時編譯(JIT),使程序能在很短的時間內啟動。隨后JIT會監控程序的熱點代碼, 對熱點代碼進行 慢速的、復雜的二次編譯,此時才會使用多種編譯優化手段。例如改為內聯使用內在函數,而不是函數調用。

      為了查看編譯優化后的匯編代碼,最好在熱點代碼運行后才觸發斷點。其實 ImageFlipXOn24bitBenchmark.cs 里已有準備。將 Setup 方法末尾的allowDebugBreak相關的代碼取消注釋,并將 allowDebugBreak 賦值為 true。即將代碼改為下面這樣。

      // Debug break.
      bool allowDebugBreak = true;
      if (allowDebugBreak) {
          for (int i = 0; i < 10000; ++i) {
              UseVectors();
          }
          Debugger.Break();
          UseVectors();
      }
      

      按F5運行程序。不久后,程序會因斷點而暫停,停在 Debugger.Break 后面的 UseVectors語句上。此時通過主菜單,打開“Disassembly”(反匯編)窗口。隨后在“Disassembly”窗口內按多次 單步運行的快捷鍵(一般是 F11),使程序運行到 UseVectorsDoBatch 方法內。于是便可以觀察 UseVectorsDoBatch 的匯編代碼了。

      2.4.2 查看 .NET 7.0JIT編譯結果

      下圖是 .NET 7.0JIT編譯結果。

      FlipX24_x86_net7_avx2.png

      由于 .NET 7.0 不支持 Avx512,故使用了 Avx2時的算法。可參考“2.2.3 實現3向量換位(YShuffleX3Kernel)”,讀懂這一段的匯編代碼。

      2.4.3 查看 .NET 8.0JIT編譯結果

      下圖是 .NET 8.0JIT編譯結果。

      FlipX24_x86_net8_avx512.png

      比起 .NET 7.0的編譯結果,內循環的要簡短很多,且出現了Avx512的zmm寄存器。這是由于處理器支持 Avx512,于是便使用了 Avx512的算法。可參考“2.3.4 利用512位向量,進一步優化3向量換位(YShuffleX3Kernel)”,讀懂這一段的匯編代碼。

      從上圖可以看出,JIT編譯的本機代碼的質量很高。這幾點處理的比較好:

      • 將 indices0、indices1、indices的加載,挪至循環前(ymm0、ymm1、ymm2)。
      • 對3次 YShuffleX3Kernel 中的相同操作進行了合并,所以僅有1條 vinserti64x4 指令。
      • 充分利用了 “zmm的低256位就是ymm寄存器”的特點,沒有多余的寄存器 mov操作。

      三、Arm架構

      Arm架構提供了 TableLookup(查表) 指令,可以用它來實現向量內的換位。

      32位時,該指令的返回值只有64位,用起來不太方便。而64位時,該指令能返回完整的128位結果。于是先從64位指令介紹起。

      .NET 8.0 新增了對 AdvSimd指令集里的“2-4向量查表”指令的支持。這能給我們的方法帶來進一步的性能提升,最后會來講解它。

      3.1 用64位的AdvSimd指令集操作128位向量

      3.1.1 實現單向量換位(YShuffleKernel)

      64位的Arm架構提供了 TableLookup(查表) 指令,它對應了 AdvSimd.Arm64.VectorTableLookup 方法。該方法的定義如下。

      // https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.intrinsics.arm.advsimd.arm64.vectortablelookup?view=net-8.0
      // uint8x16_t vqvtbl1q_u8(uint8x16_t t,uint8x16_t idx)
      // A64:TBL Vd.16B、{Vn.16B}、Vm.16B
      public static Vector128<byte> VectorTableLookup (Vector128<byte> table, Vector128<byte> byteIndexes);
      

      使用該方法,便能實現單向量換位的方法 YShuffleKernel。源碼在 WVectorTraits128AdvSimdB64.YS.cs

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleKernel(Vector128<byte> vector, Vector128<byte> indices) {
          var rt = AdvSimd.Arm64.VectorTableLookup(vector, indices);
          return rt;
      }
      

      3.1.2 實現2向量換位(YShuffleX2Kernel)

      根據先前Sse時的處理經驗,組合使用2個單向量的Shuffle方法,就能實現2向量換位的方法 YShuffleX2Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX2Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> indices) {
          Vector128<byte> vCount = Vector128.Create((byte)Vector128<byte>.Count);
          Vector128<byte> indices1 = AdvSimd.Subtract(indices, vCount);
          Vector128<byte> rt0 = YShuffleKernel(vector0, indices);
          Vector128<byte> rt1 = YShuffleKernel(vector1, indices1);
          Vector128<byte> rt = AdvSimd.Or(rt0, rt1);
          return rt;
      }
      

      Arm的查表指令有一個特點,索引一旦超過范圍就會被清零。而X86的shuffle指令,僅在最高位為1時才會清零,于是需要手動判斷索引是否超過范圍,比較繁瑣。

      根據Arm的這個特點,便可以利用減法來調整索引,最后用 或(Or)運算將2次查表的結果給合并。所以上面的代碼對比Sse的,看來更清爽一些。

      3.1.3 實現3向量換位(YShuffleX3Kernel)

      根據先前的處理經驗,組合使用3個單向量的Shuffle方法,就能實現3向量換位的方法 YShuffleX3Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX3Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> vector2, Vector128<byte> indices) {
          Vector128<byte> vCount = Vector128.Create((byte)Vector128<byte>.Count);
          Vector128<byte> indices1 = AdvSimd.Subtract(indices, vCount);
          Vector128<byte> indices2 = AdvSimd.Subtract(indices1, vCount);
          Vector128<byte> rt0 = YShuffleKernel(vector0, indices);
          Vector128<byte> rt1 = YShuffleKernel(vector1, indices1);
          rt0 = AdvSimd.Or(rt0, rt1);
          Vector128<byte> rt2 = YShuffleKernel(vector2, indices2);
          rt0 = AdvSimd.Or(rt0, rt2);
          return rt0;
      }
      

      3.2 用32位的AdvSimd指令集操作128位向量

      3.2.1 實現單向量換位(YShuffleKernel)

      32位的Arm架構也提供了 TableLookup(查表) 指令,它對應了 AdvSimd.VectorTableLookup 方法。該方法的定義如下。

      // https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.intrinsics.arm.advsimd.vectortablelookup?view=net-8.0
      // uint8x8_t vqvtbl1_u8(uint8x16_t t, uint8x8_t idx)
      // A32:VTBL Dd、{Dn, Dn+1}、Dm
      // A64:TBL Vd.8B、{Vn.16B}、Vm.8B
      public static Vector64<byte> VectorTableLookup (Vector128<byte> table, Vector64<byte> byteIndexes);
      

      可以注意到,它的索引(byteIndexes)及返回值,都只有64位(Vector64)。

      若需要128位的結果,就得調用2次VectorTableLookup。源碼在 WVectorTraits128AdvSimd.YS.cs

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleKernel(Vector128<byte> vector, Vector128<byte> indices) {
          var lower = AdvSimd.VectorTableLookup(vector, indices.GetLower());
          var upper = AdvSimd.VectorTableLookup(vector, indices.GetUpper());
          var rt = lower.ToVector128Unsafe().WithUpper(upper); //Vector128.Create(lower, upper);
          return rt;
      }
      

      GetLower 方法,可以獲取向量的下半部分。GetUpper 方法,可以獲取向量的上半部分。故可以分別傳遞給 VectorTableLookup,得到2個64位的結果。

      最后將這2個64位的結果,組合成1個128位,便完成了處理。

      注意在早期版本 .NET 中,Vector128.Create 的性能比較低,因它是借助內存來組合向量的。可以改為用 ToVector128Unsafe與WithUpper,這便在寄存器內完成了向量的組合。

      3.2.2 實現2向量換位(YShuffleX2Kernel)

      根據先前的處理經驗,組合使用2個單向量的Shuffle方法,就能實現2向量換位的方法 YShuffleX2Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX2Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> indices) {
          Vector128<byte> vCount = Vector128.Create((byte)Vector128<byte>.Count);
          Vector128<byte> indices1 = AdvSimd.Subtract(indices, vCount);
          Vector128<byte> rt0 = YShuffleKernel(vector0, indices);
          Vector128<byte> rt1 = YShuffleKernel(vector1, indices1);
          Vector128<byte> rt = AdvSimd.Or(rt0, rt1);
          return rt;
      }
      

      代碼與64位架構時完全一致。由于在不同的類中,于是調用了各自的 YShuffleKernel 方法。而 YShuffleKernel 封裝了底層不一致的細節(32位架構僅返回64位),使上層代碼能夠完全一致。

      3.2.3 實現3向量換位(YShuffleX3Kernel)

      根據先前的處理經驗,組合使用3個單向量的Shuffle方法,就能實現3向量換位的方法 YShuffleX3Kernel。源代碼如下。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX3Kernel_Combine(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> vector2, Vector128<byte> indices) {
          Vector128<byte> vCount = Vector128.Create((byte)Vector128<byte>.Count);
          Vector128<byte> indices1 = AdvSimd.Subtract(indices, vCount);
          Vector128<byte> indices2 = AdvSimd.Subtract(indices1, vCount);
          Vector128<byte> rt0 = YShuffleKernel(vector0, indices);
          Vector128<byte> rt1 = YShuffleKernel(vector1, indices1);
          rt0 = AdvSimd.Or(rt0, rt1);
          Vector128<byte> rt2 = YShuffleKernel(vector2, indices2);
          rt0 = AdvSimd.Or(rt0, rt2);
          return rt0;
      }
      

      3.3 使用 .NET 8.0新增的多向量查表指令

      3.3.1 簡介

      對于AdvSimd.Arm64.VectorTableLookup 方法,.NET 5.0 的文檔是只有2個重載方法。

      VectorTableLookup(Vector128<SByte>, Vector128<SByte>)    // int8x16_t vqvtbl1q_s8(int8x16_t t, uint8x16_t idx)
      VectorTableLookup(Vector128<Byte>, Vector128<Byte>)    // uint8x16_t vqvtbl1q_u8(uint8x16_t t, uint8x16_t idx)
      

      到了.NET 8.0 ,文檔多了6個重載方法。

      VectorTableLookup(ValueTuple<Vector128<Byte>,Vector128<Byte>,Vector128<Byte>,Vector128<Byte>>, Vector128<Byte>)        // uint8x16_t vqtbl4q_u8 (uint8x16x4_t t、uint8x16_t idx)
      VectorTableLookup(ValueTuple<Vector128<Byte>,Vector128<Byte>,Vector128<Byte>>, Vector128<Byte>)    // uint8x16_t vqtbl3q_u8 (uint8x16x3_t t、uint8x16_t idx)
      VectorTableLookup(ValueTuple<Vector128<Byte>,Vector128<Byte>>, Vector128<Byte>)    // uint8x16_t vqtbl2q_u8 (uint8x16x2_t t、uint8x16_t idx)
      VectorTableLookup(ValueTuple<Vector128<SByte>,Vector128<SByte>,Vector128<SByte>,Vector128<SByte>>, Vector128<SByte>)    // int8x16_t vqtbl4q_s8 (int8x16x4_t t、uint8x16_t idx)
      VectorTableLookup(ValueTuple<Vector128<SByte>,Vector128<SByte>,Vector128<SByte>>, Vector128<SByte>)    // int8x16_t vqtbl3q_s8 (int8x16x3_t t、uint8x16_t idx)
      VectorTableLookup(ValueTuple<Vector128<SByte>,Vector128<SByte>>, Vector128<SByte>)    // int8x16_t vqtbl2q_s8 (int8x16x2_t t、uint8x16_t idx)
      

      可見,2、3、4個向量的查表功能都加上了了。隨后再區分一下 Byte/SByte 這2種類型,于是共增加了 3*2=6 個重載方法。

      3.3.2 實現2向量換位(YShuffleX2Kernel)

      有了2向量查表的指令后,便能輕松的實現YShuffleX2Kernel方法。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX2Kernel(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> indices) {
      #if ARM_ALLOW_LOOKUP_X
          var rt = AdvSimd.Arm64.VectorTableLookup((vector0, vector1), indices);
          return rt;
      #else
          return YShuffleX2Kernel_Combine(vector0, vector1, indices);
      #endif
      }
      

      由于經常需要判斷是否支持多向量換位,于是在源文件頂部,專門定義了它的條件編譯符號 ARM_ALLOW_LOOKUP_X。

      #if NET8_0_OR_GREATER
      #define ARM_ALLOW_LOOKUP_X
      #endif // NET8_0_OR_GREATER
      

      3.3.3 實現3向量換位(YShuffleX3Kernel)

      有了3向量查表的指令后,便能輕松的實現YShuffleX3Kernel方法。

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public static Vector128<byte> YShuffleX3Kernel(Vector128<byte> vector0, Vector128<byte> vector1, Vector128<byte> vector2, Vector128<byte> indices) {
      #if ARM_ALLOW_LOOKUP_X
          var rt = AdvSimd.Arm64.VectorTableLookup((vector0, vector1, vector2), indices);
          return rt;
      #else
          return YShuffleX3Kernel_Combine(vector0, vector1, vector2, indices);
      #endif
      }
      

      四、結語

      弄懂YShuffleX3Kernel的算法原理后,便可以在各個場景使用它了。

      對于圖像的垂直翻轉,向量算法幾乎和標量算法一樣簡單。而對于圖像的水平翻轉,標量算法僅需改造地址計算便實現了,可向量算法一下子變得很復雜,這是為什么呢?

      這是因為標量算法是以字節(Byte)為單位進行操作的,地址計算可以靈活定位到每一個字節。而對于向量算法,向量類型的大小一般是128位(16字節)或是更長,顆粒度很大。而且大多數向量方法是在垂直方向工作的,例如 Vector.Add。這種工作方式,比較適合處理一維數組。

      但是圖像是2維的,有X、Y這2種坐標分量。垂直翻轉僅需翻轉Y坐標,X坐標沒有變,于是處理起來很簡單。而水平翻轉需要翻轉X坐標,這就涉及到向量內的字節級別地址定位了。

      為了解決向量內的字節級別地址定位難題,就需要使用換位類別的指令。例如 X86的 shuffle(換位)、permute(重排)指令,Arm的 TableLookup(查表)指令。

      32位像素雖然內部有R、G、B通道的區分,但在水平翻轉時可當做一個整體來處理,即當做32位整數。于是使用單向量的換位(YShuffleKernel),便能編寫出水平翻轉的向量算法。

      對于 24位像素,它是3個字節一組。而向量大小是16字節的整數倍,無法被3整除。此時連單向量換位都難以處理,于是需要使用3向量換位。

      高色彩深度的像素也可以按照同樣的辦法來處理,例如 64位像素(R16G16B16A16)用單向量換位,48位像素(R16G16B16)用3向量換位。

      其實大多數牽涉X坐標的圖像處理,都可以使用 YShuffleKernel 等方法。有更佳的專用方法時除外,例如對于像素數據的交織與解交織,VectorTraits庫是提供了性能更好的專用方法的,范例見 [C#] Bgr24彩色位圖轉為Gray8灰度位圖的跨平臺SIMD硬件加速向量算法

      附錄

      posted on 2024-12-11 22:37  zyl910  閱讀(305)  評論(3)    收藏  舉報
      主站蜘蛛池模板: 亚洲国产精品综合久久20| 亚洲综合国产激情另类一区| 狠狠躁夜夜躁人人爽天天古典| 国产精品人妻中文字幕| 亚洲一区二区三区丝袜| 热久在线免费观看视频| 欧美成人h亚洲综合在线观看| 鲁丝一区二区三区免费| 日本丰满的人妻hd高清在线| 在线精品国产中文字幕| 九九热在线精品视频九九| mm1313亚洲国产精品| 老司机午夜精品视频资源| 4480yy亚洲午夜私人影院剧情| 亚洲综合中文字幕首页| 吉安市| 国产精品麻豆va在线播放| 亚洲一区二区三午夜福利| 国产精品亚洲精品日韩已满十八小| 国产女人喷潮视频免费| 成在线人免费视频| 影音先锋女人AA鲁色资源| 中文字幕国产精品自拍| 人妻影音先锋啪啪av资源| 亚洲国产午夜福利精品| 精品无码国产一区二区三区51安| 欧美成人黄在线观看| 久久精品国产亚洲综合av| 国产精品黑色丝袜在线观看| 色香欲天天影视综合网| 疯狂做受xxxx高潮欧美日本| 国内不卡一区二区三区| 欧美性受xxxx黑人猛交| 91亚洲精品一区二区三区| 日韩精品无码一区二区视频| 亚洲伊人成无码综合网| 久久久无码精品亚洲日韩蜜桃| 色猫咪av在线网址| 国内综合精品午夜久久资源| 日韩中文字幕免费在线观看| 一区二区三区国产亚洲网站|