c# simd 指令_.NET / C#中的SIMD概述

c# simd 指令

Here’s a quick look at algorithm vectorization capabilities in .NET Framework and .NET Core. This article is for those who know nothing about these techniques. I will also show that .NET doesn’t actually lag behind "real compiled" languages for native development.

快速浏览一下.NET Framework和.NET Core中的算法矢量化功能。 本文适用于对这些技术一无所知的人。 我还将说明,.NET实际上在本机开发方面并不落后于“真正的编译”语言。

I’m just starting to learn vectorization techniques. So, I will appreciate if community members find clear errors or suggest improvements to the described algorithms.

我才刚刚开始学习矢量化技术。 因此,如果社区成员发现明显的错误或对所描述的算法提出改进建议,我将不胜感激。

一些历史 (Some history)

SIMD appeared in .NET Framework 4.6 in 2015. That's when Matrix3x2, Matrix4x4, Plane, Quaternion, Vector2, Vector3 and Vector4 types were added. They allowed vectorized computations. Next was the Vector<T> type that gave more opportunities to vectorize algorithms. However, many programmers were still dissatisfied as these types restricted coders’ idea streams and didn’t let use the full capacity of SIMD instructions in modern processors. Now in .NET Core 3.0 Preview, we have System.Runtime.Intrinsics namespace that gives much freedom in the choice of instructions. To get the most in speed you need to use RyuJit and resort either to x64 assembly or switch off Prefer 32-bit and choose AnyCPU assembly. I ran all the benchmarks on Intel Core i7-6700 3.40 GHz (Skylake) CPU computer.

SIMD出现在2015年的.NET Framework 4.6中。那时,添加了Matrix3x2,Matrix4x4,Plane,Quaternion,Vector2,Vector3和Vector4类型。 他们允许向量化计算。 接下来是Vector <T>类型,它提供了更多的机会来对算法进行矢量化。 但是,许多程序员仍然不满意,因为这些类型限制了编码人员的想法流,并且不允许在现代处理器中使用SIMD指令的全部功能。 现在,在.NET Core 3.0 Preview中,我们有了System.Runtime.Intrinsics命名空间,该命名空间为指令选择提供了很大的自由度。 要获得最快的速度,您需要使用RyuJit并采用x64汇编或关闭Prefer 32-bit并选择AnyCPU汇编。 我在Intel Core i7-6700 3.40 GHz(Skylake)CPU计算机上运行了所有基准测试。

汇总数组元素 (Summing array elements)

I decided to start with a classic task which usually comes first if there’s vectorization involved. It deals with finding the sum of array elements. Let’s write four implementations of this task to sum the elements of Array.

我决定从经典任务开始,如果涉及矢量化,通常会首先执行。 它涉及查找数组元素的总和。 让我们编写此任务的四个实现以汇总Array的元素。

The most obvious implementation:

最明显的实现:

public int Naive() {
    int result = 0;
    foreach (int i in Array) {
        result += i;
    }
    return result;
}

LINQ-based implementation:

基于LINQ的实现:

public long LINQ() => Array.Aggregate<int, long>(0, (current, i) => current + i);

The implementation based on vectors from System.Numerics:

基于System.Numerics中的向量的实现:

public int Vectors() {
    int vectorSize = Vector<int>.Count;
    var accVector = Vector<int>.Zero;
    int i;
    var array = Array;
    for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        accVector = Vector.Add(accVector, v);
    }
    int result = Vector.Dot(accVector, Vector<int>.One);
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

The implementation based on code from System.Runtime.Intrinsics namespace:

基于System.Runtime.Intrinsics命名空间中的代码的实现:

public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            accVector = Avx2.Add(accVector, v);
        }
    }
    int result = 0;
    var temp = stackalloc int[vectorSize];
    Avx2.Store(temp, accVector);
    for (int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }   
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

I benchmarked those 4 methods on my computer and got the following results:

我在计算机上对这4种方法进行了基准测试,并得到以下结果:

