检查字节数组是否为零的SSE指令C#

检查字节数组是否为零的SSE指令C#,c#,arrays,performance,mono,simd,C#,Arrays,Performance,Mono,Simd,假设我有一个字节[],并想检查是否所有字节都是零。For循环是一种显而易见的方法,而LINQAll()是一种奇特的方法,但最高的性能至关重要 如何使用加快检查字节数组是否充满零?我正在寻找最前沿的方法,而不仅仅是正确的解决方案。下面给出了最佳代码。其他方法和时间测量在中提供 结论: 只有一组有限的指令。我没有找到计算标量和(向量)或最大(向量)的指令。然而,向量相等运算符返回bool 循环展开是一种强大的技术。即使是最快的代码也能从中受益匪浅 LINQ速度非常慢,因为它使用lambda表达式中

假设我有一个
字节[]
,并想检查是否所有字节都是零。For循环是一种显而易见的方法,而LINQ
All()
是一种奇特的方法,但最高的性能至关重要


如何使用加快检查字节数组是否充满零?我正在寻找最前沿的方法,而不仅仅是正确的解决方案。

下面给出了最佳代码。其他方法和时间测量在中提供

结论:

  • 只有一组有限的指令。我没有找到计算标量和(向量)或最大(向量)的指令。然而,向量相等运算符返回bool
  • 循环展开是一种强大的技术。即使是最快的代码也能从中受益匪浅
  • LINQ速度非常慢,因为它使用lambda表达式中的委托。如果你需要最先进的性能,那么很明显,这不是你要走的路
  • 所有呈现的方法都会使用,这意味着一旦遇到非零,它们就会结束
  • SIMD代码最终被击败。关于SIMD是否真的能让事情变得更快,还有其他问题存在争议

在同行审查中,迄今为止发现并修复了两个bug。

标量实现一次处理64位(8字节)的long,并从这种强大的并行性中获得大部分加速

上述SIMD/SSE代码使用128位SIMD/SSE(16字节)指令。使用较新的256位(32字节)SSE指令时,SIMD实现速度大约快10%。使用最新处理器中512位(64字节)的AVX/AVX2指令,使用这些指令的SIMD实现应该更快

    private static bool ZeroDetectSseInner(this byte[] arrayToOr, int l, int r)
    {
        var zeroVector = new Vector<byte>(0);
        int concurrentAmount = 4;
        int sseIndexEnd = l + ((r - l + 1) / (Vector<byte>.Count * concurrentAmount)) * (Vector<byte>.Count * concurrentAmount);
        int i;
        int offset1 = Vector<byte>.Count;
        int offset2 = Vector<byte>.Count * 2;
        int offset3 = Vector<byte>.Count * 3;
        int increment = Vector<byte>.Count * concurrentAmount;
        for (i = l; i < sseIndexEnd; i += increment)
        {
            var inVector  = new Vector<byte>(arrayToOr, i          );
            inVector     |= new Vector<byte>(arrayToOr, i + offset1);
            inVector     |= new Vector<byte>(arrayToOr, i + offset2);
            inVector     |= new Vector<byte>(arrayToOr, i + offset3);
            if (!Vector.EqualsAll(inVector, zeroVector))
                return false;
        }
        byte overallOr = 0;
        for (; i <= r; i++)
            overallOr |= arrayToOr[i];
        return overallOr == 0;
    }

    public static bool ZeroValueDetectSse(this byte[] arrayToDetect)
    {
        return arrayToDetect.ZeroDetectSseInner(0, arrayToDetect.Length - 1);
    }
