老周在幾個世紀前曾寫過樹莓派相關的 iOT 水文,之所以沒寫 Nano Framework 相關的內容,是因為那時候這貨還不成熟,可玩性不高。不過,這貨現在已經相對完善,老周都把它用在項目上了——第一個是自製的智能插座,這個某寶上50多塊可以買到,搜“esp32 插座”就能找到。一種是 86 型盒子 ...
原文鏈接:
https://devblogs.microsoft.com/dotnet/dotnet-8-hardware-intrinsics/
Hardware Intrinsics in .NET 8
Tanner Gooding [MSFT]
December 11th, 2023
譯文:
.NET 8 中的硬體內在函數 坦納·古丁 [MSFT] 2023年12月11日.NET在通過JIT編譯器本質上理解的API提供對附加硬體功能的訪問方面有著悠久的歷史。這始於2014年的.NET Framework,並隨著2019年.NET Core 3.0的引入而擴展。從那時起,運行時迭代地提供了更多的API,併在每個版本中更好地利用了這一點。
簡要概述如下:
- 2014年- .NET 4.5.2 -第一批在
System.Numerics
命名空間中公開的 API- 介紹
Vector<T>
- 引入
Vector2
、Vector3
、Vector4
、Matrix4x4
、Quaternion
和Plane
- 64-僅位
- 另請參閱:https://devblogs.microsoft.com/dotnet/the-jit-finally-proposed-jit-and-simd-are-getting-married/
- 介紹
- 2019年- .NET Core 3.0 -第一批在
System.Runtime.Intrinsics
命名空間中公開的 API- 介紹
Vector128<T>
和Vector256<T>
- 為
x86和x64引入Sse、
Sse2、
Sse3
、Ssse3
、Sse41
、Sse42
、Avx
、Avx2
、Fma
、Bmi1
、Bmi2
、Lzcnt
、Popcnt
、Aes
、Pclmul
- 32-位和64位支持
- 另請參閱:https://devblogs.microsoft.com/dotnet/hardware-intrinsics-in-net-core/
- 介紹
- 2020年- .NET 5 -
System.Runtime.Intrinsics
命名空間中添加了 Arm 支持- 介紹
Vector64<T>
- 為
Arm/Arm64
引入AdvSimd、ArmBase、Dp
、Rdm
、Aes
、Crc32
、Sha1
、Sha256
- 為
X86Base
/x86
引入x64
- 另請參閱:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-7/
- 介紹
- 2021 - .NET 6 - Codegen和基礎設施改進
- 為
x86/x64
引入AvxVnni
- 重寫
System.Numerics
實現以使用System.Runtime.Intrinsics
- 另請參閱:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/
- 為
- 2022 - .NET 7 -支持編寫跨平臺演算法
- 在跨平臺工作的
Vector64<T>
、Vector128<T>
和Vector256<T>
類型上引入了重要的新功能 - 為
x86/x64
引入X86Serialize
- 使上述向量類型和
Vector<T>
公開的API界面具有奇偶性 - 另請參閱:https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/
- 在跨平臺工作的
- 2023 - .NET 8 - 支持 wasm 和AVX-512
- 為
Wasm引入PackedSimd
和WasmBase
- 介紹
Vector512<T>
- 為
x86/x64
引入Avx512F、Avx512BW、Avx512CD
、Avx512DQ
、Avx512Vbmi
- 另請參閱:此博客文章的其餘部分
- 為
由於這項工作,每個版本的.NET庫和應用程式都獲得了更多的能力來利用底層硬體。在這篇文章中,我將深入介紹我們在.NET 8中引入的內容以及它所支持的功能類型。
WebAssembly支持
WebAssembly,簡稱Wasm,本質上是在瀏覽器中運行的代碼,它允許比典型的解釋型腳本支持更高的性能配置文件。作為一個平臺,Wasm已經開始提供底層SIMD(單指令,多數據)支持,以便可以加速核心演算法,.NET相應地選擇通過硬體內部函數公開對此功能的支持。
這種支持與其他平臺提供的基礎非常相似,因此我們不會詳細介紹。相反,您可以簡單地期望使用Vector128<T>
的現有跨平臺演算法將隱式地照亮支持的地方。如果您想更直接地利用Wasm獨有的功能,那麼您可以顯式地使用PackedSimd
命名空間中的WasmBase
和System.Runtime.Intrinsics.Wasm
類公開的API。
AVX-512支持
AVX-512是為x86和x64電腦提供的新功能集。它帶來了沿著的大量新指令和硬體功能,包括支持16個額外的SIMD寄存器,專用掩碼,以及一次操作512位數據。訪問此功能需要一個相對較新的處理器,即需要英特爾的Skylake-X或更新版本,以及AMD的Zen 4或更新版本。因此,可以利用此新功能的用戶數量較少,但它可以為硬體帶來的改進仍然很重要,並且值得支持數據繁重的工作負載。此外,JIT將在其確定存在益處的情況下針對現有SIMD代碼機會性地利用這些指令。一些例子包括:
- 當完成按位條件選擇時使用
vpternlog
而不是and, andn, or
(Vector128.ConditionalSelect
) - 使用EVEX編碼將更多操作放入更少的代碼位元組中,例如用於嵌入式廣播(
x + Vector128.Create(5)
) - 使用更新的指令,其中支持AVX-512,例如全寬度混洗和許多
long
/ulong
(Int64
/UInt64
)操作 - 還有其他的改進,這裡沒有列出,你可以期待隨著時間的推移會有更多的改進。
- 某些情況下,例如
Vector<T>
允許擴展到512位,在.NET 8中沒有完成
- 某些情況下,例如
為了支持512位的新向量大小,.NET引入了Vector512<T>
類型。這公開了與其他固定大小的向量類型(如Vector256<T>
)相同的通用API錶面。它同樣繼續暴露Vector512.IsHardwareAccelerated
屬性,該屬性允許您確定是否應該在硬體中加速通用邏輯,或者是否最終通過軟體回退來模擬行為。
Vector 512在Ice Lake和更新的硬體上預設使用AVX-512加速(因此Vector512.IsHardwareAccelerated
報告true
),其中AVX-512指令不會導致CPU顯著降頻;而使用AVX-512指令會導致Skylake-X,Cascade Lake和庫珀Lake硬體上更顯著的降頻(另請參見2.5.3 Skylake Server Power Management
中的Intel® 64 and IA-32 Architectures Optimization Reference Manual: Volume 1
)。雖然這最終有利於大型工作負載,但它可能會對其他較小的工作負載產生負面影響,因此我們預設在這些平臺上報告false
為Vector512.IsHardwareAccelerated
。Avx512F.IsSupported
仍然會報告true,如果直接調用,Vector512
的底層實現仍然會使用AVX-512
指令。這允許工作負載利用他們知道的功能,而不會意外地對其他人造成負面影響。
特別感謝
這一功能的實現得益於我們在英特爾的朋友們的巨大貢獻。多年來,.NET團隊和英特爾已經進行了多次合作,我們繼續在整體設計和實現方面進行合作,從而使AVX-512支持登陸.NET 8。
還有來自.NET社區的大量輸入和驗證,幫助實現了成功並使發佈變得更好。
如果您想貢獻或提供輸入,請加入我們在GitHub上的dotnet/runtimerepos,並按照我們的時間表在.NET Foundation YouTube頻道上收聽API Review,您可以看到我們討論.NET庫的新添加,甚至通過聊天頻道提供您自己的輸入。
不只是512位?
與名稱相反,AVX-512不僅僅是512位支持。額外的寄存器、掩碼支持、嵌入式舍入或廣播支持以及新指令也都適用於128位和256位向量。這意味著您現有的工作負載可以隱式地變得更好,並且您可以顯式地利用新功能,而這種隱式的點亮是不可能的。
當SSE於1999年在Intel Pentium III上首次引入時,它提供了8個寄存器,每個寄存器長度為128位。這些寄存器被稱為xmm0
到xmm7
。當x64平臺後來於2003年在AMD Athlon 64上推出時,它提供了8個額外的寄存器,可以訪問64位代碼。這些寄存器被命名為xmm8
到xmm15
。這種初始支持使用了一種簡單的編碼方案,其工作方式與通用指令非常相似,只允許指定2個寄存器。對於需要2個輸入的加法,這意味著其中一個寄存器既充當輸入又充當輸出。這意味著如果你的輸入和輸出需要不同,你需要2條指令來完成操作。z = x + y
會變成z = x; z += y
。在高級別上,這些行為是相同的,但在低級別上,有兩個步驟而不是一個步驟來實現它。
2011年,英特爾在基於桑迪橋的處理器上推出了AVX,將支持擴展到256位,從而進一步擴展了這一點。這些較新的寄存器被命名為ymm0
到ymm15
,只有直到ymm7
的寄存器才能訪問32位代碼。這也引入了一種稱為VEX
(矢量擴展)的新編碼,允許對3個寄存器進行編碼。這意味著您可以直接編碼z = x + y
,而不必將其分為兩個單獨的步驟。
AVX-512隨後由英特爾於2017年推出,採用基於Skylake-X的處理器。這將支持擴展到512位,並將寄存器命名為zmm0
到zmm15
。它還引入了16個新寄存器,恰當地命名為zmm16
到zmm31
,並且還有xmm16-xmm31
和ymm16-ymm31
變體。與前面的情況一樣,只有zmm7
以下的寄存器才能訪問32位代碼。它引入了8個新的寄存器,命名為k0
到k7
,旨在支持“掩碼”和另一種名為EVEX
(增強型矢量擴展)的新編碼,允許表達所有這些新信息。EVEX編碼還具有允許以更緊湊的方式表達更常見的信息和操作的其他特征。這可以幫助減少代碼大小,同時提高性能。
有哪些新的指示?
有很多新功能,太多了,無法在這篇博客文章中涵蓋所有內容。但一些最值得註意的新指令提供了以下內容:
- 支持對64位整數進行
Abs
、Max
、Min
和移位等操作-以前必須使用多條指令來模擬此功能 - 支持在無符號整數和浮點類型之間進行轉換
- 支持使用浮點邊緣情況
- 支持在一個或多個向量中完全重新排列元素
- 支持在單個指令中執行2個按位操作
64位整數支持是值得註意的,因為這意味著處理64位數據不需要使用較慢或替代的代碼序列來支持相同的功能。這使得編寫代碼並期望其行為相同變得更加容易,而不管您正在使用的底層數據類型如何。
浮點數到無符號整數轉換的支持也是出於類似的原因。從double
轉換到long
需要一條指令,但是從double
轉換到ulong
需要很多指令。使用AVX-512,這變成了一條指令,允許用戶在處理無符號數據時獲得預期的性能。這在各種圖像處理或機器學習場景中很常見。
對浮點數據的擴展支持是我最喜歡的AVX-512特性之一。一些示例包括提取無偏指數(Avx512F.GetExponent
)或歸一化尾數(Avx512F.GetMantissa
)、將浮點值舍入為特定小數位數(Avx512F.RoundScale
)、將值乘以2^x(Avx512F.Scale
,在C中稱為scalebn
),以正確處理Min
和Max
(MinMagnitude
)來執行MaxMagnitude
、+0
、-0
和Avx512DQ.Range
,甚至可以進行簡化,這在處理像Sin
或Cos
(Avx512DQ.Reduce
)這樣的三角函數的大值時是有用的。
然而,我個人最喜歡的指令之一是名為vfixupimm
(Avx512F.Fixup
)的指令。在高級別上,此指令允許您檢測許多輸入邊緣情況,並將輸出“修複”為常見輸出之一,並按元素執行此操作。這可以大大提高某些演算法的性能,並大大減少所需的處理量。它的工作方式是它需要4個輸入,即left
,right
,table
和control
。它首先對right
中的浮點值進行分類,並確定它是QNaN
(0)、SNaN
(1)、+/-0
(2)、+1
(3)、-Infinity
(4)、+Infinity
(5)、Negative
(6)還是Positive
(7)。然後,它使用它從4
讀取table
位(QNaN
是0
,讀取位0..3
;Negative
是6
讀取位24..27
)。table
中這4位的值決定了結果。可能的結果(每個元素)是:
位模式 | 定義 |
---|---|
0b0000 | 左[i] |
0b0001 | 右[i] |
0b0010 | QNaN(右[i]) |
0b0011 | QNaN |
0b0100 | -Infinity |
0b0101 | +Infinity |
0b0110 | IsNegative(right[i])?-Infinity:+Infinity |
0b0111 | -0.0 |
0b1000 | +0.0 |
0b1001 | -1.0 |
0b1010 | +1.0 |
0b1011 | +0.5 |
0b1100 | +90.0 |
0b1101 | Pi / 2 |
0b1110 | MaxValue |
0b1111 | MinValue |
在SSE中,有一些支持在向量中重新排列數據。例如,你有0, 1, 2, 3
,你想訂購3, 1, 2, 0
。隨著AVX的引入和擴展到256位,這種支持也得到了擴展。然而,由於指令的操作方式,你實際上會執行兩次相同的128位操作。這使得將現有演算法擴展到256位變得簡單,因為你實際上只是做了兩次同樣的事情。然而,當你實際上需要考慮整個向量時,它使使用其他演算法變得更加困難。有一些指令可以讓你在整個256位向量中重新排列數據,但它們通常在數據如何重新排列或它們支持的類型方面受到限制(位元組元素的完全洗牌是缺少支持的一個明顯例子)。AVX-512對於其擴展的512位支持有許多相同的考慮。但是,它還引入了新的指令來填充差距,現在可以讓您完全重新排列任何大小的元素的元素。
最後,我個人最喜歡的指令之一是名為vpternlog
(Avx512F.TernaryLogic
)的指令。此指令允許您採用任何2個按位操作並將它們聯合收割機組合,因此它們可以在單個指令中執行。例如,您可以執行(a & b) | c
。它的工作方式是它需要4個輸入,a
,b
,c
和control
。然後你有三個鍵要記住:A: 0xF0
,B: 0xCC
,C: 0xAA
。為了表示所需的操作,您只需通過對這些鍵執行該操作來構建control
。所以,如果你想簡單地返回a
,你可以使用0xF0
。如果你想做a & b
,你會使用(byte)(0xF0 & 0xCC)
。如果你想做(a & b) | c
,那麼它就是(byte)((0xF0 & 0xCC) | 0xAA
。總共有256種不同的操作,基本的構建塊是那些鍵和以下按位操作:
操作 | 定義 | |
---|---|---|
not | ~x | |
and | X & Y | |
nand | ~x & y | |
or | X | 和 |
nor | ~x | 和 |
xor | X ^ y | |
xnor | ~x ^ y |
然後還有一些特殊的操作,也支持上述基本操作,並且可以進一步擴展。
操作 | 定義 |
---|---|
假 | 位模式為0x00 |
真 | 0xFF的位模式 |
主要 | 如果兩個或多個輸入位為0,則返回0;如果兩個或多個輸入位為1,則返回1 |
次要 | 如果兩個或多個輸入位為1,則返回0;如果兩個或多個輸入位為0,則返回1 |
條件選擇 | 邏輯上是(x & y) | (~x & z) ,因為它是(x and y) or (x nand y) |
在.NET 8中,我們沒有完成對隱式識別和摺疊這些模式以發出vpternlog
的支持。我們希望它在.NET 9中首次亮相。
什麼是屏蔽支持?
在最簡單的級別上,編寫向量化代碼涉及使用SIMD在單個指令中對類型Count
的T
不同元素執行相同的基本操作。當需要對所有數據執行相同的操作時,這非常有效。然而,並非所有數據都是統一的,有時您需要以不同的方式處理特定的輸入。例如,您可能希望對正數和負數執行不同的操作。如果用戶傳入了NaN
,你可能需要返回一個不同的結果,等等。在編寫常規代碼時,你通常會用一個分支來處理這個問題,這工作得很好。但是,在編寫向量化代碼時,這樣的分支會破壞使用SIMD指令的能力,因為您必須獨立處理每個元素。.NET在不同的地方利用了這一點,包括新的TensorPrimitives
API,它允許我們處理不適合完整向量的尾隨數據。
典型的解決方案是編寫“無分支”代碼。最簡單的方法之一是計算兩個答案,然後使用按位運算來選擇正確的答案。你可以把它想象成一個三元條件cond ? result1 : result2
。為了在SIMD中支持這一點,存在一個名為ConditionalSelect
的API,它接受一個掩碼和兩個結果。掩碼也是一個向量,但其值通常為AllBitsSet
或Zero
。當你有了這個模式,那麼ConditionalSelect
的實現實際上就是(cond & result1) | (~cond & result2)
。這分解為從result1
中取出位,其中cond
中的對應位是1
,否則從result2
中取出對應位(當cond
中的位是0
時)。因此,如果你想將所有負值轉換為0
,那麼對於常規代碼,你會得到類似於(x < 0) ? 0 : x
的值,而對於矢量化代碼,你會得到類似於Vector128.ConditionalSelect(Vector128.LessThan(x, Vector128.Zero), Vector128.Zero, x)
的值。它有點冗長,但也可以提供顯著的性能改進。
當硬體第一次開始支持SIMD時,您必須通過執行3條指令來支持這種掩碼:and, nand, or
。隨著新硬體的出現,添加了更多優化版本,允許您在單個指令中執行此操作,例如x86/x64上的blendv
和Arm 64上的bsl
。AVX-512則進一步引入了專用硬體支持來表達掩碼併在寄存器中跟蹤它們(前面提到的k0-k7
)。然後,它提供了額外的支持,允許這種掩蔽作為幾乎任何其他操作的一部分來完成。因此,不必指定vcmpltps; vblendvps; vaddps
(比較,掩碼,然後添加),您可以直接將掩碼編碼為加法的一部分(從而發出vcmpltps; vaddps
)。這允許硬體在更少的空間中表示更多的操作,提高代碼密度,並更好地利用預期的行為。
值得註意的是,我們在這裡沒有直接公開與底層硬體的1對1概念。相反,JIT繼續獲取並返回用於比較結果的常規向量,並基於此進行相關的模式識別和掩蔽特征的後續機會光照。這允許暴露的API錶面顯著更小(減少超過3000個API),現有代碼在很大程度上“只是工作”並利用較新的硬體支持而無需顯式操作,並且希望支持AVX-512的用戶不必學習新概念或以新方式編寫代碼。
AVX-512在實踐中的使用示例如何?
AVX-512可用於加速所有與SSE或AVX相同的場景。識別.NET庫已經使用這種加速的一種簡單方法是搜索我們稱之為Vector512.IsHardwareAccelerated
的地方
我們加速了以下案例:
- System.Collections.BitArray – creation, bitwise and, bitwise or, bitwise xor, bitwise not
- System.Linq.Enumerable – Max and Min
- System.Buffers.Text.Base64 – Decoding, Encoding
- System.String – Equals, IgnoreCase
- System.Span – IndexOf, IndexOfAny, IndexOfAnyInRange, SequenceEqual, Reverse, Contains, etc
在.NET庫和一般的.NET生態系統中還有其他例子,太多了,無法列出和覆蓋。這些包括但不限於顏色轉換、圖像處理、機器學習、文本轉碼、JSON解析、軟體渲染、光線跟蹤、游戲加速等場景。
接下來呢?
我們計劃繼續改進.NET中的硬體內部支持,無論何時何地。請註意,以下項目是前瞻性的思考和推測。該列表是不完整的,我們不提供任何這些功能將土地或當他們將船舶,如果他們這樣做。
我們長期路線圖中的一些項目包括以下內容:
Arm64的SVE
和SVE 2x86/x64的AVX10
- 允許
Vector<T>
隱式擴展到512位 ISimdVector<TSelf, T>
介面,允許更好地重用SIMD邏輯- 一個分析器,幫助鼓勵用戶使用語義相同的跨平臺API(使用
x + y
而不是Sse.Add(x, y)
) - 一個分析器,用於識別可能具有更優替代方案的模式(執行
value + value
而不是value * 2
或Sse.UnpackHigh(value, value)
而不是Sse.Shuffle(value, value, 0b11_11_10_10)
- 在各種.NET API中額外顯式使用硬體內部函數
- 額外的跨平臺API,幫助抽象通用操作
- 獲取掩碼中第一個/最後一個匹配項的索引
- 獲取掩碼中的匹配數
- 確定是否存在任何匹配項
- 允許像
Shuffle
或ConditionalSelect
這樣的情況下的非確定性行為 - 這些API在當今的所有平臺上都有定義良好的行為,例如
Shuffle
將任何超出範圍的索引視為將目標元素歸零 - 新的API(如
ShuffleUnsafe
)將允許超出範圍索引的不同行為 - 對於這種情況,Arm64將具有相同的行為,而x64只有在設置了最高有效位時才具有相同的行為
- 其他模式識別,例如
- 嵌入式屏蔽(AVX 512,AVX 10,SVE/SVE 2)
- 組合位操作(AVX512上的
vpternlog
) - 有限的JIT時間常數摺疊機會