MethodItemsCountMeanErrorStdDevRatio
Naive103.531 ns0.0336 ns0.0314 ns1.00
LINQ1076.925 ns0.4166 ns0.3897 ns21.79
Vectors102.750 ns0.0210 ns0.0196 ns0.78
Intrinsics106.513 ns0.0623 ns0.0582 ns1.84
Naive10047.982 ns0.3975 ns0.3524 ns1.00
LINQ100590.414 ns3.8808 ns3.4402 ns12.31
Vectors10010.122 ns0.0747 ns0.0699 ns0.21
Intrinsics10014.277 ns0.0566 ns0.0529 ns0.30
Naive1000569.910 ns2.8297 ns2.6469 ns1.00
LINQ10005,658.570 ns31.7465 ns29.6957 ns9.93
Vectors100079.598 ns0.3498 ns0.3272 ns0.14
Intrinsics100066.970 ns0.3937 ns0.3682 ns0.12
Naive100005,637.571 ns37.5050 ns29.2814 ns1.00
LINQ1000056,498.987 ns294.8776 ns275.8287 ns10.02
Vectors10000772.900 ns2.6802 ns2.5070 ns0.14
Intrinsics10000579.152 ns2.8371 ns2.6538 ns0.10
Naive10000056,352.865 ns230.7916 ns215.8826 ns1.00
LINQ100000562,610.571 ns3,775.7631 ns3,152.9332 ns9.98
Vectors1000008,389.647 ns165.9590 ns227.1666 ns0.15
Intrinsics1000007,261.334 ns89.6468 ns69.9903 ns0.13
方法 ItemsCount 意思 错误 标准差
幼稚 10 3.531 ns 0.0336纳秒 0.0314纳秒 1.00
LINQ 10 76.925 ns 0.4166纳秒 0.3897 ns 21.79
向量 10 2.750纳秒 0.0210纳秒 0.0196 ns 0.78
本征 10 6.513 ns 0.0623 ns 0.0582 ns 1.84
幼稚 100 47.982 ns 0.3975纳秒 0.3524 ns 1.00
LINQ 100 590.414 ns 3.8808 ns 3.4402纳秒 12.31
向量 100 10.122 ns 0.0747 ns 0.0699 ns 0.21
本征 100 14.277 ns 0.0566 ns 0.0529 ns 0.30
幼稚 1000 569.910 ns 2.8297 ns 2.6469 ns 1.00
LINQ 1000 5,658.570 ns 31.7465 ns 29.6957 ns 9.93
向量 1000 79.598 ns 0.3498 ns 0.3272纳秒 0.14
本征 1000 66.970 ns 0.3937纳秒 0.3682纳秒 0.12
幼稚 10000 5,637.571 ns 37.5050 ns 29.2814 ns 1.00
LINQ 10000 56,498.987 ns 294.8776 ns 275.8287 ns 10.02
向量 10000 772.900 ns 2.6802纳秒 2.5070 ns 0.14
本征 10000 579.152 ns 2.8371 ns 2.6538 ns 0.10
幼稚 100000 56,352.865 ns 230.7916 ns 215.8826 ns 1.00
LINQ 100000 562,610.571 ns 3,775.7631 ns 3,152.9332 ns 9.98
向量 100000 8,389.647 ns 165.9590 ns 227.1666 ns 0.15
本征 100000 7,261.334 ns 89.6468 ns 69.9903 ns 0.13

It’s clear that solutions with Vectors and Intrinsics are much faster than the obvious and LINQ-based solutions. Now we need to figure out what goes on in these two methods.

显然,使用向量和内在函数的解决方案比明显的基于LINQ的解决方案要快得多。 现在,我们需要弄清楚这两种方法的情况。

Let’s consider Vectors method more closely:

让我们更仔细地考虑Vectors方法:

向量 (Vectors)

