主頁 > .NET開發 > C# 使用SIMD向量型別加速浮點陣列求和運算(3):回圈展開

C# 使用SIMD向量型別加速浮點陣列求和運算(3):回圈展開

2022-11-17 06:09:35 .NET開發

作者: zyl910

目錄
  • 一、背景
    • 1.1 回圈展開簡介
    • 1.2 測驗準備
  • 二、在C#中使用
    • 2.1 對基礎演算法做回圈展開
      • 2.1.1 測驗結果:
    • 2.2 對 Vector4 版演算法做回圈展開
      • 2.2.1 測驗結果:
    • 2.3 對 Vector<T> 版演算法做回圈展開
      • 2.3.1 測驗結果:
    • 2.4 對 Avx版演算法做回圈展開
      • 2.4.1 測驗結果:
    • 2.5 對 Avx版演算法做回圈展開16次
      • 2.5.1 測驗結果:
    • 2.6 嘗試用陣列來存盤回圈展開的臨時變數
      • 2.6.1 測驗結果:
    • 2.7 嘗試用堆疊陣列來減少相關性
      • 2.7.1 測驗結果:
    • 2.8 測驗結果匯總
  • 三、在C++中使用
    • 3.1 修改代碼
    • 3.2 測驗結果
  • 四、小結
  • 參考文獻

一、背景

先前的2篇文章,說了向量型別的型別選擇問題,本文討論一個使用方面的問題——回圈展開,

現在的CPU采用了流水線、超標量等機制來提高運算性能,如果完全是順序代碼,那么流水線的效果會非常好,
但是程式中不可避免的需要 分支 與 回圈來處理各種復雜的邏輯,分支與回圈會被編譯為跳轉指令,而跳轉指令會導致CPU流水線失效,對性能的影響很大,雖然現代處理器增加了分支預測技術,但總會有預測失敗的概率,
尤其是在使用向量型別進行SIMD運算時,因向量型別僅盡可能榨干CPU內部的ALU(算術邏輯單元),于是在跳轉時的性能損失更大,

故在使用向量型別處理大規模數學計算時,應盡可能的避免分支與回圈,
對于分支,最好盡量將分支挪到內回圈外,若是內回圈中必須的分支,可盡量用位掩碼等辦法來寫無分支代碼,
對于回圈,一般可使用回圈展開技術,來避免短的回圈,

1.1 回圈展開簡介

摘錄——

回圈展開(Loop unrolling)技術是一種提升程式執行速度的非常有效的優化方法,它可以由程式員手工撰寫,也可由編譯器自動優化,回圈展開的本質是,利用CPU指令級并行,來降低回圈的開銷,當然,同時也有利于指令流水線的高效調度,
……

回圈展開的優點:
第一,減少了分支預測失敗的可能性,
第二,增加了回圈體內陳述句并發執行的可能性,當然,這需要回圈體內各陳述句不存在資料相關性,

回圈展開的缺點:
第一,造成代碼膨脹,導致ELF檔案(或Windows PE檔案)尺寸增大,
第二,代碼可讀性顯著降低,前一個人寫的回圈展開代碼,很可能被不熟悉的后續維護人員改回去,

1.2 測驗準備

注意,回圈展開提高的是流水線性能,對小回圈效果明顯,此時分支造成的延時,大多與內回圈的運算耗時差不多,
對于有些復雜的大回圈,內回圈的運算耗時已經很大了,而分支造成的延時仍是常數值,比例下降了很多,此時回圈展開的收益就少了,

由于回圈展開是程式員手工撰寫的,故必須在編碼前就確定好展開次數,
本文就來探討一下大多數時候的展開次數選擇,

展開2倍的話,性能最多為原來2倍,即大多數情況下只有1倍多的性能提升,提升不大,
展開2倍的話,性能最多為原來4倍,區間大了,很多時候能達到2倍以上的提升,
故一開始可以用展開4倍來測驗,下面將進行測驗,

測驗電腦的配置資訊為:lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10,

二、在C#中使用

為了對比測驗 Avx指令的效果,故可在 BenchmarkVectorCore30 工程里進行測驗,因是64位作業系統,故選取 x64、Release版的測驗結果.

2.1 對基礎演算法做回圈展開

回顧一下基礎演算法:

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;
}

改為回圈展開4倍后,代碼為:

private static float SumBaseU4(float[] src, int count, int loops) {
    float rt = 0; // Result.
    float rt1 = 0;
    float rt2 = 0;
    float rt3 = 0;
    int nBlockWidth = 4; // Block width.
    int cntBlock = count / nBlockWidth; // Block count.
    int cntRem = count % nBlockWidth; // Remainder count.
    int p; // Index for src data.
    int i;
    for (int j = 0; j < loops; ++j) {
        p = 0;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            rt += src[p];
            rt1 += src[p + 1];
            rt2 += src[p + 2];
            rt3 += src[p + 3];
            p += nBlockWidth;
        }
        // Remainder processs.
        //p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    rt = rt + rt1 + rt2 + rt3;
    return rt;
}

之前內回圈只處理1個資料,現在內回圈處理了4個資料,
注意內回圈在處理者4個資料時,并不是直接將結果全部累加到 rt 變數,而是使用新增的 rt1、rt2、rt3 變數來臨時存盤累加值,這是為了消除變數之間的相關性,因為變數之間的相關性會影響流水線性能,故分別使用獨立的變數就好了,
最后在 Reduce 階段,將 rt1、rt2、rt3 的值累加到 rt,

2.1.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336

可以發現,基礎演算法使用4倍回圈展開后,性能是原先的 2.6336 倍,

2.2 對 Vector4 版演算法做回圈展開

回顧一下Vector4 版演算法:

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 = https://www.cnblogs.com/zyl910/archive/2022/11/16/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;
}

改為回圈展開4倍后,代碼為:

private static float SumVector4U4(float[] src, int count, int loops) {
    float rt = 0; // Result.
    const int LoopUnrolling = 4;
    const int VectorWidth = 4;
    int nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    int cntBlock = count / nBlockWidth; // Block count.
    int cntRem = count % nBlockWidth; // Remainder count.
    Vector4 vrt = Vector4.Zero; // Vector result.
    Vector4 vrt1 = Vector4.Zero;
    Vector4 vrt2 = Vector4.Zero;
    Vector4 vrt3 = Vector4.Zero;
    int p; // Index for src data.
    int i;
    // Load.
    Vector4[] vsrc = https://www.cnblogs.com/zyl910/archive/2022/11/16/new Vector4[count / VectorWidth]; // 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) {
        p = 0;
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt += vsrc[p]; // Add.
            vrt1 += vsrc[p + 1];
            vrt2 += vsrc[p + 2];
            vrt3 += vsrc[p + 3];
            p += LoopUnrolling;
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    vrt = vrt + vrt1 + vrt2 + vrt3;
    rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
    return rt;
}