private static bool ZeroDetectSseInner(此字节[]数组或,int l,int r)
{
var zeroVector=新向量(0);
int concurrentAmount=4;
int-sseIndexEnd=l+((r-l+1)/(Vector.Count*concurrentAmount))*(Vector.Count*concurrentAmount);
int i;
int offset1=向量计数;
int offset2=Vector.Count*2;
int offset3=Vector.Count*3;
int increment=Vector.Count*concurrentAmount;
对于(i=l;i对于(;i,这假设您的数组长度为16*N,这是一个很大的假设,但在受控环境中可能是有效的,而且从您在BySimdEquals上的时间来看,我高度假设您没有使用O=simd运行此程序,因此得到的是非simd O=-simd时间(?),这实际上并没有将代码执行时间提高那么多。用C和p编写代码/调用GC固定数组会更快。展开版本确实更快,但将循环展开2次(因此每个循环只比较2 x 8字节)会得到类似的结果(如果不是更好的话)在我的机器上的性能为16倍。当您知道大多数x64机器只有两个64位数据通道时(如果您有两个记忆棒,并且它们安装在正确的插槽中),这是有意义的.内存读取可能是这里最大的瓶颈。在对.NET应用程序执行性能测试时,您应该确保运行几次并跳过第一次测试,因为JIT可能会参与其中。如果您想谈论绝对最快的性能,那么您可能也应该指定硬件…使用BenchmarkDotNet运行您的应用程序不同的候选者和报告结果将是理想的,因为它确保以尽可能精确的方式运行候选者,并且其输出包括运行参数,如硬件、GC模式等。为什么要将
=
放入累加器,但仍要在每次迭代中检查累加器?这是有意义的ode>一个
pcmpeqb
/
pmovmskb
/
test
/
jnz
循环中断条件的缓存线或两个向量。但在发现之前的值均为零后,您希望启动一个新的
或向量
,从而中断依赖关系链。如果按照编写的方式进行编译,则每个循环只能有一个向量t最佳(通过
或矢量
实现数据依赖性,而不是像现代x86那样每时钟加载2倍16字节(AMD从K10开始,Intel从Sandybridge开始).或Haswell及更高版本的每个时钟2 x 32字节负载。是的,这是显示标量展开循环可以被击败的初始实现。后续实现使用循环中的几个独立Or向量来打破依赖关系。短路测试它们是否为非零而不引入依赖关系是一个难题。l查看建议。在源代码中手动展开的循环的顶部,执行
orVector=new Vector(arrayToOr,i);
然后对以后的向量执行
orVector |=new Vector(arrayToOr,i+1)
…i+2
等。在这个循环的底部,测试它是否为非零。感谢您的解释-帮助很大,使它具体化。此方法带来了10%的加速比,256位(32字节)的总增益为20%SSE safe C#实现优于上面展开的标量实现。您能否进一步说明此方法如何不具有依赖性,因为它或将所有读取向量放入单个orVector?我实现了单独/多个orVector,但只是为了比您的建议获得最小的性能增益。它在主体中具有依赖性单个循环迭代的,但在循环迭代中打破依赖关系。因此没有循环携带的依赖关系(指针增量除外)。这使得无序执行在单独的迭代中重叠工作,因为它们是单独的依赖链。(分支预测/推测)
static unsafe bool ByFixedLongUnrolled (byte[] data)
{
    fixed (byte* bytes = data) {
        int len = data.Length;
        int rem = len % (sizeof(long) * 16);
        long* b = (long*)bytes;
        long* e = (long*)(bytes + len - rem);

        while (b < e) {
            if ((*(b) | *(b + 1) | *(b + 2) | *(b + 3) | *(b + 4) |
                *(b + 5) | *(b + 6) | *(b + 7) | *(b + 8) |
                *(b + 9) | *(b + 10) | *(b + 11) | *(b + 12) | 
                *(b + 13) | *(b + 14) | *(b + 15)) != 0)
                return false;
            b += 16;
        }

        for (int i = 0; i < rem; i++)
            if (data [len - 1 - i] != 0)
                return false;

        return true;
    }
}
LINQ All(b => b == 0)                   : 6350,4185 ms
Foreach over byte[]                     : 580,4394 ms
For with byte[].Length property         : 809,7283 ms
For with Length in local variable       : 407,2158 ms
For unrolled 16 times                   : 334,8038 ms
For fixed byte*                         : 272,386 ms
For fixed byte* unrolled 16 times       : 141,2775 ms
For fixed long*                         : 52,0284 ms
For fixed long* unrolled 16 times       : 25,9794 ms
SIMD Vector16b equals Vector16b.Zero    : 56,9328 ms
SIMD Vector16b also unrolled 16 times   : 32,6358 ms
    private static bool ZeroDetectSseInner(this byte[] arrayToOr, int l, int r)
    {
        var zeroVector = new Vector<byte>(0);
        int concurrentAmount = 4;
        int sseIndexEnd = l + ((r - l + 1) / (Vector<byte>.Count * concurrentAmount)) * (Vector<byte>.Count * concurrentAmount);
        int i;
        int offset1 = Vector<byte>.Count;
        int offset2 = Vector<byte>.Count * 2;
        int offset3 = Vector<byte>.Count * 3;
        int increment = Vector<byte>.Count * concurrentAmount;
        for (i = l; i < sseIndexEnd; i += increment)
        {
            var inVector  = new Vector<byte>(arrayToOr, i          );
            inVector     |= new Vector<byte>(arrayToOr, i + offset1);
            inVector     |= new Vector<byte>(arrayToOr, i + offset2);
            inVector     |= new Vector<byte>(arrayToOr, i + offset3);
            if (!Vector.EqualsAll(inVector, zeroVector))
                return false;
        }
        byte overallOr = 0;
        for (; i <= r; i++)
            overallOr |= arrayToOr[i];
        return overallOr == 0;
    }

    public static bool ZeroValueDetectSse(this byte[] arrayToDetect)
    {
        return arrayToDetect.ZeroDetectSseInner(0, arrayToDetect.Length - 1);
    }