SIMD是Single Instruction Multiple Data的縮寫,通常中文翻譯為單指令多數據流,用較白話的說法是,同時對多個數據執行同一條CPU指令,達到平行運算的目的。
What’s SIMD?
SIMD是Single Instruction Multiple Data的縮寫,通常中文翻譯為單指令多數據流,用較白話的說法是,同時對多個數據執行同一條CPU指令,達到平行運算的目的。
在GPU還沒蓬勃發展前,CPU運用這個技術來增加圖形運算的速度,例如Intel的MMX、SSE、SSE2,AMD 的3D Now!等都是使用SIMD為基礎概念,在GPU技術突飛猛進的今天,CPU的SIMD技術反而比較少用在圖形運算了,而轉往資料庫或是其他用途上。
從概念上來說,SIMD通常伴隨著特別的CPU指令和暫存器,相較於傳統的16、32、64bit大小的暫存器,SIMD專用的暫存器多半是128bit或是256bit,當SIMD控制器收到指令後,會指定一個暫存器及數個定位的位址,發送給數個PE(Processor Execution unit),也就是說這些PE將同時對單一暫存器執行同一條指令,由於給的位址不同,所以各個PE回傳的結果也就不同,達到平行運算的目的。
用更簡單的說法,你可以想像128bit可以切割為16 Byte,可切割為4個4 Byte的Int32區段,搭配4個PE,一條指令就能同時運算4個Int32,如果需要運算10000個Int32,切成4等份後,SIMD可以用2500條指令處理完,而不是10000。要特別注意的是PE與Core是不同的概念,一個Core可以有多個PE,因此SIMD跟多執行緒是完全不同的概念。另外,視乎執行的指令及資料而定,效能的增加幅度也不同。
SIMD and .NET Framework、JIT
.NET Framework 4.6開始引進SIMD支援,目前僅作用於x64的應用程式,透過另外安裝的NuGet Package,所有的.NET 4.6應用程式都可以使用SIMD改寫部分邏輯來加速應用程式,當然,前提是CPU要支援,所幸現今的CPU多半都具備SIMD能力。
Using SIMD in C#
在C# 中使用SIMD並不難,首先必須要切換專案為x64模式,接著確定.NET Framework為4.6以上版本,然後安裝需要的System.Numerics.Vector Package。
底下是我們用來測試SIMD效果的範例,這個範例的目的很簡單,就是找最大值跟最小值,一開始先用傳統的版本。
static List<int> GenerateData()
{
List<int> data = new List<int>();
Random r = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);
data.Add(15);
for (int i = 0; i < 10000000; i++)
data.Add(r.Next(20, 60000));
return data;
}
static void NonSIMDMinMax(int[] data)
{
var min = int.MaxValue;
var max = int.MinValue;
foreach (var value in data)
{
min = Math.Min(min, value);
max = Math.Max(max, value);
}
Console.WriteLine($" vmin {min}, vmax {max}");
}
static void Main(string[] args)
{
Console.WriteLine("gernate data....");
var data = GenerateData();
Stopwatch sw = new Stopwatch();
Console.WriteLine("go find...(Non SIMD)");
sw.Reset();
sw.Start();
NonSIMDMinMax(data.ToArray());
sw.Stop();
Console.WriteLine($" use times {sw.ElapsedMilliseconds}");
Console.Read();
}
執行結果如下。
gernate data....
go find...(Non SIMD)
vmin 15, vmax 59999
use times 74
接著加入SIMD版本。
…………………………….
static void SIMDMinMax(int[] data)
{
var simdLength = Vector<int>.Count;
var vmin = new Vector<int>(int.MaxValue);
var vmax = new Vector<int>(int.MinValue);
int currentMin = 0, currentMax = 0;
int[] tempArray = new int[simdLength];
for (int i = 0; i <= data.Length - simdLength; i += simdLength)
{
var va = new Vector<int>(data, i);
vmin = Vector.Min(va, vmin);
vmax = Vector.Max(va, vmax);
}
vmin.CopyTo(tempArray);
currentMin = tempArray.Min();
vmax.CopyTo(tempArray);
currentMax = tempArray.Max();
for (int i = data.Length - data.Length % 8; i < data.Length; i++)
{
if (data[i] < currentMin)
currentMin = data[i];
if (data[i] > currentMax)
currentMax = data[i];
}
Console.WriteLine($" vmin {currentMin}, vmax {currentMax}");
}
static void Main(string[] args)
{
Console.WriteLine("gernate data....");
var data = GenerateData();
Stopwatch sw = new Stopwatch();
Console.WriteLine("go find...(Non SIMD)");
sw.Reset();
sw.Start();
NonSIMDMinMax(data.ToArray());
sw.Stop();
Console.WriteLine($" use times {sw.ElapsedMilliseconds}");
Console.WriteLine($"go find...(SIMD : {Vector.IsHardwareAccelerated})");
sw.Reset();
sw.Start();
SIMDMinMax(data.ToArray());
sw.Stop();
Console.WriteLine($" use times {sw.ElapsedMilliseconds}");
Console.Read();
}
執行結果如下。
gernate data....
go find...(Non SIMD)
vmin 15, vmax 59999
use times 103
go find...(SIMD : True)
vmin 15, vmax 59999
use times 39
很有趣是吧?不過要特別注意,這與CPU有很緊密的關係,如果SIMD在你的CPU上不能使用,那麼使用Vector反而會變得更慢,因此要先利用Vector. IsHardwareAccelerated來判斷SIMD是否可用。有興趣的話可以把int改成short,你會發現速度會變得更快,這是因為暫存器可切割為更多單元,能用到更多PE。Vector也不只可以用來做Min、Max,比對及資料運算等操作都可以透過Vector的靜態函式完成,或是使用Vector2、Vector3、Vector4 來使用多維運算。
下面是本文範例完整的程式碼。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
static List<int> GenerateData()
{
List<int> data = new List<int>();
Random r = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);
data.Add(15);
for (int i = 0; i < 10000000; i++)
data.Add(r.Next(20, 60000));
return data;
}
static void NonSIMDMinMax(int[] data)
{
var min = int.MaxValue;
var max = int.MinValue;
foreach (var value in data)
{
min = Math.Min(min, value);
max = Math.Max(max, value);
}
Console.WriteLine($" vmin {min}, vmax {max}");
}
static void SIMDMinMax(int[] data)
{
var simdLength = Vector<int>.Count;
var vmin = new Vector<int>(int.MaxValue);
var vmax = new Vector<int>(int.MinValue);
int currentMin = 0, currentMax = 0;
int[] tempArray = new int[simdLength];
for (int i = 0; i <= data.Length - simdLength; i += simdLength)
{
var va = new Vector<int>(data, i);
vmin = Vector.Min(va, vmin);
vmax = Vector.Max(va, vmax);
}
vmin.CopyTo(tempArray);
currentMin = tempArray.Min();
vmax.CopyTo(tempArray);
currentMax = tempArray.Max();
for (int i = data.Length - data.Length % 8; i < data.Length; i++)
{
if (data[i] < currentMin)
currentMin = data[i];
if (data[i] > currentMax)
currentMax = data[i];
}
Console.WriteLine($" vmin {currentMin}, vmax {currentMax}");
}
static void Main(string[] args)
{
Console.WriteLine("gernate data....");
var data = GenerateData();
Stopwatch sw = new Stopwatch();
Console.WriteLine("go find...(Non SIMD)");
sw.Reset();
sw.Start();
NonSIMDMinMax(data.ToArray());
sw.Stop();
Console.WriteLine($" use times {sw.ElapsedMilliseconds}");
Console.WriteLine($"go find...(SIMD : {Vector.IsHardwareAccelerated})");
sw.Reset();
sw.Start();
SIMDMinMax(data.ToArray());
sw.Stop();
Console.WriteLine($" use times {sw.ElapsedMilliseconds}");
Console.Read();
}
}
}