跟剛才的辦法一樣,使用新增的 rt1、rt2、rt3 變數來臨時存盤累加值,消除變數之間的相關性,
最后在 Reduce 階段,將 vrt1、vrt2、vrt3 的值累加到 vrt,

2.2.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVector4:     2.748779E+11    # msUsed=1218, MFLOPS/s=3362.8899835796387, scale=4.054187192118227
SumVector4U4:   1.0995116E+12   # msUsed=532, MFLOPS/s=7699.248120300752, scale=9.281954887218046

SumVector4U4對比基礎演算法(SumBase),性能倍數是 9.281954887218046,
SumVector4U4對比未回圈展開的演算法(SumVector4),倍數是 9.281954887218046/4.054187192118227=2.2894736842105263092984587836542

2.3 對 Vector<T> 版演算法做回圈展開

回顧一下 Vector<T> 版演算法:

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 = https://www.cnblogs.com/zyl910/archive/2022/11/16/new Vector[cntBlock]; // Vector src.
    p = 0;
    for (i = 0; i < vsrc.Length; ++i) {
        vsrc[i] = new Vector(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;
}

改為回圈展開4倍后,代碼為:

private static float SumVectorTU4(float[] src, int count, int loops) {
    float rt = 0; // Result.
    const int LoopUnrolling = 4;
    int VectorWidth = Vector<float>.Count; // Block width.
    int nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    int cntBlock = count / nBlockWidth; // Block count.
    int cntRem = count % nBlockWidth; // Remainder count.
    Vector<float> vrt = Vector<float>.Zero; // Vector result.
    Vector<float> vrt1 = Vector<float>.Zero;
    Vector<float> vrt2 = Vector<float>.Zero;
    Vector<float> vrt3 = Vector<float>.Zero;
    int p; // Index for src data.
    int i;
    // Load.
    Vector<float>[] vsrc = https://www.cnblogs.com/zyl910/archive/2022/11/16/new Vector[count / VectorWidth]; // Vector src.
    p = 0;
    for (i = 0; i < vsrc.Length; ++i) {
        vsrc[i] = new Vector(src, p);
        p += VectorWidth;
    }
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = 0;
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt += vsrc[p]; // Add.
            vrt1 += vsrc[p + 1];
            vrt2 += vsrc[p + 1];
            vrt3 += vsrc[p + 1];
            p += LoopUnrolling;
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    vrt = vrt + vrt1 + vrt2 + vrt3;
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt[i];
    }
    return rt;
}

跟剛才的辦法一樣,使用新增的 rt1、rt2、rt3 變數來臨時存盤累加值,消除變量之間的相關性,
最后在 Reduce 階段,將 vrt1、vrt2、vrt3 的值累加到 vrt,

2.3.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVectorT:     5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.108374384236454
SumVectorTU4:   2.1990233E+12   # msUsed=203, MFLOPS/s=20177.339901477833, scale=24.32512315270936

SumVectorTU4對比基礎演算法(SumBase),性能倍數是 24.32512315270936,
SumVectorTU4對比未回圈展開的演算法(SumVectorT),倍數是 24.32512315270936/8.108374384236454=2.9999999999999997533414337788579
初步發現 Vector<T>回圈展開(2.9999)帶來的性能提升, 比VectorT回圈展開(2.2894)更高一些,

2.4 對 Avx版演算法做回圈展開

先前分別嘗試用 陣列、Span、指標 的辦法來操縱資料、使用Avx指令集,現在對這3種辦法,均寫一套回圈展開4次的代碼:

/// <summary>
/// Sum - Vector AVX.
/// </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 SumVectorAvx(float[] src, int count, int loops) {
#if Allow_Intrinsics
    float rt = 0; // Result.
    //int VectorWidth = 32 / 4; // sizeof(__m256) / sizeof(float);
    int VectorWidth = Vector256<float>.Count; // Block width.
    int nBlockWidth = VectorWidth; // Block width.
    int cntBlock = count / nBlockWidth; // Block count.
    int cntRem = count % nBlockWidth; // Remainder count.
    Vector256<float> vrt = Vector256<float>.Zero; // Vector result.
    int p; // Index for src data.
    int i;
    // Load.
    Vector256<float>[] vsrc = https://www.cnblogs.com/zyl910/archive/2022/11/16/new Vector256[cntBlock]; // Vector src.
    p = 0;
    for (i = 0; i < cntBlock; ++i) {
        vsrc[i] = Vector256.Create(src[p], src[p + 1], src[p + 2], src[p + 3], src[p + 4], src[p + 5], src[p + 6], src[p + 7]); // Load.
        p += VectorWidth;
    }
    // Body.
    for (int j = 0; j < loops; ++j) {
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt = Avx.Add(vrt, vsrc[i]);    // Add. vrt += vsrc[i];
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt.GetElement(i);
    }
    return rt;
#else
    throw new NotSupportedException();
#endif
}

/// 
/// Sum - Vector AVX - Loop unrolling *4.
/// 
/// [count / VectorWidth]; // Vector src.
    p = 0;
    for (i = 0; i < vsrc.Length; ++i) {
        vsrc[i] = Vector256.Create(src[p], src[p + 1], src[p + 2], src[p + 3], src[p + 4], src[p + 5], src[p + 6], src[p + 7]); // Load.
        p += VectorWidth;
    }
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = 0;
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt = Avx.Add(vrt, vsrc[p]);    // Add. vrt += vsrc[p];
            vrt1 = Avx.Add(vrt1, vsrc[p + 1]);
            vrt2 = Avx.Add(vrt2, vsrc[p + 2]);
            vrt3 = Avx.Add(vrt3, vsrc[p + 3]);
            p += LoopUnrolling;
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    vrt = Avx.Add(Avx.Add(vrt, vrt1), Avx.Add(vrt2, vrt3)); // vrt = vrt + vrt1 + vrt2 + vrt3;
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt.GetElement(i);
    }
    return rt;
#else
    throw new NotSupportedException();
#endif
}

/// 
/// Sum - Vector AVX - Span.
/// 
///  >(new Span(src)); // Reinterpret cast. `float*` to `Vector256*`.
        for (i = 0; i < cntBlock; ++i) {
            vrt = Avx.Add(vrt, vsrc[i]);    // Add. vrt += vsrc[i];
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt.GetElement(i);
    }
    return rt;
#else
    throw new NotSupportedException();
#endif
}

