使用SIMD加速.NET 應用程式

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的概念在CPU上多半是以新指令來提供,很難窺知其實際的做法,只能說透過這些新指令能達到近似平行的效果,但內部流程就很難確認。

 

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; //get simd register length
       var vmin = new Vector<int>(int.MaxValue);
       var vmax = new Vector<int>(int.MinValue);

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

       Console.WriteLine($" vmin {vmin[0]}, vmax {vmax[0]}");
}


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

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

            Console.WriteLine($" vmin {vmin[0]}, vmax {vmax[0]}");
        }


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