public int Vectors() {
    int vectorSize = Vector<int>.Count;
    var accVector = Vector<int>.Zero;
    int i;
    var array = Array;
    for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        accVector = Vector.Add(accVector, v);
    }
    int result = Vector.Dot(accVector, Vector<int>.One);
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}
  • int vectorSize = Vector<int>.Count; — the amount of 4-byte numbers we can place in a vector. If hardware acceleration is used, this value shows how many 4-byte numbers we can put in one SIMD register. In fact, it shows how many elements of this type can be handled concurrently;

    int vectorSize = Vector <int> .Count; —我们可以在向量中放置的4字节数字的数量。 如果使用硬件加速,则该值显示我们可以在一个SIMD寄存器中放入多少个4字节数字。 实际上,它显示了可以同时处理此类型的元素的数量。
  • accVector is a vector that accumulates the result of the function;

    accVector是累积函数结果的向量;
  • var v = new Vector<int>(array, i); — the data from array is loaded into a new v vector, starting from i index. The vectorSize of data will be loaded exactly;

    var v = new Vector <int>(array,i); —从i索引开始,将数组中的数据加载到新的v向量中。 数据的vectorSize将被完全加载;
  • accVector = Vector.Add(accVector, v); — two vectors are summed.

    accVector = Vector.Add(accVector,v); —两个向量相加。

    For example, there are 8 numbers in Array: {0, 1, 2, 3, 4, 5, 6, 7} and vectorSize == 4.

    例如,数组中有8个数字:{0、1、2、3、4、5、6、7}和vectorSize == 4。

    Then during the first cycle iteration accVector = {0, 0, 0, 0}, v = {0, 1, 2, 3} and after addition accVector will hold: {0, 0, 0, 0} + {0, 1, 2, 3} = {0, 1, 2, 3}.

    然后在第一个循环迭代期间,accVector = {0,0,0,0},v = {0,1,2,3},加法后,accVector将保持:{0,0,0,0} + {0,1 ,2,3} = {0,1,2,3}。

    During the second iteration v = {4, 5, 6, 7} and after addition accVector = {0, 1, 2, 3} + {4, 5, 6, 7} = {4, 6, 8, 10}.

    在第二次迭代中,v = {4,5,6,7},加法后accVector = {0,1,2,3} + {4,5,6,7} = {4,6,8,10}。

  • Now we just need to get the sum of all vector elements. To do this we can use scalar multiplication by a vector filled with ones: int result = Vector.Dot(accVector, Vector<int>.One);

    现在我们只需要获取所有向量元素的总和即可。 为此,我们可以使用标量乘以一个填充有一个的向量:int result = Vector.Dot(accVector,Vector <int> .One);

    Then we get: {4, 6, 8, 10} * {1, 1, 1, 1} = 4 * 1 + 6 * 1 + 8 * 1 + 10 * 1 = 28.

    然后得到:{4,6,8,10} * {1,1,1,1} = 4 * 1 + 6 * 1 + 8 * 1 + 10 * 1 = 28。

  • If necessary, those numbers that don’t fit the last vector will be summed at the end.

    如有必要,那些不适合最后一个向量的数字将在末尾求和。

Let's look into Intrinsics code:

让我们看一下内部代码:

本征 (Intrinsics)

public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            accVector = Avx2.Add(accVector, v);
        }
    }
    int result = 0;
    var temp = stackalloc int[vectorSize];
    Avx2.Store(temp, accVector);
    for (int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }   
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

We can see that it’s like Vectors with one exception:

我们可以看到它类似于Vectors,但有一个例外:

  • vectorSize is specified by a constant. This is because this method explicitly uses Avx2 instructions that operate with 256-bit registers. A real application should include a check of whether a current processor supports Avx2. If not, another code should be called. It looks like this:

    vectorSize由常量指定。 这是因为此方法明确使用了与256位寄存器一起操作的Avx2指令。 实际的应用程序应包括检查当前处理器是否支持Avx2。 如果不是,则应调用另一个代码。 看起来像这样:

    if (Avx2.IsSupported) {
    DoThingsForAvx2();
    }
    else if (Avx.IsSupported) {
    DoThingsForAvx();
    }
    ...
    else if (Sse2.IsSupported) {
    DoThingsForSse2();
    }
    ...
  • var accVector = Vector256<int>.Zero; accVector is declared as 256-bit vector filled with zeros.

    var accVector = Vector256 <int> .Zero; accVector被声明为填充零的256位向量。
  • fixed (int* ptr = Array) — the pointer to the array is placed in ptr.

    固定的(int * ptr = Array)—数组的指针放在ptr中。
  • Next are the same operations as in Vectors: loading data into a vector and addition of two vectors.

    接下来是与向量中相同的操作:将数据加载到向量中并添加两个向量。
  • The summing of vector elements uses the following method:

    向量元素的求和使用以下方法:

    • create an array on stack: var temp = stackalloc int[vectorSize];

      在堆栈上创建一个数组:var temp = stackalloc int [vectorSize];
    • load a vector into this array: Avx2.Store(temp, accVector);

      将向量加载到此数组中:Avx2.Store(temp,accVector);
    • sum array elements during the cycle.

      在循环中对数组元素求和。
  • Next, the elements which don’t fit the last vector are summed up.

    接下来,汇总不适合最后一个向量的元素。