/// 
/// Sum - Vector AVX - Span - Loop unrolling *4.
/// 
/// >(new Span(src)); // Reinterpret cast. `float*` to `Vector256*`.
        for (i = 0; i < cntBlock; ++i) {
            vrt = Avx.Add(vrt, vsrc[p]);    // Add. vrt += vsrc[p];
            vrt1 = Avx.Add(vrt1, vsrc[p + 1]);
            vrt2 = Avx.Add(vrt2, vsrc[p + 2]);
            vrt3 = Avx.Add(vrt3, vsrc[p + 3]);
            p += LoopUnrolling;
        }
        // Remainder processs.
        p = cntBlock * nBlockWidth;
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    vrt = Avx.Add(Avx.Add(vrt, vrt1), Avx.Add(vrt2, vrt3)); // vrt = vrt + vrt1 + vrt2 + vrt3;
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt.GetElement(i);
    }
    return rt;
#else
    throw new NotSupportedException();
#endif
}

/// 
/// Sum - Vector AVX - Ptr.
/// 
/// 

2.4.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVectorAvx:   5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.108374384236454
SumVectorAvxSpan:       5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.9008
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.095081967213115
SumVectorAvxU4: 2.1990233E+12   # msUsed=328, MFLOPS/s=12487.80487804878, scale=15.054878048780488
SumVectorAvxSpanU4:     2.1990233E+12   # msUsed=312, MFLOPS/s=13128.205128205129, scale=15.826923076923078
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=157, MFLOPS/s=26089.171974522294, scale=31.452229299363058

未做回圈展開時,這3鐘辦法的性能拉不開差距,都是8倍左右,
而現在用了回圈展開后,陣列版(SumVectorAvxU4)、Span版(SumVectorAvxSpanU4)只有15倍左右的性能提升,而指標版有 31倍性能提升,是 陣列版、Span版 的2倍,
可能是因為指標更貼近底層硬體、更易于編譯器優化,故當使用內在函式時,推薦優先使用指標,

SumVectorAvxPtrU4 對比基礎演算法(SumBase),性能倍數是 31.452229299363058,
SumVectorAvxPtrU4 對比未回圈展開的演算法(SumVectorAvxPtr),倍數是 31.452229299363058/8.095081967213115=3.8853503184713375449974589366746,

2.5 對 Avx版演算法做回圈展開16次

剛才嘗試了4倍回圈展開,故理論上限是4倍,而SumVectorAvxPtrU4版有約 3.8853 倍性能提升,故可考慮進一步加大,于是可測驗一下 4*4=16 次的回圈展開,

將 SumVectorAvxPtr 改造為回圈展開16次的,代碼如下:

private static float SumVectorAvxPtrU16(float[] src, int count, int loops) {
#if Allow_Intrinsics && UNSAFE
    unsafe {
        float rt = 0; // Result.
        const int LoopUnrolling = 16;
        int VectorWidth = Vector256<float>.Count; // Block width.
        int nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        Vector256<float> vrt = Vector256<float>.Zero; // Vector result.
        Vector256<float> vrt1 = Vector256<float>.Zero;
        Vector256<float> vrt2 = Vector256<float>.Zero;
        Vector256<float> vrt3 = Vector256<float>.Zero;
        Vector256<float> vrt4 = Vector256<float>.Zero;
        Vector256<float> vrt5 = Vector256<float>.Zero;
        Vector256<float> vrt6 = Vector256<float>.Zero;
        Vector256<float> vrt7 = Vector256<float>.Zero;
        Vector256<float> vrt8 = Vector256<float>.Zero;
        Vector256<float> vrt9 = Vector256<float>.Zero;
        Vector256<float> vrt10 = Vector256<float>.Zero;
        Vector256<float> vrt11 = Vector256<float>.Zero;
        Vector256<float> vrt12 = Vector256<float>.Zero;
        Vector256<float> vrt13 = Vector256<float>.Zero;
        Vector256<float> vrt14 = Vector256<float>.Zero;
        Vector256<float> vrt15 = Vector256<float>.Zero;
        float* p; // Pointer for src data.
        int i;
        // Body.
        fixed (float* p0 = &src[0]) {
            for (int j = 0; j < loops; ++j) {
                p = p0;
                // Vector processs.
                for (i = 0; i < cntBlock; ++i) {
                    //vload = Avx.LoadVector256(p);    // Load. vload = *(*__m256)p;
                    vrt = Avx.Add(vrt, Avx.LoadVector256(p)); // Add. vrt[k] += *((*__m256)(p)+k);
                    vrt1 = Avx.Add(vrt1, Avx.LoadVector256(p + VectorWidth * 1));
                    vrt2 = Avx.Add(vrt2, Avx.LoadVector256(p + VectorWidth * 2));
                    vrt3 = Avx.Add(vrt3, Avx.LoadVector256(p + VectorWidth * 3));
                    vrt4 = Avx.Add(vrt4, Avx.LoadVector256(p + VectorWidth * 4));
                    vrt5 = Avx.Add(vrt5, Avx.LoadVector256(p + VectorWidth * 5));
                    vrt6 = Avx.Add(vrt6, Avx.LoadVector256(p + VectorWidth * 6));
                    vrt7 = Avx.Add(vrt7, Avx.LoadVector256(p + VectorWidth * 7));
                    vrt8 = Avx.Add(vrt8, Avx.LoadVector256(p + VectorWidth * 8));
                    vrt9 = Avx.Add(vrt9, Avx.LoadVector256(p + VectorWidth * 9));
                    vrt10 = Avx.Add(vrt10, Avx.LoadVector256(p + VectorWidth * 10));
                    vrt11 = Avx.Add(vrt11, Avx.LoadVector256(p + VectorWidth * 11));
                    vrt12 = Avx.Add(vrt12, Avx.LoadVector256(p + VectorWidth * 12));
                    vrt13 = Avx.Add(vrt13, Avx.LoadVector256(p + VectorWidth * 13));
                    vrt14 = Avx.Add(vrt14, Avx.LoadVector256(p + VectorWidth * 14));
                    vrt15 = Avx.Add(vrt15, Avx.LoadVector256(p + VectorWidth * 15));
                    p += nBlockWidth;
                }
                // Remainder processs.
                for (i = 0; i < cntRem; ++i) {
                    rt += p[i];
                }
            }
        }
        // Reduce.
        vrt = Avx.Add( Avx.Add( Avx.Add(Avx.Add(vrt, vrt1), Avx.Add(vrt2, vrt3))
            , Avx.Add(Avx.Add(vrt4, vrt5), Avx.Add(vrt6, vrt7)) )
            , Avx.Add( Avx.Add(Avx.Add(vrt8, vrt9), Avx.Add(vrt10, vrt11))
            , Avx.Add(Avx.Add(vrt12, vrt13), Avx.Add(vrt14, vrt15)) ) )
        ; // vrt = vrt + vrt1 + vrt2 + vrt3 + ... vrt15;
        for (i = 0; i < VectorWidth; ++i) {
            rt += vrt.GetElement(i);
        }
        return rt;
    }
#else
    throw new NotSupportedException();
#endif
}

