C# 使用SIMD向量類型加速浮點數組求和運算(1):使用Vector4、Vector<T>

来源:https://www.cnblogs.com/zyl910/archive/2022/08/20/dotnet_simd_BenchmarkVector1.html
-Advertisement-
Play Games

由於net core 中預設沒有System.Drawing,可以通過nuget下載一個來代替System.Drawing.Common 直接壓縮圖片 /// <summary> /// 圖片壓縮 /// </summary> /// <param name="sFile">原圖片位置</param ...


作者:

目錄
目錄

    一、緣由

    從.NET Core 1.0開始,.NET里增加了2種向量類型——

    1. 大小固定的向量(Vectors with a fixed size)。例如 結構體(struct) Vector2、Vector3、Vector4。
    2. 大小與硬體相關的向量(Vectors with a hardware dependent size)。例如 只讀結構體(readonly struct) Vector<T>,及輔助的靜態類 Vector。

    到了 .NET Core 3.0,增加了內在函數(Intrinsics Functions)的支持,並增加了第3類向量類型——
    3. 總位寬固定的向量(Vector of fixed total bit width)。例如 只讀結構體 Vector64<T>Vector128<T>Vector256<T>,及輔助的靜態類 Vector64、Vector128、Vector256。

    這3類向量類型,均能利用CPU硬體的SIMD(float Instruction Multiple Data,單指令多數據流)功能,來加速多媒體數據的處理。但是它們名稱很接近,對於初學者來說容易混淆,而且應用場景稍有區別,本文致力於解決這些問題。
    本章重點解說前2種向量類型(Vector4、Vector<T>),第3種向量類型將由第2章來解說。

    本章回答了這些問題——

    • 怎樣使用這2種向量類型?以做浮點數組求和運算為例。
    • 這2種向量類型的使用場景,及最佳實踐是怎樣的?
    • 我們的普通PC機的浮點運算性能,能達到每秒多少 MFLOPS(百萬次浮點運算)?
    • 官方文檔上,.NET Framework 4.6 才支持大小固定的向量(如Vector4),且Vector<T>未提到.NET Framework的支持版本。難道 .NET Framework用不了Vector<T> 嗎? .NET Framework 4.5等版本時是否能使用它們?
    • 官方文檔上,僅 .NET Standard 2.1 才支持這2種向量類型。而.NET Standard 2.0應用最廣泛,該怎麼在.NET Standard 2.0上使用它們?
    • 若在類庫里使用了向量類型,那麼 .NET Core或.NET Framework引用類庫時,向量類型是否仍會有硬體加速?
    • 當沒有硬體加速(Vector.IsHardwareAccelerated==false)時,使用向量類型會有什麼問題嗎?
    • 有人說“僅64位、Release模式編譯時”向量類型才會有硬體加速,而其他情況沒有硬體加速,是這樣的嗎?

    二、使用向量類型

    用高級語言處理數據時,一般是SISD(float instruction float data,單指令流單數據流)模型的,即一個語句只能處理一條數據。
    而對於多媒體數據處理,任務的特點是運算相對簡單,但是數據量很大,導致SISD模型的效率很低。
    若使用SIMD模型的話,一次能處理多條數據,從而能成倍的提高性能。
    .NET Core引入了向量數據類型,從而使C#(等.NET中語言)能使用SIMD加速數據的處理。

    並不是所有的數據處理工作都適合SIMD處理。一般來說,需滿足以下條件,才能充分利用SIMD加速——

    1. 數據量大(至少超過1000)且連續的存放在記憶體里。若數據規模小,SIMD無法體現性能優勢;若數據不是連續存放,那麼會遇到記憶體傳輸率的瓶頸,無法發揮SIMD的實力。
    2. 每個元素的處理運算需比較簡單。因為SIMD的函數,只能處理簡單的數學函數。
    3. 每個元素的處理步驟,大致相同。當每個元素的處理運算相同時,便能一個命令同時處理多條數據。當存在差異時,便需要利用掩碼與位運算,分別進行處理。當差異很大時,甚至向量代碼比起標量代碼,沒有優勢。
    4. 元素的數據類型,必須是.NET的基元類型,如 float、double、int 等。這是.NET向量類型的限制。

    對於以下情況,SIMD代碼的性能會急劇下降,應儘量避免——

    • 分支跳轉。分支跳轉會導致流水線失效,導致SIMD性能會急劇下降。故在處理步驟稍有差異時,應儘量利用掩碼與位運算分別進行處理,而不是分支。
    • 元素間的數據相關性高。當沒有相關性時,才適合SIMD併發處理。若相關性高,那麼等待相關處理處理會浪費不少時間,無法發揮SIMD併發處理的優勢。很多時候可以使用MapReduce策略來處理數據,先在Map階段處理併發處理“無相關性的步驟”,最後在Reduce階段專門處理“有相關性的步驟”。

    基於以上原因,發現最適合演示SIMD運算優勢的,是做“浮點數組求和運算”。先在Map階段處理併發的進行分組求和,最後在Reduce階段將各組結果加起來。

    2.1 基本演算法

    為了對比測試,先用傳統的辦法來編寫一個“單精度浮點數組求和”的函數。
    其實演算法很簡單,寫個迴圈進行累加求和就行。代碼如下。

    private static float SumBase(float[] src, int count) {
        float rt = 0; // Result.
        for(int i=0; i< count; ++i) {
            rt += src[i];
        }
        return rt;
    }
    

    由於.NET向量類型的初始化會有一些開銷,為了避免這些開銷影響主迴圈的性能測試結果,於是需要將它們移到迴圈外。為了測試方便,求和函數可增加一個loops參數,它是測試次數,作為外迴圈。loops為1時,就是標準的變數求和;為其他值時,是多輪變數求和的累計值。由於浮點精度有限的問題,累計值可能與乘法結果不同。
    為了能統一進行測試,於是基本演算法也增加了 loops 參數。

    private static float SumBase(float[] src, int count, int loops) {
        float rt = 0; // Result.
        for (int j=0; j< loops; ++j) {
            for(int i=0; i< count; ++i) {
                rt += src[i];
            }
        }
        return rt;
    }
    

    2.2 使用大小固定的向量(如 Vector4)

    2.2.1 介紹

    大小固定的向量類型,是以下3種結構體——

    • Vector2:表示一個具有兩個單精度浮點值的向量。
    • Vector3:表示一個具有三個單精度浮點值的向量。
    • Vector4:表示一個具有四個單精度浮點值的向量。

    它們實際上是對數學(線性代數分支)里“向量”(Vector)的封裝。命名規則為“'Vector' + [維數]”,例如 Vector2是數學里的“二維向量”、Vector3是數學里的“三維向量”、Vector4是數學里的“四維向量”。
    於是這些類型,除了提供了常見的四則運算函數外,還提供了 向量長度(Length)、向量距離(Distance)、點積(Dot)、叉積(Cross) 等線性代數領域的函數。
    它其中元素的數據類型,被限製為 float(32位單精度浮點值)。能用於常見單精度浮點運算場合。

    使用這些向量類型時,JIT會儘可能的利用硬體加速,但是沒有提供“是否有硬體加速”的標誌。
    這是因為不同的運算函數,在不同的CPU指令集里,有些能硬體加速,而另一些不能,很難通過簡單的標誌來區分。於是JIT僅是保證能儘可能的利用硬體加速,讓使用者不用關心這些硬體細節。
    一般來說,直接用這些類型的封裝函數(如點積、叉積 運算等),比手工按數學定義編寫的運算函數,效率更高。因為即使沒有硬體加速時,這些封裝好的函數是高水平的程式員編寫的成熟代碼。

    Vector2、Vector3 比起 Vector4,元素個數要少一些,從數學定義上來看,理論運算量要少一些。
    但是硬體的SIMD加速,大多是按“4元素並行處理”來設計。故很多時候,“Vector2、Vector3”運算性能與“Vector4”差不多。甚至在一些特別場合,比“Vector4”性能還低,因為對於硬體來說,可能會有多餘的 忽略多餘元素處理、數據轉換 工作。

    於是建議這樣使用——

    • 若是開發數學上的向量運算相關的功能,可根據業務上對向量運算的要求,使用維度匹配的向量類。例如 2維向量處理時用Vector2、3維向量處理時用Vector3、3維齊次向量處理時用Vector4。
    • 若是想對數據進行SIMD優化,那麼應該用 Vector4。

    2.2.2 用Vector4編寫浮點數組求和函數

    現在,我們使用Vector4,來編寫浮點數組求和函數。
    思路:Vector4內有4個元素,於是可以分為4個組分別進行求和(即Map階段),最後再將4個組的結果加起來(即Reduce階段)。

    我們先可建立SumVector4函數。根據之前所說(為了.NET向量類型的初始化),該函數還增加了1個loops參數。

    /// <summary>
    /// Sum - Vector4.
    /// </summary>
    /// <param name="src">Soure array.</param>
    /// <param name="count">Soure array count.</param>
    /// <param name="loops">Benchmark loops.</param>
    /// <returns>Return the sum value.</returns>
    private static float SumVector4(float[] src, int count, int loops) {
        float rt = 0; // Result.
        // TODO
        return rt;
    }
    

    註意,數組長度可能不是4的整數倍。此時僅能對前面的、4的整數倍的數據用Vector4進行運算,而對於末尾剩餘的元素,只能用傳統辦法來處理。
    此時可利用“塊”(Block)的概念來簡化思路:每次內迴圈處理1個塊,先對能湊齊整塊的數據用Vector4進行迴圈處理(cntBlock),最後再對末尾剩餘的元素(cntRem)按傳統方式來處理。
    Vector4有4個元素,於是塊寬度(nBlockWidth)為4。代碼摘錄如下。

        const int VectorWidth = 4;
        int nBlockWidth = VectorWidth; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
    

    C#是強類型的,會嚴格檢查類型是否匹配,為了能使用Vector4,需要先將浮點數組轉換為Vector4。這一步驟,一般叫做“Load”(載入)。
    再加上相關變數的定義及初始化,“Load”部分的代碼摘錄如下。

        Vector4 vrt = Vector4.Zero; // Vector result.
        int p; // Index for src data.
        int i;
        // Load.
        Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
        p = 0;
        for (i = 0; i < vsrc.Length; ++i) {
            vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
            p += VectorWidth;
        }
    

    由於 Vector4 的構造函數不支持從數組裡載入數據,僅支持“傳遞4個浮點變數”。於是上面的迴圈里,使用“傳遞4個浮點變數”的方式創建Vector4,然後放到vsrc數組中。vsrc數組中的每一項,就是一個塊(Block)。

    現在已經準備好了,可以用迴圈進行數據運算(Map階段:分為4個組分別進行求和)了。代碼摘錄如下。

        // Body.
        for (int j = 0; j < loops; ++j) {
            // Vector processs.
            for (i = 0; i < cntBlock; ++i) {
                // Equivalent to scalar model: rt += src[i];
                vrt += vsrc[i]; // Add.
            }
            // Remainder processs.
            p = cntBlock * nBlockWidth;
            for (i = 0; i < cntRem; ++i) {
                rt += src[p + i];
            }
        }
    

    外迴圈loops的作用僅是為了方便測試,關鍵代碼在2個內迴圈里:

    1. Vector processs(向量處理):以塊為單位進行迴圈處理,利用 Vector4 有4個元素特點,進行4路併發加法,將 vsrc[i] 的值,加到 vrt 里。vrt是Vector4類型的變數,定義時已初始化為0。
    2. Remainder processs(剩餘數據處理):先計算一下剩餘數據的起始索引(p = cntBlock * nBlockWidth),然後使用傳統迴圈寫法,將剩餘數據累積到 rt 里。

    由於Vector4重載了“+”運演算法,所以可以很簡單的使用“+=”運算符來做“相加並賦值”操作。代碼寫法,與傳統的標量代碼很相似,代碼可讀性高。

    rt += src[i]; // 標量代碼.
    vrt += vsrc[i]; // 向量代碼.
    

    最後我們需要將各組的結果加在一起(Reduce階段)。代碼摘錄如下。

        // Reduce.
        rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
        return rt;
    

    因 Vector4 暴露了 X、Y、Z、W 這4個成員,於是可以很方便的用“+”運算符,將結果加在一起。

    該函數的完整代碼如下。

    private static float SumVector4(float[] src, int count, int loops) {
        float rt = 0; // Result.
        const int VectorWidth = 4;
        int nBlockWidth = VectorWidth; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        Vector4 vrt = Vector4.Zero; // Vector result.
        int p; // Index for src data.
        int i;
        // Load.
        Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
        p = 0;
        for (i = 0; i < vsrc.Length; ++i) {
            vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
            p += VectorWidth;
        }
        // Body.
        for (int j = 0; j < loops; ++j) {
            // Vector processs.
            for (i = 0; i < cntBlock; ++i) {
                // Equivalent to scalar model: rt += src[i];
                vrt += vsrc[i]; // Add.
            }
            // Remainder processs.
            p = cntBlock * nBlockWidth;
            for (i = 0; i < cntRem; ++i) {
                rt += src[p + i];
            }
        }
        // Reduce.
        rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
        return rt;
    }
    

    2.3 使用大小與硬體相關的向量(如 Vector<T>

    2.3.1 介紹

    Vector4的痛點是——元素類型固定為float,且僅有4個元素。導致它的使用範圍有限。
    Vector<T> 解決了這2大痛點——

    1. 它具有泛型參數T,可以支持各種數值型的基元類型,如 float、double、int 等。
    2. 它的元素個數不止4個,而是由硬體決定的。若硬體支持向量位寬越寬,那麼Vector<T>的元素個數便越大。使用Vector<T>在各種向量位寬的硬體上運行時,會以最大向量位寬來運行,而僅需只編寫一套代碼。

    以下是官方文檔對 Vector<T> 的介紹。

    `Vector<T>` 是一個不可變結構,表示指定數值類型的單個向量。 實例計數是固定的 `Vector<T>` ,但其上限取決於 CPU 寄存器。 它旨在用作向量大型演算法的構建基塊,因此不能直接用作任意長度向量或張量。
    該 `Vector<T>` 結構為硬體加速提供支持。
    本文中的術語 基元數值數據類型 是指 CPU 直接支持的數值數據類型,並具有可以操作這些數據類型的說明。 下表顯示了哪些基元數值數據類型和操作組合使用內部指令來加快執行速度:
    
    基元類型 + - * /
    sbyte
    byte
    short
    ushort
    int
    uint
    long
    ulong
    float
    double
    2.2.1.1 使用經驗

    有一個跟 Vector<T> 配合使用的靜態類 Vector。它有2大作用——

    1. 提供了 IsHardwareAccelerated 屬性,用於檢查 Vector<T> 是否有硬體加速。應用程式應該檢查該屬性,僅在該屬性為true,才使用 Vector<T>
    2. 提供了大量的數學函數,能便於 SIMD數據處理。Vector<T> 只是重載了運算符,對於運算符無法辦到的一些數學運算,可以去靜態類 Vector 里找。

    Vector<T> 具有這些屬性:

    • Count:【靜態】返回存儲在向量中的元素數量。
    • Item[int]:獲取指定索引處的元素。
    • One:【靜態】返回一個包含所有 1 的向量。
    • Zero:【靜態】返回一個包含所有 0 的向量。

    因為 Vector<T> 長度是與硬體有關的,所以每次在使用 Vector<T> 時,別忘了需要先從 Count 屬性里的到元素數量。

    一般來說——

    • 若CPU是 x86體系的,且支持 AVX2指令集 時,那麼 Vector<T> 長度為256位,即32位元組。此時能並行的處理 32個byte,或 16個short、8個int、4個long、8個float、4個double。
    • 若CPU是 x86體系的,不支持AVX2指令集,但支持 SSE2指令集 時,那麼 Vector<T> 長度為128位,即16位元組。此時能並行的處理 16個byte,或 8個short、4個int、2個long、4個float、2個double。
    • 若CPU不支持向量硬體加速時,那麼 Vector<T> 長度仍為128位,即16位元組。Vector.IsHardwareAccelerated為false,不建議使用。長度仍為128位,這可能是為了方便代碼相容性。

    這些情況的IsHardwareAccelerated、Count屬性,一般為這些值——

    // If the CPU is x86 and supports the AVX2 instruction set.
    Vector.IsHardwareAccelerated = true
    Vector<sbyte>.Count = 32
    Vector<byte>.Count = 32
    Vector<short>.Count = 16
    Vector<ushort>.Count = 16
    Vector<int>.Count = 8
    Vector<uint>.Count = 8
    Vector<long>.Count = 4
    Vector<ulong>.Count = 4
    Vector<float>.Count = 8
    Vector<double>.Count = 4
    
    // If the CPU is x86, the AVX2 instruction set is not supported, but the SSE2 instruction set is supported.
    Vector.IsHardwareAccelerated = true
    Vector<sbyte>.Count = 16
    Vector<byte>.Count = 16
    Vector<short>.Count = 8
    Vector<ushort>.Count = 8
    Vector<int>.Count = 4
    Vector<uint>.Count = 4
    Vector<long>.Count = 2
    Vector<ulong>.Count = 2
    Vector<float>.Count = 4
    Vector<double>.Count = 2
    
    // If the CPU does not support vector hardware acceleration.
    Vector.IsHardwareAccelerated = false
    Vector<sbyte>.Count = 16
    Vector<byte>.Count = 16
    Vector<short>.Count = 8
    Vector<ushort>.Count = 8
    Vector<int>.Count = 4
    Vector<uint>.Count = 4
    Vector<long>.Count = 2
    Vector<ulong>.Count = 2
    Vector<float>.Count = 4
    Vector<double>.Count = 2
    

    2.3.2 用 Vector<T> 編寫浮點數組求和函數

    現在,我們使用 Vector<T>,來編寫浮點數組求和函數。
    思路:先使用Count屬性獲得元素個數,然後按Count分組分別進行求和(即Map階段),最後再將這些組的結果加起來(即Reduce階段)。

    根據上面的經驗,我們可編寫好 SumVectorT 函數。

    private static float SumVectorT(float[] src, int count, int loops) {
        float rt = 0; // Result.
        int VectorWidth = Vector<float>.Count; // Block width.
        int nBlockWidth = VectorWidth; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        Vector<float> vrt = Vector<float>.Zero; // Vector result.
        int p; // Index for src data.
        int i;
        // Load.
        Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src.
        p = 0;
        for (i = 0; i < vsrc.Length; ++i) {
            vsrc[i] = new Vector<float>(src, p);
            p += VectorWidth;
        }
        // Body.
        for (int j = 0; j < loops; ++j) {
            // Vector processs.
            for (i = 0; i < cntBlock; ++i) {
                vrt += vsrc[i]; // Add.
            }
            // Remainder processs.
            p = cntBlock * nBlockWidth;
            for (i = 0; i < cntRem; ++i) {
                rt += src[p + i];
            }
        }
        // Reduce.
        for (i = 0; i < VectorWidth; ++i) {
            rt += vrt[i];
        }
        return rt;
    }
    

    對比 SumVector4,除了將 Vector4 類型換為 Vector<T>,還有這些變化——

    • VectorWidth不再是一個固定常數,而是通過 Vector<float>.Count 屬性來得到。
    • Vector<T> 的構造函數支持數組參數。於是可以用 new Vector<float>(src, p),代替繁瑣的 new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3])
    • Vector<T>支持索引器(文檔里的Item屬性),可以使用索引器運算符 [],簡潔的獲取它的元素。於是在Reduce階段,可以寫個迴圈對結果進行累加。

    三、搭建測試程式

    對於這2類向量類型,計劃在以下平臺進行測試——

    • .NET Core
    • .NET Framework
    • .NET Standard

    開發環境選擇VS2017。解決方案名的名稱是“BenchmarkVector”。
    因需要測試這麼多平臺,為了避免代碼重覆問題,故將主測試代碼放到共用項目(Shared Project)里。隨後各個平臺的測試程式,可以引用該共用項目。

    3.1 主測試代碼(BenchmarkVectorDemo)

    共用項目的名稱是“BenchmarkVector”。其中的BenchmarkVectorDemo類,是主測試代碼。

    3.1.1 測試方法(Benchmark)

    Benchmark是測試方法,代碼如下。

    /// <summary>
    /// Do Benchmark.
    /// </summary>
    /// <param name="tw">Output <see cref="TextWriter"/>.</param>
    /// <param name="indent">The indent.</param>
    public static void Benchmark(TextWriter tw, string indent) {
        if (null == tw) return;
        if (null == indent) indent = "";
        //string indentNext = indent + "\t";
        // init.
        int tickBegin, msUsed;
        double mFlops; // MFLOPS/s .
        double scale;
        float rt;
        const int count = 1024*4;
        const int loops = 1000 * 1000;
        //const int loops = 1;
        const double countMFlops = count * (double)loops / (1000.0 * 1000);
        float[] src = new float[count];
        for(int i=0; i< count; ++i) {
            src[i] = i;
        }
        tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops));
        // SumBase.
        tickBegin = Environment.TickCount;
        rt = SumBase(src, count, loops);
        msUsed = Environment.TickCount - tickBegin;
        mFlops = countMFlops * 1000 / msUsed;
        tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops));
        double mFlopsBase = mFlops;
        // SumVector4.
        tickBegin = Environment.TickCount;
        rt = SumVector4(src, count, loops);
        msUsed = Environment.TickCount - tickBegin;
        mFlops = countMFlops * 1000 / msUsed;
        scale = mFlops / mFlopsBase;
        tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
        // SumVectorT.
        tickBegin = Environment.TickCount;
        rt = SumVectorT(src, count, loops);
        msUsed = Environment.TickCount - tickBegin;
        mFlops = countMFlops * 1000 / msUsed;
        scale = mFlops / mFlopsBase;
        tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
    }
    

    變數說明——

    • count:浮點數組的長度。
    • loops:測試所用的外迴圈次數。
    • countMFlops:每次測試運算量是多少 MFLOPS(百萬次浮點運算)。
    • src:測試所用的浮點數組。
    • tickBegin:記錄測試開始的時刻。測試計時用的是 Environment.TickCount,它以毫秒為單位.
    • msUsed:測試所用的毫秒數。
    • mFlops:該函數的浮點性能。單位是 MFLOPS/s(百萬次浮點運算/秒)。
    • mFlopsBase:基本演算法的浮點性能。單位是 MFLOPS/s(百萬次浮點運算/秒)。
    • scale:性能提高倍數。既 當前演算法的性能,是基本演算法的多少倍。

    註:只有一級緩存是在CPU中的,一級緩存的讀取需要1-4個時鐘周期;二級緩存的讀取需要10個左右的時鐘周期;而三級緩存需要30-40個時鐘周期,但是容量一次增大。
    SIMD的數據規模大,一級緩存放不下。為了避免緩存速度干擾運算速度評測,故一般建議測試數據不要超過二級緩存的大小。
    於是本範例的數據長度為 4K(1024*4),這是現代CPU的二級緩存大多能接受的長度。

    例如在 .NET Core 2.0、lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10 平臺運行時,該測試函數的測試結果為:

    Benchmark:      count=4096, loops=1000000, countMFlops=4096
    SumBase:        6.871948E+10    # msUsed=4937, MFLOPS/s=829.653635811221
    SumVector4:     2.748779E+11    # msUsed=1234, MFLOPS/s=3319.2868719611, scale=4.00081037277147
    SumVectorT:     5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.8992
    

    輸出信息說明——

    • SumBase:浮點性能為 MFLOPS/s=829.653635811221,即約 0.829 GFLOPS/s。
    • SumVector4:浮點性能為 MFLOPS/s=3319.2868719611,即約 3.319 GFLOPS/s。性能是基礎演算法的 4.00081037277147 倍。
    • SumVectorT:浮點性能為 MFLOPS/s=6553.6,即約 6.553 GFLOPS/s。性能是基礎演算法的 7.8992 倍。

    性能提高倍數(scale),與理論值相符。因為SumVector4能同時處理4個浮點數,支持AVX2指令集時的SumVectorT能同時處理8個浮點數。
    i5-8250U是2017年Intel發佈的晶元,對於現在來說是老掉牙的配置了。C#代碼不使用硬體加速時,是 0.829 GFLOPS/s 的浮點性能;使用 Vector<T> 並有硬體加速時,能達到 6.553 GFLOPS/s 的浮點性能,這樣的指標已經很不錯了。
    而且我們的測試,只是對單核的測試,多核並行處理的浮點性能會更高。編寫多線程程式便利用CPU多核,有興趣的讀者可以自己試試。

    註意上面的測試結果中,各函數返回的累加結果是不同的。這是主要是因為是分組統計,迴圈次數(loops)比較多,導致超過單精度浮點數的精度範圍。
    若臨時將loops改回1,會發現各函數的返回值是相同。故在開發時,可將loops改回1,便於檢查程式是否有問題;帶了測試時,再將loops改為較大的值。

    3.1.2 輸出環境信息(OutputEnvironment)

    因為這次測試了多個平臺,不同平臺的環境信息信息均不同。於是可以專門用一個函數來輸出環境信息,源碼如下。

    /// <summary>
    /// Is release make.
    /// </summary>
    public static readonly bool IsRelease =
    #if DEBUG
        false
    #else
        true
    #endif
    ;
    
    /// <summary>
    /// Output Environment.
    /// </summary>
    /// <param name="tw">Output <see cref="TextWriter"/>.</param>
    /// <param name="indent">The indent.</param>
    public static void OutputEnvironment(TextWriter tw, string indent) {
        if (null == tw) return;
        if (null == indent) indent="";
        //string indentNext = indent + "\t";
        tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease));
        tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")));
        tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount));
        tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem));
        tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess));
        tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion));
        tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version));
        //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version
        tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()));
    #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0)
    #else
        tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription));
    #endif
        tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian));
        tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size));
        tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated));
        tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8));
        tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8));
        tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8));
        Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly;
        //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly));
        tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase));
        assembly = typeof(Vector<float>).GetTypeInfo().Assembly;
        tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase));
    }
    

    例如在 .NET Core 2.0 平臺運行時,會輸出這些信息:

    IsRelease:      True
    EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
    Environment.ProcessorCount:     8
    Environment.Is64BitOperatingSystem:     True
    Environment.Is64BitProcess:     True
    Environment.OSVersion:  Microsoft Windows NT 10.0.19044.0
    Environment.Version:    4.0.30319.42000
    RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\
    RuntimeInformation.FrameworkDescription:        .NET Core 4.6.26614.01
    BitConverter.IsLittleEndian:    True
    IntPtr.Size:    8
    Vector.IsHardwareAccelerated:   True
    Vector<byte>.Count:     32      # 256bit
    Vector<float>.Count:    8       # 256bit
    Vector<double>.Count:   4       # 256bit
    Vector4.Assembly.CodeBase:      file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    

    輸出信息說明——

    • IsRelease: 是不是以 Release方式編譯的程式。
    • EnvironmentVariable(PROCESSOR_IDENTIFIER): CPU型號標識。
    • Environment.ProcessorCount: 邏輯處理器數量。
    • Environment.Is64BitOperatingSystem: 是不是64位操作系統。
    • Environment.Is64BitProcess: 當前進程是不是64位的。
    • Environment.OSVersion: 操作系統的版本。
    • Environment.Version: .NET運行環境的版本。
    • RuntimeEnvironment.GetRuntimeDirectory: .NET基礎庫的運行路徑。
    • RuntimeInformation.FrameworkDescription: .NET平臺的版本。
    • BitConverter.IsLittleEndian: 是不是小端方式。
    • IntPtr.Size: 指針的大小。32位時為4,64位時為8。
    • Vector.IsHardwareAccelerated: Vector<T> 是否支持硬體加速。
    • Vector<byte>.Count: Vector<byte>的元素個數、總位數。
    • Vector<float>.Count: Vector<float>的元素個數、總位數。
    • Vector<double>.Count: Vector<double>的元素個數、總位數。
    • Vector4.Assembly.CodeBase: Vector4 所屬程式集的路徑。
    • Vector<T>.Assembly.CodeBase: Vector<T> 所屬程式集的路徑。

    3.1.3 彙總

    下麵是BenchmarkVectorDemo類的完整代碼。

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Numerics;
    using System.Reflection;
    using System.Text;
    
    namespace BenchmarkVector {
        /// <summary>
        /// Benchmark Vector Demo
        /// </summary>
        static class BenchmarkVectorDemo {
            /// <summary>
            /// Is release make.
            /// </summary>
            public static readonly bool IsRelease =
    #if DEBUG
                false
    #else
                true
    #endif
            ;
    
            /// <summary>
            /// Output Environment.
            /// </summary>
            /// <param name="tw">Output <see cref="TextWriter"/>.</param>
            /// <param name="indent">The indent.</param>
            public static void OutputEnvironment(TextWriter tw, string indent) {
                if (null == tw) return;
                if (null == indent) indent="";
                //string indentNext = indent + "\t";
                tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease));
                tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")));
                tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount));
                tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem));
                tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess));
                tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion));
                tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version));
                //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version
                tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()));
    #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0)
    #else
                tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription));
    #endif
                tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian));
                tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size));
                tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated));
                tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8));
                tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8));
                tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8));
                Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly;
                //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly));
                tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase));
                assembly = typeof(Vector<float>).GetTypeInfo().Assembly;
                tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase));
            }
    
            /// <summary>
            /// Do Benchmark.
            /// </summary>
            /// <param name="tw">Output <see cref="TextWriter"/>.</param>
            /// <param name="indent">The indent.</param>
            public static void Benchmark(TextWriter tw, string indent) {
                if (null == tw) return;
                if (null == indent) indent = "";
                //string indentNext = indent + "\t";
                // init.
                int tickBegin, msUsed;
                double mFlops; // MFLOPS/s .
                double scale;
                float rt;
                const int count = 1024*4;
                const int loops = 1000 * 1000;
                //const int loops = 1;
                const double countMFlops = count * (double)loops / (1000.0 * 1000);
                float[] src = new float[count];
                for(int i=0; i< count; ++i) {
                    src[i] = i;
                }
                tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops));
                // SumBase.
                tickBegin = Environment.TickCount;
                rt = SumBase(src, count, loops);
                msUsed = Environment.TickCount - tickBegin;
                mFlops = countMFlops * 1000 / msUsed;
                tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops));
                double mFlopsBase = mFlops;
                // SumVector4.
                tickBegin = Environment.TickCount;
                rt = SumVector4(src, count, loops);
                msUsed = Environment.TickCount - tickBegin;
                mFlops = countMFlops * 1000 / msUsed;
                scale = mFlops / mFlopsBase;
                tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
                // SumVectorT.
                tickBegin = Environment.TickCount;
                rt = SumVectorT(src, count, loops);
                msUsed = Environment.TickCount - tickBegin;
                mFlops = countMFlops * 1000 / msUsed;
                scale = mFlops / mFlopsBase;
                tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
            }
    
            /// <summary>
            /// Sum - base.
            /// </summary>
            /// <param name="src">Soure array.</param>
            /// <param name="count">Soure array count.</param>
            /// <param name="loops">Benchmark loops.</param>
            /// <returns>Return the sum value.</returns>
            private static float SumBase(float[] src, int count, int loops) {
                float rt = 0; // Result.
                for (int j=0; j< loops; ++j) {
                    for(int i=0; i< count; ++i) {
                        rt += src[i];
                    }
                }
                return rt;
            }
    
            /// <summary>
            /// Sum - Vector4.
            /// </summary>
            /// <param name="src">Soure array.</param>
            /// <param name="count">Soure array count.</param>
            /// <param name="loops">Benchmark loops.</param>
            /// <returns>Return the sum value.</returns>
            private static float SumVector4(float[] src, int count, int loops) {
                float rt = 0; // Result.
                const int VectorWidth = 4;
                int nBlockWidth = VectorWidth; // Block width.
                int cntBlock = count / nBlockWidth; // Block count.
                int cntRem = count % nBlockWidth; // Remainder count.
                Vector4 vrt = Vector4.Zero; // Vector result.
                int p; // Index for src data.
                int i;
                // Load.
                Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
                p = 0;
                for (i = 0; i < vsrc.Length; ++i) {
                    vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
                    p += VectorWidth;
                }
                // Body.
                for (int j = 0; j < loops; ++j) {
                    // Vector processs.
                    for (i = 0; i < cntBlock; ++i) {
                        // Equivalent to scalar model: rt += src[i];
                        vrt += vsrc[i]; // Add.
                    }
                    // Remainder processs.
                    p = cntBlock * nBlockWidth;
                    for (i = 0; i < cntRem; ++i) {
                        rt += src[p + i];
                    }
                }
                // Reduce.
                rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
                return rt;
            }
    
            /// <summary>
            /// Sum - Vector<T>.
            /// </summary>
            /// <param name="src">Soure array.</param>
            /// <param name="count">Soure array count.</param>
            /// <param name="loops">Benchmark loops.</param>
            /// <returns>Return the sum value.</returns>
            private static float SumVectorT(float[] src, int count, int loops) {
                float rt = 0; // Result.
                int VectorWidth = Vector<float>.Count; // Block width.
                int nBlockWidth = VectorWidth; // Block width.
                int cntBlock = count / nBlockWidth; // Block count.
                int cntRem = count % nBlockWidth; // Remainder count.
                Vector<float> vrt = Vector<float>.Zero; // Vector result.
                int p; // Index for src data.
                int i;
                // Load.
                Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src.
                p = 0;
                for (i = 0; i < vsrc.Length; ++i) {
                    vsrc[i] = new Vector<float>(src, p);
                    p += VectorWidth;
                }
                // Body.
                for (int j = 0; j < loops; ++j) {
                    // Vector processs.
                    for (i = 0; i < cntBlock; ++i) {
                        vrt += vsrc[i]; // Add.
                    }
                    // Remainder processs.
                    p = cntBlock * nBlockWidth;
                    for (i = 0; i < cntRem; ++i) {
                        rt += src[p + i];
                    }
                }
                // Reduce.
                for (i = 0; i < VectorWidth; ++i) {
                    rt += vrt[i];
                }
                return rt;
            }
    
        }
    }
    

    3.2 在 .NET Core 里進行測試

    3.2.1 搭建測試項目(BenchmarkVectorCore20)

    雖然從.NET Core 1.0開始就支持了向量類型,但本文考慮到需要與.NET Standard進行對比測試,故選擇 .NET Core 2.0 比較好。
    在解決方案里建立新項目“BenchmarkVectorCore20”,它是 .NET Core 2.0 控制台程式的項目。並讓“BenchmarkVectorCore20”引用共用項目“BenchmarkVector”。
    隨後我們修改一下 Program 類的代碼,加上調用測試函數的代碼。代碼如下。

    using BenchmarkVector;
    using System;
    using System.IO;
    using System.Numerics;
    
    namespace BenchmarkVectorCore20 {
        class Program {
            static void Main(string[] args) {
                string indent = "";
                TextWriter tw = Console.Out;
                tw.WriteLine("BenchmarkVectorCore20");
                tw.WriteLine();
                BenchmarkVectorDemo.OutputEnvironment(tw, indent);
                //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase));
                tw.WriteLine(indent);
                BenchmarkVectorDemo.Benchmark(tw, indent);
                // Vector<int> a = Vector<int>.One;
                // a <<= 1; // CS0019	Operator '<<=' cannot be applied to operands of type 'Vector<int>' and 'int'
            }
        }
    }
    

    註:上面代碼還測試了一下 Vector<T> 是否支持移位運算符,發現目前不支持。從 .NET 的發展路線圖來看,到了 .NET 7Vector<T>會支持移位運算符。

    3.2.2 BenchmarkVectorCore20的測試結果

    在我的電腦(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10)上運行時,輸出信息為:

    BenchmarkVectorCore20
    
    IsRelease:      True
    EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
    Environment.ProcessorCount:     8
    Environment.Is64BitOperatingSystem:     True
    Environment.Is64BitProcess:     True
    Environment.OSVersion:  Microsoft Windows NT 10.0.19044.0
    Environment.Version:    4.0.30319.42000
    RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\
    RuntimeInformation.FrameworkDescription:        .NET Core 4.6.26614.01
    BitConverter.IsLittleEndian:    True
    IntPtr.Size:    8
    Vector.IsHardwareAccelerated:   True
    Vector<byte>.Count:     32      # 256bit
    Vector<float>.Count:    8       # 256bit
    Vector<double>.Count:   4       # 256bit
    Vector4.Assembly.CodeBase:      file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    
    Benchmark:      count=4096, loops=1000000, countMFlops=4096
    SumBase:        6.871948E+10    # msUsed=4937, MFLOPS/s=829.653635811221
    SumVector4:     2.748779E+11    # msUsed=1234, MFLOPS/s=3319.2868719611, scale=4.00081037277147
    SumVectorT:     5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.8992
    

    3.3 在 .NET Core 里測試 .NET Standard類庫里的測試代碼

    官方文檔上,僅 .NET Standard 2.1 才支持這2種向量類型。而.NET Standard 2.0應用最廣泛,該怎麼在.NET Standard 2.0上使用它們?
    在nuget上找了一下,發現 System.Numerics.Vectors 包提供了這2類向量類型,且它支持 .NET Standard 2.0 平臺。可以考慮引用該包。

    此時有一個疑問——若引用的是nuget的System.Numerics.Vectors 包,向量類型是否仍會有硬體加速?
    我們將建立一個測試程式,來檢測這一點。

    3.4.1 搭建類庫項目(BenchmarkVectorLib)

    在解決方案里建立新項目“BenchmarkVectorLib”,它是 .NET Standard 2.0 類庫項目。並讓“BenchmarkVectorLib”引用共用項目“BenchmarkVector”。
    隨後建立一個 BenchmarkVectorUtil 類,用於暴露測試函數。代碼如下。

    using BenchmarkVector;
    using System;
    using System.IO;
    
    namespace BenchmarkVectorLib {
        /// <summary>
        /// Benchmark Vector Util
        /// </summary>
        public static class BenchmarkVectorUtil {
    
            /// <summary>
            /// Output Environment.
            /// </summary>
            /// <param name="tw">Output <see cref="TextWriter"/>.</param>
            /// <param name="indent">The indent.</param>
            public static void OutputEnvironment(TextWriter tw, string indent) {
                BenchmarkVectorDemo.OutputEnvironment(tw, indent);
            }
    
            /// <summary>
            /// Do Benchmark.
            /// </summary>
            /// <param name="tw">Output <see cref="TextWriter"/>.</param>
            /// <param name="indent">The indent.</param>
            public static void Benchmark(TextWriter tw, string indent) {
                BenchmarkVectorDemo.Benchmark(tw, indent);
            }
        }
    }
    

    3.4.2 搭建測試項目(BenchmarkVectorCore20UseLib)

    在解決方案里建立新項目“BenchmarkVectorCore20UseLib”,它是 .NET Core 2.0 控制台程式的項目。並讓“BenchmarkVectorCore20”引用剛纔建立的.NET Standard 2.0類庫“BenchmarkVectorLib”。
    隨後我們修改一下 Program 類的代碼,加上調用測試函數的代碼。代碼如下。

    using BenchmarkVectorLib;
    using System;
    using System.IO;
    using System.Numerics;
    
    namespace BenchmarkVectorCore20UseLib {
        class Program {
            static void Main(string[] args) {
                string indent = "";
                TextWriter tw = Console.Out;
                tw.WriteLine("BenchmarkVectorCore20UseLib");
                tw.WriteLine();
                BenchmarkVectorUtil.OutputEnvironment(tw, indent);
                //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase));
                tw.WriteLine(indent);
                BenchmarkVectorUtil.Benchmark(tw, indent);
            }
        }
    }
    

    3.4.3 BenchmarkVectorCore20UseLib的測試結果

    在我的電腦(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10)上運行時,輸出信息為:

    BenchmarkVectorCore20UseLib
    
    IsRelease:      True
    EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
    Environment.ProcessorCount:     8
    Environment.Is64BitOperatingSystem:     True
    Environment.Is64BitProcess:     True
    Environment.OSVersion:  Microsoft Windows NT 10.0.19044.0
    Environment.Version:    4.0.30319.42000
    RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\
    RuntimeInformation.FrameworkDescription:        .NET Core 4.6.26614.01
    BitConverter.IsLittleEndian:    True
    IntPtr.Size:    8
    Vector.IsHardwareAccelerated:   True
    Vector<byte>.Count:     32      # 256bit
    Vector<float>.Count:    8       # 256bit
    Vector<double>.Count:   4       # 256bit
    Vector4.Assembly.CodeBase:      file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
    
    Benchmark:      count=4096, loops=1000000, countMFlops=4096
    SumBase:        6.871948E+10    # msUsed=4906, MFLOPS/s=834.896045658377
    SumVector4:     2.748779E+11    # msUsed=1219, MFLOPS/s=3360.13125512715, scale=4.02461033634126
    SumVectorT:     5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.8496
    

    可以發現該程式測得的浮點性能,與BenchmarkVectorCore20的差不多,表示硬體加速生效了。於是可以解答之前的問題了——

    • 若引用的是nuget的System.Numerics.Vectors 包,向量類型仍會有硬體加速。
    • 若在.NET Standard 2.0 類庫里使用了向量類型,那麼 .NET Core引用類庫時,向量類型仍會有硬體加速。

    3.4 在 .NET Framework 里進行測試

    官方文檔上,.NET Framework 4.6 才支持大小固定的向量(如Vector4),且Vector<T>未提到.NET Framework的支持版本。難道 .NET Framework用不了Vector<T> 嗎? .NET Framework 4.5等版本時是否能使用它們?
    在nuget上找了一下,發現 System.Numerics.Vectors 包支持.NET Framework,最早能支持 .NET Framework 4.5。
    而且 System.Numerics.Vectors 包里提供了這2類向量類型。對比官方文檔,此時有這些疑惑——

    • 官方文檔的Vector<T>未提到.NET Framework的支持版本,當 .NET Framework 下使用System.Numerics.Vectors 包時,是否有硬體加速?
    • 官方文檔里說.NET Framework 4.6才支持大小固定的向量(如Vector4),當 .NET Framework 4.5下使用System.Numerics.Vectors 包時,是否有硬體加速?
    • 官方文檔里說.NET Framework 4.6才支持大小固定的向量(如Vector4),當 .NET Framework 4.5下使用System.Numerics.Vectors 包時,Vector4是屬於哪個程式集的?

    下麵的測試程式,將回答以上問題。

    3.4.1 搭建4.5的測試項目(BenchmarkVectorFw45)

    在解決方案里建立新項目“BenchmarkVectorFw45”,它是 .NET Framework 4.5 控制台程式的項目。並讓“BenchmarkVectorFw45”引用共用項目“BenchmarkVector”。
    隨後我們修改一下 Program 類的代碼,加上調用測試函數的代碼。代碼如下:

    using BenchmarkVector;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace BenchmarkVectorFw45 {
        class Program {
            static void Main(string[] args) {
                string indent = "";
                TextWriter tw = Console.Out;
                tw.WriteLine("BenchmarkVectorFw45");
                tw.WriteLine();
                BenchmarkVectorDemo.OutputEnvironment(tw, indent);
                //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase));
                tw.WriteLine(indent);
                BenchmarkVectorDemo.Benchmark(tw, indent);
            }
        }
    }
    

    3.4.2 BenchmarkVectorFw45的測試結果

    在我的電腦(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10)上運行時,輸出信息為:

    BenchmarkVectorFw45
    
    IsRelease:      True
    EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
    Environment.ProcessorCount:     8
    Environment.Is64BitOperatingSystem:     True
    Environment.Is64BitProcess:     True
    Environment.OSVersion:  Microsoft Windows NT 6.2.9200.0
    Environment.Version:    4.0.30319.42000
    RuntimeEnvironment.GetRuntimeDirectory: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\
    BitConverter.IsLittleEndian:    True
    IntPtr.Size:    8
    Vector.IsHardwareAccelerated:   True
    Vector<byte>.Count:     32      # 256bit
    Vector<float>.Count:    8       # 256bit
    Vector<double>.Count:   4       # 256bit
    Vector4.Assembly.CodeBase:      file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw45/bin/Release/System.Numerics.Vectors.DLL
    Vector<T>.Assembly.CodeBase:    file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw45/bin/Release/System.Numerics.Vectors.DLL
    
    Benchmark:      count=4096, loops=1000000, countMFlops=4096
    SumBase:        6.871948E+10    # msUsed=4922, MFLOPS/s=832.182039821211
    SumVector4:     2.748779E+11    # msUsed=1235, MFLOPS/s=3316.5991902834, scale=3.98542510121457
    SumVectorT:     5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.8752
    

    可以發現該程式測得的浮點性能,與BenchmarkVectorCore20的差不多,表示硬體加速生效了。於是可以解答之前的問題了——

    • 官方文檔的Vector<T>未提到.NET Framework的支持版本,當 .NET Framework 下使用System.Numerics.Vectors 包時,仍會有硬體加速。
    • 官方文檔里說.NET Framework 4.6才支持大小固定的向量(如Vector4),當 .NET Framework 4.5下使用System.Numerics.Vectors 包時,仍會有硬體加速。

    這一點貌似有點奇怪——.NET Framework 4.5 標準庫未提供向量類型,靠nuget引用第三方庫使用向量類型,卻也能得到硬體加速。
    其實原因並不複雜,讓向量類型獲得硬體加速,其實是JIT(即時編譯器)的工作。具體來說,是 RyuJIT 讓向量類型獲得了硬體加速的。
    .NET Framework 4.5 標準庫未提供向量類型,僅是編譯無法通過的問題;通過nuget包,可以引入向量類型,解決了編譯問題。隨後.NET Framework 4.5程式運行時,若用了RyuJIT且硬體支持SIMD時,程式便能用上硬體加速。

    3.4.3 搭建4.6.1的測試項目(BenchmarkVectorFw46)

    官方文檔里說.NET Framework 4.6才支持大小固定的向量(如Vector4),我們來測試一下吧。隨後為了便於與 .NET Standard 2.0類庫測試做對比,故選擇了 .NET Framework 4.6.1。為了使項目名簡單,故項目名為“BenchmarkVectorFw46”。
    在解決方案里建立新項目“BenchmarkVectorFw46”,它是 .NET Framework 4.6.1 控制台程式的項目。並讓“BenchmarkVectorFw46”引用共用項目“BenchmarkVector”。
    隨後我們修改一下 Program 類的代碼,加上調用測試函數的代碼。代碼如下:

    using BenchmarkVector;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Numerics;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace BenchmarkVectorFw46 {
        class Program {
            static void Main(string[] args) {
                string indent = "";
                TextWriter tw = Console.Out;
                tw.WriteLine("BenchmarkVectorFw46");
                tw.WriteLine();
                BenchmarkVectorDemo.OutputEnvironment(tw, indent);
                //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase));
                tw.WriteLine(indent);
                BenchmarkVectorDemo.Benchmark(tw, indent);
            }
        }
    }
    

    3.4.4 BenchmarkVectorFw46的測試結果

    在我的電腦(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10)上運行時,輸出信息為:

    BenchmarkVectorFw46
    
    IsRelease:      True
    EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
    Environment.ProcessorCount:     8
    Environment.Is64BitOperatingSystem:     True
    Environment.Is64BitProcess:     True
    Environment.OSVers
    
    您的分享是我們最大的動力!

    -Advertisement-
    Play Games
    更多相關文章
    • 最近做一個圖片上傳需要裁剪一下的功能然後百度了一下,找到了cropperjs 說明文檔 https://www.npmjs.com/package/cropperjs 這是一個簡單的基礎使用 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...
    • 本文是深入淺出 ahooks 源碼系列文章的第十篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 今天我們來聊聊 ahooks 中對 Map 和 Set 類型進行狀態管理的 hook,順便複習一下 Set 和 Map 這兩種數據類型。 useMap 管理 Map ...
    • button組件幾乎是每個組件庫都有的;其實實現一個button組件是很簡單的。本篇文章將帶你一步一步的實現一個button組件。如果你想瞭解完整的組件庫搭建,你可以先看使用Vite和TypeScript帶你從零打造一個屬於自己的Vue3組件庫,這篇文章有詳細介紹。當然如果你只想知道一個button ...
    • 在我們開發開發H5程式或者小程式的時候,有時候需要基於內置瀏覽器或者微信開發者工具進行測試,這個時候可以採用預設的localhost進行訪問後端介面,一般來說沒什麼問題,如果我們需要通過USB基座方式發佈到手機進行App測試的時候,那就需要使用區域網的IP地址了,否則就無法訪問後端的介面,本篇隨筆總... ...
    • 微任務 在js中,當使用promise,會將當前任務加入事件執行的微任務隊列,有且只有這一種方法可以,因為當使用了promise,在JS引擎中會觸發VM::queueMicrotask,會向m_microtaskQueue隊列中壓入事件,在V8中只有這一種暴露方式,沒有其他介面可以調用這個方法 vo ...
    • C# 在中國的採用需要一個殺手級應用的帶動, 那麼這樣的一個殺手級應用是 Unity嗎,我這裡大膽推測採用CoreCLR 的新一代完全採用C#構建的Unity 將是這樣的一個殺手級應用。Unity已被廣泛應用於數字孿生、數字城市、數字工廠等場景,成為各產業加速數字化轉型的一個通用技術平臺底座,而對接 ...
    • 學習內容及其引用 [ ] 委托的定義以及如何理解委托 [ ] 委托的聲明及其由來 [ ] 委托類型的實例 [ ] 多播委托 [ ] 委托的缺點 [ ] Action委托與Func委托 委托•語法篇 C#語言入門詳解 Delegate詳解 委托的定義以及如何理解委托 委托現實的定義: 本人不需要親自去 ...
    • #前言 前段時間需要在一個新項目里添加兩個後臺任務,去定時請求兩個供應商的API來同步數據;由於項目本身只是一個很小的服務,不太希望引入太重的框架,同時也沒持久化要求;於是我開始尋找在Quartz.Net、Hangfire之外,是否還有更為輕量級的框架滿足我的要求,最終我選擇了Coravel. #簡 ...
    一周排行
      -Advertisement-
      Play Games
    • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
    • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
    • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
    • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
    • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
    • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
    • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
    • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
    • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
    • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...