比较两个数组 (Comparing two arrays)

We need to compare two arrays of bytes. It is exactly this task that made me study SIMD in .NET. Again, let’s write several methods for benchmarking and compare two arrays: ArrayA and ArrayB.

我们需要比较两个字节数组。 正是这一任务使我在.NET中学习SIMD。 再次,让我们编写几种基准测试方法并比较两个数组:ArrayA和ArrayB。

The most obvious solution:

最明显的解决方案:

public bool Naive() {
    for (int i = 0; i < ArrayA.Length; i++) {
        if (ArrayA[i] != ArrayB[i]) return false;
    }
    return true;
}

LINQ-based solution:

基于LINQ的解决方案:

public bool LINQ() => ArrayA.SequenceEqual(ArrayB);

The solution based on MemCmp function:

基于MemCmp功能的解决方案:

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);

public bool MemCmp() => memcmp(ArrayA, ArrayB, ArrayA.Length) == 0;

The solution based on vectors from System.Numerics:

基于System.Numerics中的向量的解决方案:

public bool Vectors() {
    int vectorSize = Vector<byte>.Count;
    int i = 0;
    for (; i <= ArrayA.Length - vectorSize; i += vectorSize) {
        var va = new Vector<byte>(ArrayA, i);
        var vb = new Vector<byte>(ArrayB, i);
        if (!Vector.EqualsAll(va, vb)) {
            return false;
        }
    }
    for (; i < ArrayA.Length; i++) {
        if (ArrayA[i] != ArrayB[i])
            return false;
    }
    return true;
}

Intrinsics-based solution:

基于内在的解决方案:

public unsafe bool Intrinsics() {
    int vectorSize = 256 / 8;
    int i = 0;
    const int equalsMask = unchecked((int) (0b1111_1111_1111_1111_1111_1111_1111_1111));
    fixed (byte* ptrA = ArrayA)
    fixed (byte* ptrB = ArrayB) {
        for (; i <= ArrayA.Length - vectorSize; i += vectorSize) {
            var va = Avx2.LoadVector256(ptrA + i);
            var vb = Avx2.LoadVector256(ptrB + i);
            var areEqual = Avx2.CompareEqual(va, vb);
            if (Avx2.MoveMask(areEqual) != equalsMask) {
                return false;
            }
        }
        for (; i < ArrayA.Length; i++) {
            if (ArrayA[i] != ArrayB[i])
                return false;
        }
        return true;
    }
}

The results of running the benchmark on my computer:

在计算机上运行基准测试的结果:

MethodItemsCountMeanErrorStdDevRatio
Naive100007,033.8 ns50.636 ns47.365 ns1.00
LINQ1000064,841.4 ns289.157 ns270.478 ns9.22
Vectors10000504.0 ns2.406 ns2.251 ns0.07
MemCmp10000368.1 ns2.637 ns2.466 ns0.05
Intrinsics10000283.6 ns1.135 ns1.061 ns0.04
Naive10000085,214.4 ns903.868 ns845.478 ns1.00
LINQ100000702,279.4 ns2,846.609 ns2,662.720 ns8.24
Vectors1000005,179.2 ns45.337 ns42.409 ns0.06
MemCmp1000004,510.5 ns24.292 ns22.723 ns0.05
Intrinsics1000002,957.0 ns11.452 ns10.712 ns0.03
Naive1000000844,006.1 ns3,552.478 ns3,322.990 ns1.00
LINQ10000006,483,079.3 ns42,641.040 ns39,886.455 ns7.68
Vectors100000054,180.1 ns357.258 ns334.180 ns0.06
MemCmp100000049,480.1 ns515.675 ns457.133 ns0.06
Intrinsics100000036,633.9 ns680.525 ns636.564 ns0.04
方法 ItemsCount 意思 错误 标准差
幼稚 10000 7,033.8 ns 50.636 ns 47.365 ns 1.00
LINQ 10000 64,841.4 ns 289.157 ns 270.478 ns 9.22
向量 10000 504.0 ns 2.406纳秒 2.251纳秒 0.07
记忆卡 10000 368.1 ns 2.637 ns 2.466纳秒 0.05
本征 10000 283.6 ns 1.135纳秒 1.061纳秒 0.04
幼稚 100000 85,214.4 ns 903.868 ns 845.478 ns 1.00
LINQ 100000 702,279.4 ns 2,846.609 ns 2,662.720 ns 8.24
向量 100000 5,179.2 ns 45.337 ns 42.409 ns 0.06
记忆卡 100000 4,510.5 ns 24.292 ns 22.723 ns 0.05
本征 100000 2,957.0 ns 11.452 ns 10.712 ns 0.03
幼稚 1000000 844,006.1 ns 3,552.478 ns 3,322.990 ns 1.00
LINQ 1000000 6,483,079.3 ns 42,641.040 ns 39,886.455 ns 7.68
向量 1000000 54,180.1 ns 357.258 ns 334.180 ns 0.06
记忆卡 1000000 49,480.1 ns 515.675 ns 457.133 ns 0.06
本征 1000000 36,633.9 ns 680.525 ns 636.564 ns 0.04