2.5.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.095081967213115
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=157, MFLOPS/s=26089.171974522294, scale=31.452229299363058
SumVectorAvxPtrU16:     8.386202E+12    # msUsed=125, MFLOPS/s=32768, scale=39.504

SumVectorAvxPtrU16 對比基礎演算法(SumBase),性能倍數是 39.504,
SumVectorAvxPtrU16 對比未回圈展開的演算法(SumVectorAvxPtr),倍數是 39.504/8.095081967213115=4.8799999999999998517618469015796,
SumVectorAvxPtrU16 對比回圈展開4次的演算法(SumVectorAvxPtrU4),倍數是 39.504/31.452229299363058=1.2559999999999999730384771162414,

從回圈展開4次,改為回圈展開16次,性能倍數只是從 31倍多,提升到 39 倍多,僅提升 25% 左右,
性能提升的少,但編碼麻煩多了,看來回圈展開16次的性價比很低,故一般情況下用回圈展開4次就行了,

2.6 嘗試用陣列來存盤回圈展開的臨時變數

使用回圈展開N次時,將會導致臨時變數數量是非回圈展開版的N倍,例如剛才的 SumVectorAvxPtrU16 函式,因回圈展開16次,導致臨時變數是非回圈展開版的16倍,寫起了很啰嗦,
這些變數的型別是一樣的,放到陣列中的話,代碼會清晰不少,但會不會影響性能呢?
于是做了一個測驗,代碼如下:

private static float SumVectorAvxPtrU16A(float[] src, int count, int loops) {
#if Allow_Intrinsics && UNSAFE
    unsafe {
        float rt = 0; // Result.
        const int LoopUnrolling = 16;
        int VectorWidth = Vector256<float>.Count; // Block width.
        int nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        int i;
        //Vector256<float>[] vrt = new Vector256<float>[LoopUnrolling]; // Vector result.
        Vector256<float>* vrt = stackalloc Vector256<float>[LoopUnrolling]; ; // Vector result.
        for (i = 0; i< LoopUnrolling; ++i) {
            vrt[i] = Vector256<float>.Zero;
        }
        float* p; // Pointer for src data.
        // Body.
        fixed (float* p0 = &src[0]) {
            for (int j = 0; j < loops; ++j) {
                p = p0;
                // Vector processs.
                for (i = 0; i < cntBlock; ++i) {
                    //vload = Avx.LoadVector256(p);    // Load. vload = *(*__m256)p;
                    vrt[0] = Avx.Add(vrt[0], Avx.LoadVector256(p)); // Add. vrt[k] += *((*__m256)(p)+k);
                    vrt[1] = Avx.Add(vrt[1], Avx.LoadVector256(p + VectorWidth * 1));
                    vrt[2] = Avx.Add(vrt[2], Avx.LoadVector256(p + VectorWidth * 2));
                    vrt[3] = Avx.Add(vrt[3], Avx.LoadVector256(p + VectorWidth * 3));
                    vrt[4] = Avx.Add(vrt[4], Avx.LoadVector256(p + VectorWidth * 4));
                    vrt[5] = Avx.Add(vrt[5], Avx.LoadVector256(p + VectorWidth * 5));
                    vrt[6] = Avx.Add(vrt[6], Avx.LoadVector256(p + VectorWidth * 6));
                    vrt[7] = Avx.Add(vrt[7], Avx.LoadVector256(p + VectorWidth * 7));
                    vrt[8] = Avx.Add(vrt[8], Avx.LoadVector256(p + VectorWidth * 8));
                    vrt[9] = Avx.Add(vrt[9], Avx.LoadVector256(p + VectorWidth * 9));
                    vrt[10] = Avx.Add(vrt[10], Avx.LoadVector256(p + VectorWidth * 10));
                    vrt[11] = Avx.Add(vrt[11], Avx.LoadVector256(p + VectorWidth * 11));
                    vrt[12] = Avx.Add(vrt[12], Avx.LoadVector256(p + VectorWidth * 12));
                    vrt[13] = Avx.Add(vrt[13], Avx.LoadVector256(p + VectorWidth * 13));
                    vrt[14] = Avx.Add(vrt[14], Avx.LoadVector256(p + VectorWidth * 14));
                    vrt[15] = Avx.Add(vrt[15], Avx.LoadVector256(p + VectorWidth * 15));
                    p += nBlockWidth;
                }
                // Remainder processs.
                for (i = 0; i < cntRem; ++i) {
                    rt += p[i];
                }
            }
        }
        // Reduce.
        for (i = 1; i < LoopUnrolling; ++i) {
            vrt[0] = Avx.Add(vrt[0], vrt[i]); // vrt[0] += vrt[i]
        }
        for (i = 0; i < VectorWidth; ++i) {
            rt += vrt[0].GetElement(i);
        }
        return rt;
    }
#else
    throw new NotSupportedException();
#endif
}

2.6.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.095081967213115
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=157, MFLOPS/s=26089.171974522294, scale=31.452229299363058
SumVectorAvxPtrU16:     8.386202E+12    # msUsed=125, MFLOPS/s=32768, scale=39.504
SumVectorAvxPtrU16A:    8.3862026E+12   # msUsed=187, MFLOPS/s=21903.74331550802, scale=26.406417112299465

可以發現 SumVectorAvxPtrU16A 的性能比 SumVectorAvxPtrU16 差,
曾經以為是因為陣列是在堆中分配的(new Vector256)引起的,有堆記憶體分配的開銷,且需要多次尋址才能定位變數,
隨后改為堆疊中分配的陣列(stackalloc Vector256),且用最貼近硬體的指標來操作,可性能幾乎一致,故猜測可能是編譯優化時難以將它們優化為暫存器變數,

故在使用回圈展開時,臨時變數不要用陣列來存,還是逐個定義區域變數比較好,

2.7 嘗試用堆疊陣列來減少相關性

還嘗試了用堆疊陣列來減少相關性,代碼如下:

