.Net7矢量化的性能優化

前言

矢量化是性能優化的重要技術,也是寄託硬件層面的優化技術。本篇來看下。文章來源:微軟官方博客


概括

一:矢量化支持的問題:
矢量化的System.Runtime.Intrinsics.X86.Sse2.MoveMask
函數和矢量化的Vector128.Create().ExtractMostSignificantBits()
函數返回的結果是一樣的。但是前者只能在支持SSE2的128位矢量化平臺上工作,而後者可以在任何支持128位矢量化平臺上工作,包括Risc-V,Arm64,WASM等平臺。這裏以一段代碼看下:

private static void Main()
{
   Vector128<byte> v = Vector128.Create((byte)123);
   while (true)
   {
      WithIntrinsics(v);
      WithVector(v);
      break;
   }
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static int WithIntrinsics(Vector128<byte> v) => Sse2.MoveMask(v);
[MethodImpl(MethodImplOptions.NoInlining)]
private static uint WithVector(Vector128<byte> v) => v.ExtractMostSignificantBits();

看下它的ASM代碼:

WithIntrinsics:
G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       C5F877               vzeroupper
       488BEC               mov      rbp, rsp
       48894D10             mov      bword ptr [rbp+10H], rcx

G_M000_IG02:                ;; offset=000BH
       488B4510             mov      rax, bword ptr [rbp+10H]
       C5F91000             vmovupd  xmm0, xmmword ptr [rax]
       C5F9D7C0             vpmovmskb eax, xmm0

G_M000_IG03:                ;; offset=0017H
       5D                   pop      rbp
       C3                   ret


WithVector
G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       C5F877               vzeroupper
       488BEC               mov      rbp, rsp
       48894D10             mov      bword ptr [rbp+10H], rcx

G_M000_IG02:                ;; offset=000BH
       488B4510             mov      rax, bword ptr [rbp+10H]
       C5F91000             vmovupd  xmm0, xmmword ptr [rax]
       C5F9D7C0             vpmovmskb eax, xmm0

G_M000_IG03:                ;; offset=0017H
       5D                   pop      rbp
       C3                   ret

可以看到這兩個函數生成的ASM幾乎一模一樣。

2.矢量化的一個例子
由於以上代碼體現的SSE2的侷限性,所以需要把一些代碼矢量化,以便在任何平臺上運行,這裏看一個例子。

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
    for (int i = 0; i < haystack.Length; i++)
    {
        if (haystack[i] == needle)
        {
            return true;
        }
    }

    return false;
}

查找元素,找到了然後返回。怎麼對這例子進行矢量化呢?首先需要判斷你代碼運行的硬件是否支持矢量化,可以通過Vector.IsHardwareAccelerated的返回值來判斷。其次,傳入的變量長度(haystack.length)必須的大於一個向量的長度(Vector.Count,win11加VS2022這個值是32)。那麼改造之後如下:

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
    if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)
    {
        // ...
    }
    else
    {
        for (int i = 0; i < haystack.Length; i++)
        {
            if (haystack[i] == needle)
            {
                return true;
            }
        }
    }
    return false;
}

如果以上if的兩個判斷均爲true的話,那麼我們進入矢量化階段。代碼如下:

static unsafe bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
    if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)//判斷當前運行的硬件是否符合矢量化以及變量的長度不能小於矢量化裏面一個向量的長度。
    {
        fixed (byte* haystackPtr = &MemoryMarshal.GetReference(haystack))//獲取變量的頭指針
        {
            Vector<byte> target = new Vector<byte>(needle);//向量化需要查找的變量needle
            byte* current = haystackPtr;//變量haystack的頭指針,以便於後面循環
            byte* endMinusOneVector = haystackPtr + haystack.Length - Vector<byte>.Count;//頭指針+變量的長度減去一個向量的長度。同頭指針current開始到endMinusOneVector在這個裏面遍歷循環,查找需要查找的變量target也就是向量化的needle,這裏爲什麼要進去Vector<byte>.Count因爲向量是從0開始查找的。
            do
            {
                if (Vector.EqualsAny(target, *(Vector<byte>*)current))//判斷當前的指針是否與需要查找的變量相等
                {
                    return true;//相等就返回true
                }

                current += Vector<byte>.Count;//不相等指針就位移到下一個向量,繼續遍歷循環。
            }
            while (current < endMinusOneVector);//這裏判斷是否達到循環終點。
        }
    }
    else
    {
        for (int i = 0; i < haystack.Length; i++)
        {
            if (haystack[i] == needle)
            {
                return true;
            }
        }
    }
    return false;
}

