使用一个字节数组快速复制和填充(或复制)另一个字节数组

3

我目前正在处理一个复制函数,它从源字节数组填充目标字节数组,并复制源数组直到目标数组被填满(有些人称其为MemCpyReplicate或类似名称)。目标数组的长度始终是源数组长度的倍数。 我的第一次尝试是通过Unsafe.CopyBlockUnaligned内部函数进行简单的复制,它会简单地发出rep movsb指令:

public static void CopyRepeat(byte* destination, byte* source, int byteCount, int count) {
  while(count-- > 0) {
    Unsafe.CopyBlockUnaligned(destination, source, (uint)byteCount);
    destination += byteCount;
  }
}

由于结果不令人满意,我现在想使用SIMD,更确切地说是Vector<T>接口。但我不知道如何处理未对齐地址和小于向量长度的字节模式。 这将是我的理想解决方案: 源数组 -> 10字节,向量 -> 32字节 = 3个字节模式
这些字节序列大多在1到64字节的范围内。重复次数从1到500不等。有更好的解决方案吗?或者有类似功能的示例实现吗?
更新: 我从原始版本构建了两种矢量化变体。第一种重复向量中的模式,以使向量包含n个模式。如果模式太大无法适应向量,则使用CopyBlock。第二个变体重复模式,直到目标中超过向量大小的字节数,然后始终复制向量大小的块(并移动源窗口),而不使用CopyBlock。

方法 字节数 次数 平均值 误差 标准偏差
Repeat_CopyBlock 3 16 19.38 ns 0.002 ns 0.002 ns
Repeat_NoCopyBlock 3 16 13.90 ns 0.106 ns 0.100 ns
Repeat_CopyBlock 3 128 25.00 ns 0.005 ns 0.005 ns
Repeat_NoCopyBlock 3 128 39.31 ns 0.135 ns 0.126 ns
Repeat_CopyBlock 12 16 10.64 ns 0.037 ns 0.031 ns
Repeat_NoCopyBlock 12 16 13.35 ns 0.024 ns 0.023 ns
Repeat_CopyBlock 12 128 25.56 ns 0.020 ns 0.019 ns
Repeat_NoCopyBlock 12 128 108.61 ns 0.164 ns 0.154 ns
Repeat_CopyBlock 16 16 68.74 ns 0.010 ns 0.009 ns
Repeat_NoCopyBlock 16 16 13.50 ns 0.002 ns 0.002 ns
Repeat_CopyBlock 16 128 81.41 ns 0.024 ns 0.022 ns
Repeat_NoCopyBlock 16 128
public static unsafe void Repeat_NoCopyBlock(byte* destination, byte* source, int byteCount, int count) {
    if(byteCount == 1) {
        Unsafe.InitBlockUnaligned(destination, *source, (uint)count);
        return;
    }

    var absoluteByteCount = byteCount * count;
    var dst = destination;
    var offset = 0;

    do
    {
        if(offset == absoluteByteCount) return;

        offset += byteCount;

        var src = source;
        var remaining = byteCount;

        while((remaining & -4) != 0) {
            *((uint*)dst) = *((uint*)src);
            dst += 4;
            src += 4;
            remaining -= 4;
        }

        if((remaining & 2) != 0) {
            *((ushort*)dst) = *((ushort*)src);
            dst += 2;
            src += 2;
            remaining -= 2;
        }

        if((remaining & 1) != 0)
            *dst++ = *src;
    } while((offset & (2 * -Vector<byte>.Count)) == 0); // & -Vector<byte>.Count was 2x slower.

    var stopLoopAtOffset = absoluteByteCount - Vector<byte>.Count;
    var from = destination;
    // var buffer = offset;

    while(offset <= stopLoopAtOffset) {
        Unsafe.WriteUnaligned(dst, Unsafe.ReadUnaligned<Vector<byte>>(from));
        offset += Vector<byte>.Count;
        from   += Vector<byte>.Count;
        dst    += Vector<byte>.Count;
    }

    var rep = (offset / byteCount) * byteCount; // Closest pattern end.

    if(offset != absoluteByteCount) {
        // next line is the replacement for (buffer is the offset from destination before the loop above):
        // destination + buffer - Vector<byte>.Count
        var repEnd = destination + rep - Vector<byte>.Count;
        var dstEnd = destination + stopLoopAtOffset;
        Unsafe.WriteUnaligned(dstEnd, Unsafe.ReadUnaligned<Vector<byte>>(repEnd));
    }
}

public static unsafe void Repeat_CopyBlock(byte* destination, byte* source, int byteCount, int count) {
    if(count == 0) return;
    if(byteCount == 0) return;

    if(byteCount == 1) {
        Unsafe.InitBlockUnaligned(destination, *source, (uint)count);
        return;
    }

    var numElements = Vector<byte>.Count / byteCount;
    var numElementsByteCount = numElements * byteCount;

    var i = 0;
    var dst = destination;

    do
    {
        var remaining = byteCount;
        var src = source;

        while(remaining >= 4) {
            *((uint*)dst) = *((uint*)src);
            dst += 4;
            src += 4;
            remaining -= 4;
        }

        if((remaining & 2) != 0) {
            *((ushort*)dst) = *((ushort*)src);
            dst += 2;
            src += 2;
            remaining -= 2;
        }

        if((remaining & 1) != 0)
            *dst++ = *src;

        ++i; --count;
    } while(count != 0 && i < numElements);

    if(numElements > 0) { // Skip byteCounts larger than Vector<byte>.Count.
        var src = Unsafe.ReadUnaligned<Vector<byte>>(destination);

        while(count > numElements) {
            Unsafe.WriteUnaligned(dst, src);
            count -= numElements;
            dst += numElementsByteCount;
        }
    }

    while(count > 0) {
        Unsafe.CopyBlockUnaligned(dst, destination, (uint)byteCount);
        dst += byteCount;
        --count;
    }
}