private static float SumVectorAvxPtrUX(float[] src, int count, int loops, int LoopUnrolling) {
#if Allow_Intrinsics && UNSAFE
    unsafe {
        float rt = 0; // Result.
        //const int LoopUnrolling = 16;
        if (LoopUnrolling <= 0) throw new ArgumentOutOfRangeException("LoopUnrolling", "Argument LoopUnrolling must >0 !");
        int VectorWidth = Vector256<float>.Count; // Block width.
        int nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        int i;
        //Vector256<float>[] vrt = new Vector256<float>[LoopUnrolling]; // Vector result.
        Vector256<float>* vrt = stackalloc Vector256<float>[LoopUnrolling]; ; // Vector result.
        for (i = 0; i < LoopUnrolling; ++i) {
            vrt[i] = Vector256<float>.Zero;
        }
        float* p; // Pointer for src data.
        // Body.
        fixed (float* p0 = &src[0]) {
            for (int j = 0; j < loops; ++j) {
                p = p0;
                // Vector processs.
                for (i = 0; i < cntBlock; ++i) {
                    for(int k=0; k< LoopUnrolling; ++k) {
                        vrt[k] = Avx.Add(vrt[k], Avx.LoadVector256(p + VectorWidth * k)); // Add. vrt[k] += *(*__m256)(p+k);
                    }
                    p += nBlockWidth;
                }
                // Remainder processs.
                for (i = 0; i < cntRem; ++i) {
                    rt += p[i];
                }
            }
        }
        // Reduce.
        for (i = 1; i < LoopUnrolling; ++i) {
            vrt[0] = Avx.Add(vrt[0], vrt[i]); // vrt[0] += vrt[i]
        } // vrt = vrt + vrt1 + vrt2 + vrt3 + ... vrt15;
        for (i = 0; i < VectorWidth; ++i) {
            rt += vrt[0].GetElement(i);
        }
        return rt;
    }
#else
    throw new NotSupportedException();
#endif
}

測驗代碼:

// SumVectorAvxPtrUX.
int[] LoopUnrollingArray = { 4, 8, 16 };
foreach (int loopUnrolling in LoopUnrollingArray) {
    tickBegin = Environment.TickCount;
    rt = SumVectorAvxPtrUX(src, count, loops, loopUnrolling);
    msUsed = Environment.TickCount - tickBegin;
    mFlops = countMFlops * 1000 / msUsed;
    scale = mFlops / mFlopsBase;
    tw.WriteLine(indent + string.Format("SumVectorAvxPtrUX[{4}]:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale, loopUnrolling));
}

2.7.1 測驗結果:

測驗結果摘錄如下:

SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.095081967213115
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=157, MFLOPS/s=26089.171974522294, scale=31.452229299363058
SumVectorAvxPtrU16:     8.386202E+12    # msUsed=125, MFLOPS/s=32768, scale=39.504
SumVectorAvxPtrU16A:    8.3862026E+12   # msUsed=187, MFLOPS/s=21903.74331550802, scale=26.406417112299465
SumVectorAvxPtrUX[4]:   2.1990233E+12   # msUsed=547, MFLOPS/s=7488.117001828154, scale=9.027422303473491
SumVectorAvxPtrUX[8]:   4.3980465E+12   # msUsed=500, MFLOPS/s=8192, scale=9.876
SumVectorAvxPtrUX[16]:  8.3862026E+12   # msUsed=500, MFLOPS/s=8192, scale=9.876

可以發現 SumVectorAvxPtrUX 版的性能比 非回圈展開版(SumVectorAvxPtr)的性能要好一些,從8倍左右,達到9倍,
調整堆疊陣列長度,達到8之后,性能幾乎差不多,看來已經達到瓶頸了,

該辦法的性能提升少,性價比不高,故還是推薦用經典的回圈展開辦法,

2.8 測驗結果匯總

測驗結果匯總如下:

BenchmarkVectorCore30

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:    3.1.26
RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.26\
RuntimeInformation.FrameworkDescription:        .NET Core 3.1.26
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/3.1.26/System.Numerics.Vectors.dll
Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/3.1.26/System.Private.CoreLib.dll

Benchmark:      count=4096, loops=1000000, countMFlops=4096
SumBase:        6.871948E+10    # msUsed=4938, MFLOPS/s=829.485621709194
SumBaseU4:      2.748779E+11    # msUsed=1875, MFLOPS/s=2184.5333333333333, scale=2.6336
SumVector4:     2.748779E+11    # msUsed=1218, MFLOPS/s=3362.8899835796387, scale=4.054187192118227
SumVector4U4:   1.0995116E+12   # msUsed=532, MFLOPS/s=7699.248120300752, scale=9.281954887218046
SumVectorT:     5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.108374384236454
SumVectorTU4:   2.1990233E+12   # msUsed=203, MFLOPS/s=20177.339901477833, scale=24.32512315270936
SumVectorAvx:   5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.108374384236454
SumVectorAvxSpan:       5.497558E+11    # msUsed=625, MFLOPS/s=6553.6, scale=7.9008
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.095081967213115
SumVectorAvxU4: 2.1990233E+12   # msUsed=328, MFLOPS/s=12487.80487804878, scale=15.054878048780488
SumVectorAvxSpanU4:     2.1990233E+12   # msUsed=312, MFLOPS/s=13128.205128205129, scale=15.826923076923078
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=157, MFLOPS/s=26089.171974522294, scale=31.452229299363058
SumVectorAvxPtrU16:     8.386202E+12    # msUsed=125, MFLOPS/s=32768, scale=39.504
SumVectorAvxPtrU16A:    8.3862026E+12   # msUsed=187, MFLOPS/s=21903.74331550802, scale=26.406417112299465
SumVectorAvxPtrUX[4]:   2.1990233E+12   # msUsed=547, MFLOPS/s=7488.117001828154, scale=9.027422303473491
SumVectorAvxPtrUX[8]:   4.3980465E+12   # msUsed=500, MFLOPS/s=8192, scale=9.876
SumVectorAvxPtrUX[16]:  8.3862026E+12   # msUsed=500, MFLOPS/s=8192, scale=9.876

三、在C++中使用

3.1 修改代碼

參考上面的經驗,現在來將 C++ 版程式也改為回圈展開的,
BenchmarkVectorCpp.cpp 的全部代碼如下:

// BenchmarkVectorCpp.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <immintrin.h>
#include <malloc.h>
#include <stdio.h>
#include <time.h>

#ifndef EXCEPTION_EXECUTE_HANDLER 
#define EXCEPTION_EXECUTE_HANDLER (1)
#endif // !EXCEPTION_EXECUTE_HANDLER 

// Sum - base.
float SumBase(const float* src, size_t count, int loops) {
    float rt = 0; // Result.
    size_t i;
    for (int j = 0; j < loops; ++j) {
        for (i = 0; i < count; ++i) {
            rt += src[i];
        }
    }
    return rt;
}

// Sum - base - Loop unrolling *4.
float SumBaseU4(const float* src, size_t count, int loops) {
    float rt = 0; // Result.
    float rt1=0;
    float rt2 = 0;
    float rt3 = 0;
    size_t nBlockWidth = 4; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    size_t p; // Index for src data.
    size_t i;
    for (int j = 0; j < loops; ++j) {
        p = 0;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            rt += src[p];
            rt1 += src[p + 1];
            rt2 += src[p + 2];
            rt3 += src[p + 3];
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += src[p + i];
        }
    }
    // Reduce.
    rt = rt + rt1 + rt2 + rt3;
    return rt;
}

// Sum - Vector AVX.
float SumVectorAvx(const float* src, size_t count, int loops) {
    float rt = 0; // Result.
    size_t VectorWidth = sizeof(__m256) / sizeof(float); // Block width.
    size_t nBlockWidth = VectorWidth; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    __m256 vrt = _mm256_setzero_ps(); // Vector result. [AVX] Set zero.
    __m256 vload; // Vector load.
    const float* p; // Pointer for src data.
    size_t i;
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = src;
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vload = _mm256_load_ps(p);    // Load. vload = *(*__m256)p;
            vrt = _mm256_add_ps(vrt, vload);    // Add. vrt += vload;
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += p[i];
        }
    }
    // Reduce.
    p = (const float*)&vrt;
    for (i = 0; i < VectorWidth; ++i) {
        rt += p[i];
    }
    return rt;
}