I guess the code of these methods is clear, except for two lines in Intrinsics:

我猜这些方法的代码很清楚,除了Intrinsics中的两行:

var areEqual = Avx2.CompareEqual(va, vb);
if (Avx2.MoveMask(areEqual) != equalsMask) {
    return false;
}

In the first line two vectors are compared for equality and the result is saved in areEqual vector in which all bits in the element at a particular position are set to 1 if the corresponding elements in va and vb are equal. So, it turns out that if byte vectors va and vb are equal, all the elements in areEquals should equal to 255 (11111111b). As Avx2.CompareEqual is a wrapper over _mm256_cmpeq_epi8, we can go to Intel website and see the pseudocode of this operation: MoveMask method makes a 32-bit number from a vector. The top bits of each 32 onebyte elements in a vector are the values of bits in the MoveMask result. The pseudocode is available here.

在第一行中,比较两个向量的相等性,并将结果保存在areEqual向量中,如果va和vb中的对应元素相等,则将特定位置元素中的所有位都设置为1。 因此,事实证明,如果字节向量va和vb相等,则areEquals中的所有元素都应等于255(11111111b)。 由于Avx2.CompareEqual是_mm256_cmpeq_epi8的包装器,因此我们可以访问Intel网站并查看此操作的伪代码:MoveMask方法从向量中得出32位数字。 向量中每32个单字节元素的高位是MoveMask结果中的位的值。 伪代码在此处可用。

Thus, if some bytes in va and vb don’t match, the corresponding bytes in areEqual will be 0. Therefore, the top bits of these bytes will be 0 too. This means the corresponding bits in Avx2.MoveMask response will also be 0 and areEqual will not equal to equalsMask.

因此,如果va和vb中的某些字节不匹配,则areEqual中的相应字节将为0。因此,这些字节的高位也将为0。 这意味着Avx2.MoveMask响应中的相应位也将为0,而areEqual将不等于equalsMask。

Let’s look at one example assuming that vector length is 8 bytes (to write less):

让我们看一个示例,假设向量长度为​​8字节(以减少写入):

  • Let va = {100, 10, 20, 30, 100, 40, 50, 100} and vb = {100, 20, 10, 30, 100, 40, 80, 90}.

    设va = {100,10,20,30,100,40,50,100},vb = {100,20,10,30,100,40,80,90}。
  • Then areEquals will be {255, 0, 0, 255, 255, 255, 0, 0}.

    然后areEquals将为{255,0,0,255,255,255,0,0}。
  • The MoveMask method will return 10011100b that should be compared with 11111111b mask. As these masks are not equal, va and vb vectors are not equal too.

    MoveMask方法将返回10011100b,应将其与11111111b掩码进行比较。 由于这些掩码不相等,因此va和vb向量也不相等。

计算元素在集合中出现的次数。 (Counting the times an element occurs in a collection.)

Sometimes you need to count the occurrences of a particular element, e.g. integers, in a collection. We can speed up this algorithm too. For comparison let’s write several methods to search Item element in Array.

