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

时间:2015-10-23 03:50:25

标签: c# arrays performance mono simd

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

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

2 个答案:

答案 0 :(得分:4)

最佳代码如下所示。其他方法和时间测量可在full source中找到。

static unsafe bool BySimdUnrolled (byte[] data)
{
    fixed (byte* bytes = data) {
        int len = data.Length;
        int rem = len % (16 * 16);
        Vector16b* b = (Vector16b*)bytes;
        Vector16b* e = (Vector16b*)(bytes + len - rem);
        Vector16b zero = Vector16b.Zero;

        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)) != zero)
                return false;
            b += 16;
        }

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

        return true;
    }
}

最终被这段代码打败了:

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

时间测量(在256MB阵列上):

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

结论:

  • Mono.Simd只有一组有限的说明。我没有找到计算标量和(向量)或最大值(向量)的指令。然而,有向量相等运算符返回bool。
  • 循环展开是一项强大的技术。即使是最快的代码也可以从使用它中获益良多。
  • LINQ非常慢,因为它使用来自lambda表达式的委托。如果你需要最先进的性能,那么显然这是不可能的。
  • 所有提交的方法都使用short circuit evaluation,这意味着它们会在遇到非零时结束。
  • SIMD代码最终遭到殴打。关于SIMD是否真的让事情变得更快,还有其他问题。

Posted this code在Peer Review上,到目前为止发现并修复了2个错误。

答案 1 :(得分:1)

标量实现一次处理长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);
    }

上面的代码中显示了一个改进的版本(由于Peter的建议),它是安全的,并且已集成到HPCsharp nuget软件包中,使用256位SSE指令可使速度提高20%。