// Sum - Vector AVX - Loop unrolling *4.
float SumVectorAvxU4(const float* src, size_t count, int loops) {
    float rt = 0;    // Result.
    const int LoopUnrolling = 4;
    size_t VectorWidth = sizeof(__m256) / sizeof(float); // Block width.
    size_t nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    __m256 vrt = _mm256_setzero_ps(); // Vector result. [AVX] Set zero.
    __m256 vrt1 = _mm256_setzero_ps();
    __m256 vrt2 = _mm256_setzero_ps();
    __m256 vrt3 = _mm256_setzero_ps();
    __m256 vload; // Vector load.
    __m256 vload1, vload2, vload3;
    const float* p; // Pointer for src data.
    size_t i;
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = src;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            vload = _mm256_load_ps(p);    // Load. vload = *(*__m256)p;
            vload1 = _mm256_load_ps(p + VectorWidth * 1);
            vload2 = _mm256_load_ps(p + VectorWidth * 2);
            vload3 = _mm256_load_ps(p + VectorWidth * 3);
            vrt = _mm256_add_ps(vrt, vload);    // Add. vrt += vload;
            vrt1 = _mm256_add_ps(vrt1, vload1);
            vrt2 = _mm256_add_ps(vrt2, vload2);
            vrt3 = _mm256_add_ps(vrt3, vload3);
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += p[i];
        }
    }
    // Reduce.
    vrt = _mm256_add_ps(_mm256_add_ps(vrt, vrt1), _mm256_add_ps(vrt2, vrt3)); // vrt = vrt + vrt1 + vrt2 + vrt3;
    p = (const float*)&vrt;
    for (i = 0; i < VectorWidth; ++i) {
        rt += p[i];
    }
    return rt;
}

// Sum - Vector AVX - Loop unrolling *16.
float SumVectorAvxU16(const float* src, size_t count, int loops) {
    float rt = 0;    // Result.
    const int LoopUnrolling = 16;
    size_t VectorWidth = sizeof(__m256) / sizeof(float); // Block width.
    size_t nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    __m256 vrt = _mm256_setzero_ps(); // Vector result. [AVX] Set zero.
    __m256 vrt1 = _mm256_setzero_ps();
    __m256 vrt2 = _mm256_setzero_ps();
    __m256 vrt3 = _mm256_setzero_ps();
    __m256 vrt4 = _mm256_setzero_ps();
    __m256 vrt5 = _mm256_setzero_ps();
    __m256 vrt6 = _mm256_setzero_ps();
    __m256 vrt7 = _mm256_setzero_ps();
    __m256 vrt8 = _mm256_setzero_ps();
    __m256 vrt9 = _mm256_setzero_ps();
    __m256 vrt10 = _mm256_setzero_ps();
    __m256 vrt11 = _mm256_setzero_ps();
    __m256 vrt12 = _mm256_setzero_ps();
    __m256 vrt13 = _mm256_setzero_ps();
    __m256 vrt14 = _mm256_setzero_ps();
    __m256 vrt15 = _mm256_setzero_ps();
    const float* p; // Pointer for src data.
    size_t i;
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = src;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt = _mm256_add_ps(vrt, _mm256_load_ps(p));    // Add. vrt += *((*__m256)(p)+k);
            vrt1 = _mm256_add_ps(vrt1, _mm256_load_ps(p + VectorWidth * 1));
            vrt2 = _mm256_add_ps(vrt2, _mm256_load_ps(p + VectorWidth * 2));
            vrt3 = _mm256_add_ps(vrt3, _mm256_load_ps(p + VectorWidth * 3));
            vrt4 = _mm256_add_ps(vrt4, _mm256_load_ps(p + VectorWidth * 4));
            vrt5 = _mm256_add_ps(vrt5, _mm256_load_ps(p + VectorWidth * 5));
            vrt6 = _mm256_add_ps(vrt6, _mm256_load_ps(p + VectorWidth * 6));
            vrt7 = _mm256_add_ps(vrt7, _mm256_load_ps(p + VectorWidth * 7));
            vrt8 = _mm256_add_ps(vrt8, _mm256_load_ps(p + VectorWidth * 8));
            vrt9 = _mm256_add_ps(vrt9, _mm256_load_ps(p + VectorWidth * 9));
            vrt10 = _mm256_add_ps(vrt10, _mm256_load_ps(p + VectorWidth * 10));
            vrt11 = _mm256_add_ps(vrt11, _mm256_load_ps(p + VectorWidth * 11));
            vrt12 = _mm256_add_ps(vrt12, _mm256_load_ps(p + VectorWidth * 12));
            vrt13 = _mm256_add_ps(vrt13, _mm256_load_ps(p + VectorWidth * 13));
            vrt14 = _mm256_add_ps(vrt14, _mm256_load_ps(p + VectorWidth * 14));
            vrt15 = _mm256_add_ps(vrt15, _mm256_load_ps(p + VectorWidth * 15));
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += p[i];
        }
    }
    // Reduce.
    vrt = _mm256_add_ps(_mm256_add_ps(_mm256_add_ps(_mm256_add_ps(vrt, vrt1), _mm256_add_ps(vrt2, vrt3))
        , _mm256_add_ps(_mm256_add_ps(vrt4, vrt5), _mm256_add_ps(vrt6, vrt7)))
        , _mm256_add_ps(_mm256_add_ps(_mm256_add_ps(vrt8, vrt9), _mm256_add_ps(vrt10, vrt11))
        , _mm256_add_ps(_mm256_add_ps(vrt12, vrt13), _mm256_add_ps(vrt14, vrt15))))
    ; // vrt = vrt + vrt1 + vrt2 + vrt3 + ... vrt15;
    p = (const float*)&vrt;
    for (i = 0; i < VectorWidth; ++i) {
        rt += p[i];
    }
    return rt;
}