有时您需要计算集合中特定元素(例如整数)的出现次数。 我们也可以加快该算法的速度。 为了进行比较,让我们编写几种方法来搜索Array中的Item元素。

The most obvious one:

最明显的一个:

public int Naive() {
    int result = 0;
    foreach (int i in Array) {
        if (i == Item) {
            result++;
        }
    }
    return result;
}

Using LINQ:

使用LINQ:

public int LINQ() => Array.Count(i => i == Item);

Using vectors from System.Numerics.Vectors:

使用System.Numerics.Vectors中的向量:

public int Vectors() {
    var mask = new Vector<int>(Item);
    int vectorSize = Vector<int>.Count;
    var accResult = new Vector<int>();
    int i;
    var array = Array;
    for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        var areEqual = Vector.Equals(v, mask);
        accResult = Vector.Subtract(accResult, areEqual);
    }
    int result = 0;
    for (; i < array.Length; i++) {
        if (array[i] == Item) {
            result++;
        }
    }
    result += Vector.Dot(accResult, Vector<int>.One);
    return result;
}

Using Intrinsics:

使用本征:

public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    var temp = stackalloc int[vectorSize];
    for (int j = 0; j < vectorSize; j++) {
        temp[j] = Item;
    }
    var mask = Avx2.LoadVector256(temp);
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i <= array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            var areEqual = Avx2.CompareEqual(v, mask);
            accVector = Avx2.Subtract(accVector, areEqual);
        }
    }
    int result = 0;
    Avx2.Store(temp, accVector);
    for(int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }
    for(; i < array.Length; i++) {
        if (array[i] == Item) {
            result++;
        }
    }
    return result;
}

The results of running the benchmark on my computer:

在计算机上运行基准测试的结果:

MethodItemsCountMeanErrorStdDevRatio
Naive108.844 ns0.0772 ns0.0603 ns1.00
LINQ1087.456 ns0.9496 ns0.8883 ns9.89
Vectors103.140 ns0.0406 ns0.0380 ns0.36
Intrinsics1013.813 ns0.0825 ns0.0772 ns1.56
Naive100107.310 ns0.6975 ns0.6183 ns1.00
LINQ100626.285 ns5.7677 ns5.3951 ns5.83
Vectors10011.844 ns0.2113 ns0.1873 ns0.11
Intrinsics10019.616 ns0.1018 ns0.0903 ns0.18
Naive10001,032.466 ns6.3799 ns5.6556 ns1.00
LINQ10006,266.605 ns42.6585 ns39.9028 ns6.07
Vectors100083.417 ns0.5393 ns0.4780 ns0.08
Intrinsics100088.358 ns0.4921 ns0.4603 ns0.09
Naive100009,942.503 ns47.9732 ns40.0598 ns1.00
LINQ1000062,305.598 ns643.8775 ns502.6972 ns6.27
Vectors10000914.967 ns7.2959 ns6.8246 ns0.09
Intrinsics10000931.698 ns6.3444 ns5.9346 ns0.09
Naive10000094,834.804 ns793.8585 ns703.7349 ns1.00
LINQ100000626,620.968 ns4,696.9221 ns4,393.5038 ns6.61
Vectors1000009,000.827 ns179.5351 ns192.1005 ns0.09
Intrinsics1000008,690.771 ns101.7078 ns95.1376 ns0.09
Naive1000000959,302.249 ns4,268.2488 ns3,783.6914 ns1.00
LINQ10000006,218,681.888 ns31,321.9277 ns29,298.5506 ns6.48
Vectors100000099,778.488 ns1,975.6001 ns4,252.6877 ns0.10
Intrinsics100000096,449.350 ns1,171.8067 ns978.5116 ns0.10
方法 ItemsCount 意思 错误 标准差
幼稚 10 8.844纳秒 0.0772 ns 0.0603 ns 1.00
LINQ 10 87.456 ns 0.9496纳秒 0.8883纳秒 9.89
向量 10 3.140纳秒 0.0406纳秒 0.0380纳秒 0.36
本征 10 13.813 ns 0.0825 ns 0.0772 ns 1.56
幼稚 100 107.310 ns 0.6975纳秒 0.6183 ns 1.00
LINQ 100 626.285 ns 5.7677 ns 5.3951 ns 5.83
向量 100 11.844 ns 0.2113 ns 0.1873 ns 0.11
本征 100 19.616纳秒 0.1018纳秒 0.0903 ns 0.18
幼稚 1000 1,032.466 ns 6.3799 ns 5.6556 ns 1.00
LINQ 1000 6,266.605 ns 42.6585 ns 39.9028 ns 6.07
向量 1000 83.417 ns 0.5393纳秒 0.4780纳秒 0.08
本征 1000 88.358 ns 0.4921纳秒 0.4603纳秒 0.09
幼稚 10000 9,942.503 ns 47.9732 ns 40.0598 ns 1.00
LINQ 10000 62,305.598 ns 643.8775 ns 502.6972 ns 6.27
向量 10000 914.967 ns 7.2959 ns 6.8246 ns 0.09
本征 10000 931.698 ns 6.3444 ns 5.9346 ns 0.09
幼稚 100000 94,834.804 ns 793.8585 ns 703.7349 ns 1.00
LINQ 100000 626,620.968 ns 4,696.9221 ns 4,393.5038 ns 6.61
向量 100000 9,000.827 ns 179.5351 ns 192.1005 ns 0.09
本征 100000 8,690.771 ns 101.7078 ns 95.1376 ns 0.09
幼稚 1000000 959,302.249 ns 4,268.2488 ns 3,783.6914 ns 1.00
LINQ 1000000 6,218,681.888 ns 31,321.9277 ns 29,298.5506 ns 6.48
向量 1000000 99,778.488 ns 1,975.6001 ns 4,252.6877 ns 0.10
本征 1000000 96,449.350 ns 1,171.8067 ns 978.5116 ns 0.10