你的问题应该直接包含向量化变体的源代码,以及asm的sharplab链接。离线链接可能会失效。您可能没有达到帖子字符数的30k限制。 - Peter Cordes
@Peter Cordes:已添加源代码。 - ListigerLurch
1个回答

1
在汇编语言中,进行重叠存储非常快,例如对于一个10字节的模式,您可以进行16字节的SIMD存储,并将指针增加10。

但是,更有效的方法是在多个寄存器中展开模式,并展开循环。理想情况下,展开到lowest_common_multiple(pattern, vector_width),但即使只是3倍展开以填充大部分32字节向量也很好。(或者在没有AVX的情况下,在一对16字节向量中展开,因此两个不重叠的存储总共为32字节)。特别是当重复计数不是很大时,因此您不能花费太长时间设置向量。

或者为了使更长的模式更容易设置(而不会读取src缓冲区之外的内容):借用glibc memcpy的策略,例如使用两个重叠的16字节加载进行30字节的复制,一个从开头开始,一个从结尾结束。因此,在主循环中,您将执行一系列N个具有潜在重叠的存储,然后将下一个30个字节存储而不与第一个重叠。

哎呀,但是变量数量的寄存器不容易循环,这将需要不同的循环。也许总是4个向量寄存器,但它们之间的偏移量可变,因此单个循环可以使用索引寻址模式和指针增量。(这对于在Ice Lake之前的Intel上运行的AGUs上运行的存储不是理想的(端口7 AGU仅处理1寄存器寻址模式),但它们不会与此逻辑核心中的任何负载竞争,因此可能没问题。)也许一些偏移量可以固定在向量宽度上,只有最后一个向量可能部分重叠第三个。

因此,由设置代码确定将多少次模式重复适合3到4倍的向量宽度,并在其中进行重叠。不幸的是,palign仅可用于立即计数,如果您使用更窄的存储器以您当前的方式进行模式的前几次迭代,则会出现存储转发停顿,并从那里重新加载到XMM或YMM寄存器中。(并且多个SF停顿无法重叠其延迟。)


我不知道如何轻松地让C#的JIT发出这样的汇编语言,无论是使用Vector<>内部函数还是Sse2.whatever/Avx.whater;我除了SO答案之外没有使用C#进行任何事情;我只是试图指向一个好目标。


谢谢,这绝对是我可以深入研究的方向。您认为类似于我的第二个解决方案的log(n)复制函数(例如此处)如何?因此,写入的结果直接再次复制(并相应增加长度)。对我来说,看起来在这种情况下unalign movsb的成本应该相当高。 - ListigerLurch
@ListigerLurch:嗯。如果你无法让C# JIT在块复制方面做得比“rep movsb”更好,那么至少你正在减少启动开销。我需要再次确认不同微架构上的非对齐“rep movsb”,但我记得它仍然受益于“快速字符串”微码。也许启动开销更高,一旦开始就不如启动n个小的rep movsb快,但仍然比较好。 (“快速短rep movs”是Ice Lake上的新功能,我认为这使得像这样的东西的启动时间显着降低,也许因为英特尔看到了C#发出这个汇编语言) - Peter Cordes
我可以使用直接的SIMD内置函数(如AVX和SSE),但是Vector<T>具有使用最大可能的向量长度的优点,代价是可用函数较少。 我可以通过使用Unsafe.SkipInit来进一步降低每个向量的成本。对于仅写入操作,可以使用Unsafe.WriteUnsafe.WriteUnaligned - ListigerLurch
@ListigerLurch:被迫使用最宽的向量长度可能是一种不利。如果LCM(pattern,16)为112,则您可以考虑三个32字节存储和一个16字节存储的策略,而不是必须准备第4个32字节向量,该向量可以正确重叠,或者始终在下一组重叠的顶部16字节中存储垃圾。在循环结束时不方便,可能需要更早地停止进行清理。也许需要额外的存储转发延迟和/或缓存行分裂加载来初始化它。 - Peter Cordes
@ListigerLurch:但我不知道如果运行时变量模式长度和缓冲区长度会出现这种特殊情况;很难利用,比如可能分支到一个带有一个较窄向量的循环版本,或者使用16字节向量来处理小模式和缓冲区?找到最大宽度Vector<>的良好折衷方案很可能是一个好计划。这很可能是一个“足够快”的情况,而试图获得“最优”将非常困难。选择一种在您的用例中典型大小表现良好的策略。 - Peter Cordes
我有一些时间来开发更好的变体,但现在使用AVX得到了奇怪的结果。如果您有时间,我将不胜感激,如果您可以阅读更新(提供源代码和生成的汇编)。 - ListigerLurch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接