// Sum - Vector AVX - Loop unrolling *16 - Array.
float SumVectorAvxU16A(const float* src, size_t count, int loops) {
    float rt = 0;    // Result.
    const int LoopUnrolling = 16;
    size_t VectorWidth = sizeof(__m256) / sizeof(float); // Block width.
    size_t nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    size_t i;
    __m256 vrt[LoopUnrolling]; // Vector result.
    for (i = 0; i < LoopUnrolling; ++i) {
        vrt[i] = _mm256_setzero_ps(); // [AVX] Set zero.
    }
    const float* p; // Pointer for src data.
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = src;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt[0] = _mm256_add_ps(vrt[0], _mm256_load_ps(p));    // Add. vrt += *((*__m256)(p)+k);
            vrt[1] = _mm256_add_ps(vrt[1], _mm256_load_ps(p + VectorWidth * 1));
            vrt[2] = _mm256_add_ps(vrt[2], _mm256_load_ps(p + VectorWidth * 2));
            vrt[3] = _mm256_add_ps(vrt[3], _mm256_load_ps(p + VectorWidth * 3));
            vrt[4] = _mm256_add_ps(vrt[4], _mm256_load_ps(p + VectorWidth * 4));
            vrt[5] = _mm256_add_ps(vrt[5], _mm256_load_ps(p + VectorWidth * 5));
            vrt[6] = _mm256_add_ps(vrt[6], _mm256_load_ps(p + VectorWidth * 6));
            vrt[7] = _mm256_add_ps(vrt[7], _mm256_load_ps(p + VectorWidth * 7));
            vrt[8] = _mm256_add_ps(vrt[8], _mm256_load_ps(p + VectorWidth * 8));
            vrt[9] = _mm256_add_ps(vrt[9], _mm256_load_ps(p + VectorWidth * 9));
            vrt[10] = _mm256_add_ps(vrt[10], _mm256_load_ps(p + VectorWidth * 10));
            vrt[11] = _mm256_add_ps(vrt[11], _mm256_load_ps(p + VectorWidth * 11));
            vrt[12] = _mm256_add_ps(vrt[12], _mm256_load_ps(p + VectorWidth * 12));
            vrt[13] = _mm256_add_ps(vrt[13], _mm256_load_ps(p + VectorWidth * 13));
            vrt[14] = _mm256_add_ps(vrt[14], _mm256_load_ps(p + VectorWidth * 14));
            vrt[15] = _mm256_add_ps(vrt[15], _mm256_load_ps(p + VectorWidth * 15));
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += p[i];
        }
    }
    // Reduce.
    vrt[0] = _mm256_add_ps(_mm256_add_ps(_mm256_add_ps(_mm256_add_ps(vrt[0], vrt[1]), _mm256_add_ps(vrt[2], vrt[3]))
        , _mm256_add_ps(_mm256_add_ps(vrt[4], vrt[5]), _mm256_add_ps(vrt[6], vrt[7])))
        , _mm256_add_ps(_mm256_add_ps(_mm256_add_ps(vrt[8], vrt[9]), _mm256_add_ps(vrt[10], vrt[11]))
        , _mm256_add_ps(_mm256_add_ps(vrt[12], vrt[13]), _mm256_add_ps(vrt[14], vrt[15]))))
    ; // vrt = vrt + vrt1 + vrt2 + vrt3 + ... vrt15;
    p = (const float*)&vrt;
    for (i = 0; i < VectorWidth; ++i) {
        rt += p[i];
    }
    return rt;
}

// Sum - Vector AVX - Loop unrolling *X - Array.
float SumVectorAvxUX(const float* src, size_t count, int loops, const int LoopUnrolling) {
    float rt = 0;    // Result.
    size_t VectorWidth = sizeof(__m256) / sizeof(float); // Block width.
    size_t nBlockWidth = VectorWidth * LoopUnrolling; // Block width.
    size_t cntBlock = count / nBlockWidth; // Block count.
    size_t cntRem = count % nBlockWidth; // Remainder count.
    size_t i;
    __m256* vrt = new __m256[LoopUnrolling]; // Vector result.
    for (i = 0; i < LoopUnrolling; ++i) {
        vrt[i] = _mm256_setzero_ps(); // [AVX] Set zero.
    }
    const float* p; // Pointer for src data.
    // Body.
    for (int j = 0; j < loops; ++j) {
        p = src;
        // Block processs.
        for (i = 0; i < cntBlock; ++i) {
            for (int k = 0; k < LoopUnrolling; ++k) {
                vrt[k] = _mm256_add_ps(vrt[k], _mm256_load_ps(p + VectorWidth * k));    // Add. vrt += *((*__m256)(p)+k);
            }
            p += nBlockWidth;
        }
        // Remainder processs.
        for (i = 0; i < cntRem; ++i) {
            rt += p[i];
        }
    }
    // Reduce.
    for (i = 1; i < LoopUnrolling; ++i) {
        vrt[0] = _mm256_add_ps(vrt[0], vrt[i]); // vrt[0] += vrt[i]
    } // vrt = vrt + vrt1 + vrt2 + vrt3 + ... vrt15;
    p = (const float*)&vrt[0];
    for (i = 0; i < VectorWidth; ++i) {
        rt += p[i];
    }
    delete[] vrt;
    return rt;
}