Vectors and Intrinsics methods completely coincide in logic but differ in the implementation of particular operations. The idea is the following:

向量和本征方法在逻辑上完全重合,但在特定操作的实现上有所不同。 这个想法如下:

  • create mask vector in which a required number is stored in each element;

    创建掩码向量,其中每个元素中都存储有所需的编号;
  • load the part of an array in v vector and compare this part with a mask. As a result, all bits will set in equal elements of areEqual. As areEqual is an array of integers, then if we set all the bits of one element, we will get -1 in this element ((int)(1111_1111_1111_1111_1111_1111_1111_1111b) == -1);

    在v向量中加载数组的一部分,并将其与掩码进行比较。 结果,所有位将设置在areEqual的相等元素中。 因为areEqual是一个整数数组,所以如果我们设置一个元素的所有位,我们将在该元素中获得-1((int)(1111_1111_1111_1111_1111_1111_1111_1111b)== -1);
  • subtract areEqual vector from accVector. Then, accVector will hold the count of how many times the item element occurred in all v vectors for each position (minus by minus is a plus).

    从accVector中减去areEqual向量。 然后,accVector将保存每个位置的所有v向量中item元素出现的次数(减乘以加号)。

The whole code from the article is on GitHub.

本文的全部代码在GitHub上

结论 (Conclusion)

I described only a small part of .NET capabilities for computation vectorization. To see the full updated list of all intrinsics available in .NET Core under x86, turn to the source code. It’s convenient that the summary of each intrinsic in C# files contains its name in C world. This helps either to understand the purpose of this intrinsic and the transfer of existing C++/C algorithms to .NET. System.Numerics.Vector documentation is available on msdn.

我仅描述了用于计算矢量化的.NET功能的一小部分。 若要查看x86下.NET Core中可用的所有内部函数的完整更新列表,请转到源代码 。 C#文件中每个内在函数的摘要都包含其在C world中的名称,这很方便。 这有助于了解此内在功能的目的,以及帮助将现有C ++ / C算法转移到.NET。 msdn上提供了System.Numerics.Vector文档。

I think .NET has a great advantage over C++. As JIT compilation already occurs on a client machine, a compiler can optimize code for a particular client processor, giving maximum performance. At the same time, a programmer can stay within one language and the same technologies to write fast code.

我认为.NET比C ++具有很大的优势。 由于JIT编译已在客户端计算机上进行,因此编译器可以为特定客户端处理器优化代码,从而提供最佳性能。 同时,程序员可以使用一种语言和相同的技术来编写快速代码。

翻译自: https://habr.com/en/post/467689/

c# simd 指令

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值