[C#] .NET 8 vs .NET 9 SIMD 加速實測:一般迴圈是否真的能自動向量化

  • 60
  • 0

最近看到一個名詞 SIMD ( Single Instruction, Multiple Data ) ,簡單一句話就是 讓 CPU 以 "一條指令同時處理多筆資料" 的平行運算技術

自 .NET Core 開始,.NET 就在 JIT 編譯器中支援 SIMD,而 .NET 9 更進一步提供了更完整的自動向量化(Auto-Vectorization)能力

使部分迴圈在不修改程式碼的情況下就能獲得向量化的效益,也就是單純迴圈也可以做到 SIMD 的效果?

這邊我只好安裝 .Net 8 跟 .Net9 的環境來進行測試,又是一個燒錢又燒命的測試

程式碼 - 以下為本次測試所使用的加總方法:一個傳統迴圈版本,以及一個使用 Vector<float> 的 SIMD 版本

傳統加總:

		
        static float Sum(float[] values)
        {
            float total = 0;
            for (int i = 0; i < values.Length; i++)
                total += values[i];
            return total;
        }
        

使用 SIMD 加總

       
       
       
       /// <summary>
       /// SIMD 向量化加總
       /// </summary>
       static float SumSimd(float[] data)
       {
           var vectorSize = Vector<float>.Count;
           var i = 0;
           var vsum = Vector<float>.Zero;

           // 每次處理 vectorSize 筆
           for (; i <= data.Length - vectorSize; i += vectorSize)
           {
               var v = new Vector<float>(data, i);
               vsum += v;
           }

           // 把向量的各元素加起來
           var  total = 0f;
           for (int j = 0; j < vectorSize; j++)
               total += vsum[j];

           // 處理尾端剩餘元素
           for (; i < data.Length; i++)
               total += data[i];

           return total;
       }
       
       

執行程式碼:

 
            for (var i = 1; i <= 10; i++)
            {
                var faker = new Faker("en");

                var data = Enumerable.Range(1, 100_000_000)
                                 .Select(_ => faker.Random.Float(0, 10))
                                 .ToArray();

                var sw = new Stopwatch();
                sw.Start();

                Sum(data);
                Console.WriteLine(sw.Elapsed);

            }

            Console.WriteLine("--- SIMD ---");
            for (var i = 1; i <= 5; i++)
            {
                var faker = new Faker("en");

                var data = Enumerable.Range(1, 100_000_000)
                                 .Select(_ => faker.Random.Float(0, 10))
                                 .ToArray();

                var sw = new Stopwatch();
                sw.Start();

                SumSimd(data);
                Console.WriteLine(sw.Elapsed);

            }

比較結果:

//.Net 8

00:00:00.4105529
00:00:00.3609744
00:00:00.3352597
00:00:00.4820423
00:00:00.4234173
00:00:00.3615139
00:00:00.3748291
00:00:00.3548129
00:00:00.3566374
00:00:00.3295502
--- SIMD ---
00:00:00.1451067
00:00:00.1049529
00:00:00.1072769
00:00:00.1048805
00:00:00.0915223
//.Net9
     
00:00:00.4156275
00:00:00.3431862
00:00:00.4404494
00:00:00.3697095
00:00:00.3433034
00:00:00.3715118
00:00:00.3673285
00:00:00.3541178
00:00:00.3897063
00:00:00.3924788
--- SIMD ---
00:00:00.1194425
00:00:00.1099284
00:00:00.0910654
00:00:00.0905410
00:00:00.1233741

結論 - 即使差異不大,若大量計算還是要用 Vector

以下為本次測試所使用的加總方法:一個傳統迴圈版本,以及一個使用 Vector<float> 的 SIMD 版本

在 .NET 8 上,SIMD 版本的速度遠快於一般迴圈,符合預期

在 .NET 9 上,SIMD 版本仍然具有明顯優勢,而一般迴圈的表現大致與 .NET 8 相近

.NET 9 引入了更多自動向量化(Auto-Vectorization)增強,理論上在某些模式下,傳統迴圈也可能被自動轉換成 SIMD 指令。然而,是否能被向量化,取決於 JIT 的分析能力以及迴圈的結構。此案例中是一個典型的加總迴圈,但因為內容非常簡單、每次迭代都需做一次相依性的累加(total += value),JIT 對這種類型的 sum-reduction 迴圈目前仍無法完全向量化,因此一般迴圈在 .NET 9 的執行時間與 .NET 8 差異不大,屬於正常現象。

反之,手動向量化(使用 Vector<T>)能強制使用 SIMD 指令,因此仍然能得到明顯的加速效果

而且我在MSIL 看來在 .NET8 跟 .NET9 其中 SUM() 是一樣的程式碼,看來這是在 runtime 時期的優化

-

本文原文首發於我的個人部落格:.NET 8 vs .NET 9 SIMD 加速實測:一般迴圈是否真的能自動向量化

-

參考資料:

https://learn.microsoft.com/zh-tw/dotnet/core/whats-new/dotnet-9/overview#:~:text=SIMD


 

 

---

Yesterday I wrote down the code. I bet I could be your hero. I am a mighty little programmer.