// Do Benchmark.
void Benchmark() {
    const size_t alignment = 256 / 8; // sizeof(__m256) / sizeof(BYTE);
    // init.
    clock_t 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 = https://www.cnblogs.com/zyl910/archive/2022/11/16/(float*)_aligned_malloc(sizeof(float)*count, alignment); // new float[count];
    if (NULL == src) {
        printf("Memory alloc fail!");
        return;
    }
    for (int i = 0; i < count; ++i) {
        src[i] = (float)i;
    }
    printf("Benchmark: \tcount=%d, loops=%d, countMFlops=%f\n", count, loops, countMFlops);
    // SumBase.
    tickBegin = clock();
    rt = SumBase(src, count, loops);
    msUsed = clock() - tickBegin;
    mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
    printf("SumBase:\t%g\t# msUsed=%d, MFLOPS/s=%f\n", rt, (int)msUsed, mFlops);
    double mFlopsBase = mFlops;
    // SumBaseU4.
    tickBegin = clock();
    rt = SumBaseU4(src, count, loops);
    msUsed = clock() - tickBegin;
    mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
    scale = mFlops / mFlopsBase;
    printf("SumBaseU4:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", rt, (int)msUsed, mFlops, scale);
    // SumVectorAvx.
    __try {
        tickBegin = clock();
        rt = SumVectorAvx(src, count, loops);
        msUsed = clock() - tickBegin;
        mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
        scale = mFlops / mFlopsBase;
        printf("SumVectorAvx:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", rt, (int)msUsed, mFlops, scale);
        // SumVectorAvxU4.
        tickBegin = clock();
        rt = SumVectorAvxU4(src, count, loops);
        msUsed = clock() - tickBegin;
        mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
        scale = mFlops / mFlopsBase;
        printf("SumVectorAvxU4:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", rt, (int)msUsed, mFlops, scale);
        // SumVectorAvxU16.
        tickBegin = clock();
        rt = SumVectorAvxU16(src, count, loops);
        msUsed = clock() - tickBegin;
        mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
        scale = mFlops / mFlopsBase;
        printf("SumVectorAvxU16:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", rt, (int)msUsed, mFlops, scale);
        // SumVectorAvxU16A.
        tickBegin = clock();
        rt = SumVectorAvxU16A(src, count, loops);
        msUsed = clock() - tickBegin;
        mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
        scale = mFlops / mFlopsBase;
        printf("SumVectorAvxU16A:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", rt, (int)msUsed, mFlops, scale);
        // SumVectorAvxUX.
        int LoopUnrollingArray[] = { 4, 8, 16 };
        for (int i = 0; i < sizeof(LoopUnrollingArray) / sizeof(LoopUnrollingArray[0]); ++i) {
            int loopUnrolling = LoopUnrollingArray[i];
            tickBegin = clock();
            rt = SumVectorAvxUX(src, count, loops, loopUnrolling);
            msUsed = clock() - tickBegin;
            mFlops = countMFlops * CLOCKS_PER_SEC / msUsed;
            scale = mFlops / mFlopsBase;
            printf("SumVectorAvxUX[%d]:\t%g\t# msUsed=%d, MFLOPS/s=%f, scale=%f\n", loopUnrolling, rt, (int)msUsed, mFlops, scale);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        printf("Run SumVectorAvx fail!");
    }
    // done.
    _aligned_free(src);
}

int main() {
    printf("BenchmarkVectorCpp\n");
    printf("\n");
    printf("Pointer size:\t%d\n", (int)(sizeof(void*)));
#ifdef _DEBUG
    printf("IsRelease:\tFalse\n");
#else
    printf("IsRelease:\tTrue\n");
#endif // _DEBUG
#ifdef _MSC_VER
    printf("_MSC_VER:\t%d\n", _MSC_VER);
#endif // _MSC_VER
#ifdef __AVX__
    printf("__AVX__:\t%d\n", __AVX__);
#endif // __AVX__
    printf("\n");
    // Benchmark.
    Benchmark();
}

3.2 測驗結果

測驗結果匯總如下:

BenchmarkVectorCpp

Pointer size:   8
IsRelease:      True
_MSC_VER:       1916
__AVX__:        1

Benchmark:      count=4096, loops=1000000, countMFlops=4096.000000
SumBase:        6.87195e+10     # msUsed=4938, MFLOPS/s=829.485622
SumBaseU4:      2.74878e+11     # msUsed=1229, MFLOPS/s=3332.790887, scale=4.017901
SumVectorAvx:   5.49756e+11     # msUsed=616, MFLOPS/s=6649.350649, scale=8.016234
SumVectorAvxU4: 2.19902e+12     # msUsed=247, MFLOPS/s=16582.995951, scale=19.991903
SumVectorAvxU16:        8.3862e+12      # msUsed=89, MFLOPS/s=46022.471910, scale=55.483146
SumVectorAvxU16A:       8.3862e+12      # msUsed=89, MFLOPS/s=46022.471910, scale=55.483146
SumVectorAvxUX[4]:      2.19902e+12     # msUsed=465, MFLOPS/s=8808.602151, scale=10.619355
SumVectorAvxUX[8]:      4.39805e+12     # msUsed=336, MFLOPS/s=12190.476190, scale=14.696429
SumVectorAvxUX[16]:     8.3862e+12      # msUsed=323, MFLOPS/s=12681.114551, scale=15.287926

先前做未回圈展開時,C# 與 Visual C++ 程式的性能差距不大,而現在使用回圈展開后,發現差距拉大了——

  • SumBaseU4:C++版的MFLOPS/s為 3332.790887,C#版的MFLOPS/s為 2184.5333333333333,3332.790887/2184.5333333333333=1.5256305940246582264042754215188,即大約是 1.5256 倍,
  • SumVectorAvxU4:C++版的MFLOPS/s為 16582.995951,C#版的MFLOPS/s為 12487.80487804878,16582.995951/12487.80487804878=1.3279352226386719268724696343231,即大約是 1.3279 倍,
  • SumVectorAvxU16:C++版的MFLOPS/s為 46022.471910,C#版的MFLOPS/s為 32768,46022.471910/32768=1.40449438201904296875,即大約是 1.4045 倍,

而且還發現——

  • SumVectorAvxU16A 與 SumVectorAvxU16 的性能差不多,表示C++編譯器能很好地優化陣列訪問,能達到區域變數同級別的速度,故C++中可以放心使用陣列來存盤回圈展開的臨時變數,
  • SumVectorAvxUX 的 C++ 版性能比C#好一些,而且臨時陣列長度大于8時,也能帶來一定的性能提升,只可惜還是存在性價比不高的問題,還是經典的回圈展開更好用,

四、小結

C#中使用回圈展開的心得總結——

  • 使用回圈展開能提高性能,但由于編碼比較麻煩,且會增加代碼維護的成本,故應作為最后手段,應該先嘗試其他優化手段,
  • 使用回圈展開后,指標版代碼比陣列版、Span版要高一些,可能是因為指標更貼近底層硬體、更易于編譯器優化,故推薦優先使用指標,
  • 在C#中做回圈展開,一般展開4次就行,展開16次只有少量提升,性價比不高,
  • 回圈展開會引起臨時變數倍增,應該堅持逐個定義區域變數,若用陣列會造成性能下降,包括用指標操作堆疊分配陣列也會下降,

對于回圈展開這樣的運算量重的情況,Visual C++編譯優化的更好,但性能大多只有1倍多,總體差距不大,
C++還有個優點,是可以用陣列來存盤回圈展開的臨時變數,且性能不會下降,

原始碼地址——
https://github.com/zyl910/BenchmarkVector/tree/main/BenchmarkVector

參考文獻

  • zyl910《C# 使用SIMD向量型別加速浮點陣列求和運算(1):使用Vector4、Vector<T>》. https://www.cnblogs.com/zyl910/p/dotnet_simd_BenchmarkVector1.html
  • zyl910《C# 使用SIMD向量型別加速浮點陣列求和運算(2):C#通過Intrinsic直接使用AVX指令集操作 Vector256<T>,及C++程式對比》. https://www.cnblogs.com/zyl910/p/dotnet_simd_BenchmarkVector2.html
  • 有色金屬?《C 語言性能優化:回圈展開》. https://zhuanlan.zhihu.com/p/359032198
作者:zyl910 出處:http://www.cnblogs.com/zyl910/ 著作權宣告:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0.

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/535110.html

標籤:.NET技术

上一篇:aspnetcore兩種上傳圖片(檔案)的方式

下一篇:WPF-CommandBinding到F#庫中定義的命令不起作用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more