以上代碼幾乎完成了90%,但是依然有點點問題。那就是最後一個向量endMinusOneVector沒有被查找。所以還需要加上它的查找。最後的點如下,第一個Contains是不矢量化的,第二個Contains_Vector是矢量化之後的。

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
    for (int i = 0; i < haystack.Length; i++)
    {
        if (haystack[i] == needle)
        {
            return true;
        }
    }

    return false;
}
static unsafe bool Contains_Vector(ReadOnlySpan<byte> haystack, byte needle)
{
    if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)
    {
        fixed (byte* haystackPtr = &MemoryMarshal.GetReference(haystack))
        {
            Vector<byte> target = new Vector<byte>(needle);
            byte* current = haystackPtr;
            byte* endMinusOneVector = haystackPtr + haystack.Length - Vector<byte>.Count;
            do
            {
                if (Vector.EqualsAny(target, *(Vector<byte>*)current))
                {
                    return true;
                }

                current += Vector<byte>.Count;
            }
            while (current < endMinusOneVector);

            if (Vector.EqualsAny(target, *(Vector<byte>*)endMinusOneVector))
            {
                return true;
            }
        }
    }
    else
    {
        for (int i = 0; i < haystack.Length; i++)
        {
            if (haystack[i] == needle)
            {
                return true;
            }
        }
    }

    return false;
}

上面的代碼幾乎是完美的,測試下基準

private byte[] _data = Enumerable.Repeat((byte)123, 999).Append((byte)42).ToArray();//Enumerable.Repeat表示999個123的byte,放在數組,最後又加了一個42數值到數組

[Benchmark(Baseline = true)]
[Arguments((byte)42)]
public bool Find(byte value) => Contains(_data, value); // just the fallback path in its own method

[Benchmark]
[Arguments((byte)42)]
public bool FindVectorized(byte value) => Contains_Vectorized(_data, value); // the implementation we just wrote



|         Method | value |      Mean |    Error |   StdDev | Ratio | Code Size |
|--------------- |------ |----------:|---------:|---------:|------:|----------:|
|           Find |    42 | 508.42 ns | 2.336 ns | 2.185 ns |  1.00 |     110 B |
| FindVectorized |    42 |  21.57 ns | 0.342 ns | 0.303 ns |  0.04 |     253 B |

可以看到矢量化之後的性能,進行了誇張的25倍的增長。這段代碼幾乎完美,但是並不完美。這裏是用的1000個元素測試,如果是小於30個元素呢?有兩個方法,第一個是退回到沒有矢量化的代碼也就是Contains函數,第二個是把Vector切換到128位來操作。代碼如下,幾乎沒變更:

static unsafe bool Contains128(ReadOnlySpan<byte> haystack, byte needle)
{
    if (Vector128.IsHardwareAccelerated && haystack.Length >= Vector128<byte>.Count)
    {
        ref byte current = ref MemoryMarshal.GetReference(haystack);

        Vector128<byte> target = Vector128.Create(needle);
        ref byte endMinusOneVector = ref Unsafe.Add(ref current, haystack.Length - Vector128<byte>.Count);
        do
        {
            if (Vector128.EqualsAny(target, Vector128.LoadUnsafe(ref current)))
            {
                return true;
            }

            current = ref Unsafe.Add(ref current, Vector128<byte>.Count);
        }
        while (Unsafe.IsAddressLessThan(ref current, ref endMinusOneVector));

        if (Vector128.EqualsAny(target, Vector128.LoadUnsafe(ref endMinusOneVector)))
        {
            return true;
        }
    }
    else
    {
        for (int i = 0; i < haystack.Length; i++)
        {
            if (haystack[i] == needle)
            {
                return true;
            }
        }
    }

    return false;
}

來進行一個基準測試:

private byte[] _data = Enumerable.Repeat((byte)123, 29).Append((byte)42).ToArray();

[Benchmark(Baseline = true)]
[Arguments((byte)42)]
public bool Find(byte value) => Contains(_data, value);

[Benchmark]
[Arguments((byte)42)]
public bool FindVectorized(byte value) => Contains_Vectorized(_data, value);


|         Method | value |      Mean |     Error |    StdDev | Ratio | Code Size |
|--------------- |------ |----------:|----------:|----------:|------:|----------:|
|           Find |    42 | 16.363 ns | 0.1833 ns | 0.1530 ns |  1.00 |     110 B |
| FindVectorized |    42 |  1.799 ns | 0.0320 ns | 0.0299 ns |  0.11 |     191 B |

同樣的性能進行了16倍的提速。


結尾

作者:江湖評談
歡迎關注公衆號:jianghupt。文章首發。
image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章