日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > C# >内容正文

C#

SIMD via C#

發布時間:2023/12/4 C# 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SIMD via C# 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

簡介 TL;DR

我們為C#(準確地說是.NET Core)引入了一套全新的機制,使得C# 以后可以像C/C++ 一樣直接使用intrinsic functions 來直接操作Intel CPU 的大多數SIMD 指令了(從SSE 到AVX2)。

(注意是以后!這個項目還沒有完成!)

Vectors in .NET

在最開始我想先說一說SIMD 編程在C#/.NET 中的現狀,以及為什么我們要引入這套全新的intrinsic。

微軟在之前的.NET Framework 和.NET Core 中引入了一個新的庫:?System.Numerics.Vectors?,其中包含幾個重要的值類型(Vector<T>,?Vector2,?Vector3, 等等)和操作它們的一些靜態方法。程序員可以用這個庫在.NET 環境中編寫SIMD 程序。以下我假定大家都大概知道SIMD 編程的概念,來具體講講這個庫 的設計與實現。


System.Numerics.Vectors?庫中的這些靜態方法的實際功能不能用C# 等.NET managed language 直接寫出來(雖然它們都有一份C# 的實現),而是由編譯器特殊對待從而生成特殊代碼(SSE, AVX, AVX2, 等指令集的指令),我們稱這些方法(函數)為intrinsic。這些intrinsic 大部分都操作在上面說到的這些值類型上(Vector<T>,?Vector2,?Vector3, 等等),這些類型的實例也會被編譯器特殊對待。其中最主要的是Vector<T>,這個類型的設計不同于傳統C/C++ intrinsic?中的vector 類型:

  • 泛型:.NET 中的這個vector類型采用了泛型設計,泛型Vector<T>的類型參數只接受numeric types,即C# 的基礎數字類型(byte, sbyte, short, ushort, long, ulong, float, double)。如果試圖創建一個Vector<UserDefinedStruct>的實例,運行時會拋出異常(一大波來自Haskell 的鄙視正在路上……)。

  • 長度可變(length-agnostic):大家都知道隨著微處理器歷史的發展SIMD 計算單元和寄存器的長度也在不斷地進化,Intel 從最初MMX 的64-bit 寄存器到后來SSE 系列128-bit 寄存器,再到AVX 擴展為256-bit,最新的AVX-512 已經有了512-bit 的SIMD 寄存器。C/C++ intrinsic 使用不同長度的vector 類型來抽象這些SIMD 寄存器,比如__m128,?__m256d。然而借助.NET 的JIT 編譯,Vector<T>?的長度可以隨著程序運行的硬件環境的不同而改變,例如一個使用了System.Numerics.Vectors?來加速的程序在Sandy Bridge 等稍微老一點的CPU 上看到Vector<byte>?的長度為16,而同一個程序運行在Haswell 以上的新CPU 上看到的Vector<byte>?的長度為32,但程序行為保持不變,并且開發者也不需要重新編譯他們的源碼就可以得到更多的提速。這個設計乍一看起來非常酷,但是也為這個庫的命運埋下了巨大的隱患。

  • System.Numerics.Vectors 的缺陷

    System.Numerics.Vectors?庫的設計初衷是要做一個跨平臺的通用的SIMD 編程庫。可以看出它的最終目標是要在統一的API 下支持不同的硬件指令集(SSE, AVX, NEON, etc.),雖然現在只做了x86/x64 平臺的支持,但一些設計缺陷已經暴露出來了。

  • 當『通用』成為設計目的時,『可用』成了重中之重。眾所周知,SIMD 編程或者叫向量化編程相對來說是比較困難的,當一個程序想使用SIMD 來加速時開發者關注的第一點肯定是『性能』。然而這個『通用』和『可用』的設計目的并不能保證『性能』。舉個最簡單的例子,不同硬件提供的指令集一般在功能上是不會完全重合的,當一個指令在Intel CPU 上存在而在ARM CPU 上不存在的時候,通用SIMD 庫就要想辦法繞著彎來在不直接提供支持的硬件上實現這個API。然而這個『彎兒』一旦開始繞了,性能提升就不能保證了(在一些極端情況下不繞彎都不能保證)。試想一個程序員發現一個函數foo在他的程序中調用非常頻繁,并且可以被向量化,于是欣喜地使用Vectors<T>?重寫了。然后他發現整個程序在他裝備了Skylake CPU的 Macbook Pro 上性能提升了50%,但在發布新版本幾天后所有ARM 用戶全來罵娘了(這只是個例子,性能退化在所有硬件平臺之間都有可能出現,不是針對某些硬件架構)。以下列出的其他缺陷都或多或少來自這一條設計原則。

  • 可變長度的Vector<T>?上無法抽象某些硬件指令的語義。比如很重要的shuffle?這族指令就沒法抽象到變長Vector<T>, Github 已經有人多次要求提供這些API,但最終都沒有很好的解決方案。再比如,對于AVX/AVX2 來說,很多時候我們需要同時操作YMM 和XMM 寄存器,但這在Vector<T>?的設計中不被允許。

  • System.Numerics.Vectors?中的類型和函數在JIT 編譯器不支持生成SIMD 指令的環境下會退回到C# 的軟件實現。這點對性能是很致命的,尤其是有些時候這種『不支持生成SIMD 指令的環境』是不可避免的,比如反射調用。

  • 還有很多細節性的缺點我就不一一列舉了,比較這篇文章重點不在System.Numerics.Vectors。有興趣的同學可以去CoreCLR 和CoreFX 的GitHub repo 翻一翻相關的issue。

  • Intel Hardware Intrinsic

    說了那么多終于進入正題了。為了探索一個新的SIMD 方案,我代表牙膏廠為.NET 提供了API Proposal: Add Intel hardware intrinsic functions and namespace #22940。總體的設計都在這份API proposal 里了,我簡單總結一下:

  • 加入兩個新的namespace:System.Runtime.Intrinsics和System.Runtime.Intrinsics.X86。其中System.Runtime.Intrinsics只包含跨平臺類型,目前有兩個新的值類型Vector128<T>和Vector256<T>?來抽象SIMD 寄存器。每個硬件平臺提供各自平臺相關的類型和方法用來操作Vector128<T>和Vector256<T>,比如x86 平臺的所有intrinsic 都在System.Runtime.Intrinsics.X86namespace 下,它提供了在managed language 中直接訪問以下指令集的能力:SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AVX2, FMA, AES, BMI1, BMI2, LZCNT, POPCNT, PCLMULQDQ。

  • 每一個指令集封裝成一個static class(例如Sse,Aes,?Avx2, 等.),每個class 都有一個IsSupported?方法用來檢測當前硬件,從而為不同的硬件提供不同的優化方案。


    if (Avx2.IsSupported)

    {

    // 為AVX2 CPU 優化的算法 ?

    }

    else if (Sse41.IsSupported)

    {

    // 為SSE4.1 CPU 優化的算法

    }

    else if (Neon.IsSupported)

    {

    // 為ARM NEON CPU 優化的算法

    }

    else

    {

    // software-fallback

    }

  • 要求一個新的C# 語言特性,const?參數。因為Intel hardware intrinsic 直接通過C# 代碼來控制最終的代碼生成,而一些SIMD 指令明確要求立即數操作數。比如shufpd?對應的C# intrinsic 是

    1

    public static Vector128<double> Shuffle(Vector128<double> left, Vector128<double> right, byte control);

    參數control?對應shufpd?的imm8?操作數,它必須是編譯時確定的,如果用戶傳入一個『變量』可能導致程序無法編譯。所以我們向C# 語言特性的開發組請求了一個新的語言特性:將const?關鍵字用于方法的形式參數。最終Shuffle?的方法簽名為:

    1

    public static Vector128<double> Shuffle(Vector128<double> left, Vector128<double> right, const byte control);

    這樣C# 編譯器(Roslyn)就只允許byte 字面量值流入control?參數。

  • Intel hardware intrinsic 在.NET Core 中所有環境下都會被編譯為直接對應的硬指令,比如JIT編譯、AOT編譯(Crossgen)、MSCorlib 內部調用(比如用來優化String)、Debugging 調用、反射調用等等。而相對的System.Numerics.Vectors?只能在第三方JIT 編譯的普通調用中才會生成SIMD 指令。

  • 具體的API 請移步?https://github.com/dotnet/coreclr/tree/master/src/mscorlib/src/System/Runtime/Intrinsics

    .NET Managed Intrinsic 與C/C++ Native Intrinsic

    如果有SIMD 編程經驗的讀者看到這里一定會覺得我們做的這套新的intrinsic 和Intel C/C++ intrinsic?很相似。對,這套新的hardware intrinsic 是比原先System.Numerics.Vectors?更偏底層的一套intrinsic 機制,我們希望可以通過managed language (目前只有C#)來直接對應編譯器的代碼生成。然而,他還是有一些區別于C/C++ intrinsic 的地方。

    • .NET Core 的JIT 編譯為hardware intrinsic 的使用和實現提供了更大的便利。因為C/C++ 都是AOT 編譯的,所以一般在編譯SIMD 程序時開發者需要選用不同的編譯器選項來編譯多分二進制分發文件來保證各個在硬件平臺都達到最優性能。然而JIT 編譯就不會有這份顧慮,JIT 編譯器會在啟動前自動探知當前的硬件參數,來自動生成最有性能的代碼。也許有人會說.NET Core 也有AOT 編譯啊!可是.NET Core 的AOT 編譯器(Crossgen)依然可以從JIT 編譯器中獲利,比如我們可以AOT 編譯一個程序的大部分,但留下硬件相關的代碼,待到運行時再JIT 編譯這些代碼(intrinsic)然后動態插入到原先AOT 編譯好的程序中。

    • 當然.NET Core 的hardware intrinsic 相比C/C++ 也有劣勢。一般SIMD 計算對內存數據都有對齊要求,CoreCLR 卻沒有提供完整的對齊內存的接口給用戶。但是這一點可以通過unsafe?代碼(目前所有和內存交互的intrinsic 都是操作指針)和后續的值類型對齊機制來逐漸解決。還有一點就是managed language 對底層硬件的控制不如native language 靈活。舉個例子,在C/C++ 中我們可以這么寫代碼來節省Load和Store:

      1

      2

      3

      // __m256 a, float* b

      __m256* v = (__m256*)b;

      __m256 result = _mm256_add_ps(a, *v); // vaddps ymm0, ymm0, ymmword ptr [rdi]

      上面這段兩行代碼可以只生成一條memory-flavor 的指令,但在C# 中我們不能持有一個泛型struct 的指針,所以我們必須寫成:

      1

      2

      3

      // Vector<float> a, float* b

      Vector<float> v = Avx.Load(b);

      Vector<float> result = Avx.Add(a, v);

      直覺上這個程序是兩條指令,但可以被編譯器優化折疊為和上面C/C++ 程序一樣的編譯結果。

    小福利

    能看到這兒還沒有關掉文章的一定是對SIMD 計算和編譯器實現都很有興趣的同學,那我順便放點編譯器實現的細節在這作為堅持到最后的獎勵。
    如果你點進了我上面給出的API 連接就會發現,所有的hardware intrinsic 有一個C# 的實現:



    /// <summary>

    /// __m256 _mm256_add_ps (__m256 a, __m256 b)

    /// </summary>

    public static Vector256<float> Add(Vector256<float> left, Vector256<float> right) => Add(left, right);

    /// <summary>

    /// __m256d _mm256_add_pd (__m256d a, __m256d b)

    /// </summary>

    public static Vector256<double> Add(Vector256<double> left, Vector256<double> right) => Add(left, right);



    每個intrinsic 在C# API 中都是一個直接遞歸方法。這是為什么呢?
    原因是我們需要intrinsic 在某些環境下既是intrinsic 又是function(method)。
    首先我們可以將在intrinsic 理解為必須內聯的函數(方法),對它的調用會被直接替換為一條或多條匯編指令,而不遵循普通函數/方法的調用約定(calling convention)。然而這一定義在某些情況下是無法工作的,比如deugger 和反射。例如.NET 在反射機制中提供了『方法調用』卻沒有提供『intrinsic調用』,那么typeof(Avx).GetMethod("Add").Invoke(null, args)?是無法工作的。但是我們可以這么做:

  • 在某些環境中編譯器看到用戶調用Avx.Add(a, b)?時不對其進行特殊處理,而只當成是普通的函數調用。

  • 編譯器如果看到Avx.Add(a, b)?是被自身調用的(遞歸),則強制將其編譯為相應的匯編指令。

  • 這樣,我們就完美解決了intrinsic 既是intrinsic (遞歸調用)又是function(用戶調用)的問題。

    最后

    如果大家對這項功能感興趣,我會在這里持續更新項目進展,也請大家耐心等候!

    原文地址:http://fiigii.com/2017/09/29/SIMD-via-C/


    .NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注

    總結

    以上是生活随笔為你收集整理的SIMD via C#的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。