最近看到一個名詞 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 時期的優化
參考資料:
https://learn.microsoft.com/zh-tw/dotnet/core/whats-new/dotnet-9/overview#:~:text=SIMD