[英]Fast copy and replicate (or fill) byte array with another byte array
我目前正坐在一个副本 function 上,它从源字节数组填充目标字节数组,并根据需要多次复制源数组,直到目标数组被填充(有人称之为 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>
接口。 但我不知道如何处理未对齐的地址和小于向量长度的字节模式。 这将是我理想的解决方案: Source Array -> 10 Bytes, Vector -> 32 Bytes = 3 x byte pattern
字节序列大多在 1 到 64 字节范围内。 重复次数从1到500不等,请问有没有更好的方案或者类似功能的实现示例?
更新:我从原始版本构建了两个矢量化变体。 第一个重复向量中的模式,使向量包含n
模式。 如果模式对于矢量来说太大,则使用 CopyBlock。 第二种变体重复该模式,直到目标中的字节超过向量大小,然后始终复制向量大小的块(并移动源窗口)而不使用 CopyBlock。
但是,对于 2 到 32 之间的模式大小(在我的例子中是矢量大小),我现在在运行时得到奇怪的结果。 我怀疑这与从移动源 window 读取有关,因为将 window 加倍会使执行时间减半。 对于大于向量大小的大小,我得到了预期的结果:
方法 | 字节数 | 数数 | 意思 | 错误 | 标准偏差 |
---|---|---|---|---|---|
重复复制块 | 3个 | 16 | 19.38 纳秒 | 0.002纳秒 | 0.002纳秒 |
Repeat_NoCopyBlock | 3个 | 16 | 13.90 纳秒 | 0.106 纳秒 | 0.100 纳秒 |
重复复制块 | 3个 | 128 | 25.00 纳秒 | 0.005纳秒 | 0.005纳秒 |
Repeat_NoCopyBlock | 3个 | 128 | 39.31 纳秒 | 0.135 纳秒 | 0.126 纳秒 |
重复复制块 | 12 | 16 | 10.64 纳秒 | 0.037 纳秒 | 0.031纳秒 |
Repeat_NoCopyBlock | 12 | 16 | 13.35 纳秒 | 0.024 纳秒 | 0.023纳秒 |
重复复制块 | 12 | 128 | 25.56 纳秒 | 0.020纳秒 | 0.019纳秒 |
Repeat_NoCopyBlock | 12 | 128 | 108.61 纳秒 | 0.164 纳秒 | 0.154 纳秒 |
重复复制块 | 16 | 16 | 68.74 纳秒 | 0.010 纳秒 | 0.009纳秒 |
Repeat_NoCopyBlock | 16 | 16 | 13.50 纳秒 | 0.002纳秒 | 0.002纳秒 |
重复复制块 | 16 | 128 | 81.41 纳秒 | 0.024 纳秒 | 0.022纳秒 |
Repeat_NoCopyBlock | 16 | 128 | 81.52 纳秒 | 0.067 纳秒 | 0.062 纳秒 |
重复复制块 | 48 | 16 | 48.84 纳秒 | 0.045 纳秒 | 0.042纳秒 |
Repeat_NoCopyBlock | 48 | 16 | 23.80 纳秒 | 0.089纳秒 | 0.083纳秒 |
重复复制块 | 48 | 128 | 364.76 纳秒 | 0.053纳秒 | 0.045 纳秒 |
Repeat_NoCopyBlock | 48 | 128 | 165.34 纳秒 | 0.145 纳秒 | 0.136 纳秒 |
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 中,重叠存储很快,例如,对于 10 字节的模式,您可以执行 16 字节的 SIMD 存储并将指针递增 10。
但更有效的是在多个寄存器中展开模式并展开一些循环。 理想情况下为lowest_common_multiple(pattern, vector_width)
,但即使只是展开 3x 以填充 32 字节向量的大部分也是好的。 (或者在没有 AVX 的情况下,跨一对 16 字节向量,所以两个不重叠的存储总共 32 字节)。 尤其是当重复次数不多时,您不能永远花时间设置向量。
或者为了使更长的模式更容易设置(无需读取 src 缓冲区的边界之外):借用 glibc memcpy 的策略,例如执行一个 30 字节的副本,其中包含两个重叠的 16 字节加载,一个从开头开始,一个最后结束。 因此,在主循环中,您将执行一系列可能重叠的 N 存储,然后将存储接下来的 30 个字节,而不会与第一个字节重叠。
嗯,但是可变数量的寄存器不容易循环,这需要不同的循环。 也许总是 4 个向量寄存器,但它们之间有可变的偏移量,因此单个循环可以使用索引寻址模式和指针增量。 (这对于在 Ice Lake 之前在英特尔的 AGU 上运行的商店来说并不理想(port7 AGU 仅处理 1-register 寻址模式),但它们不会与来自该逻辑核心的任何负载竞争,所以它可能没问题。)也许一些偏移量可以固定在矢量宽度上,只有最后一个矢量可能与第三个矢量部分重叠。
因此,设置代码将确定有多少重复的模式适合 3 到 4 倍的矢量宽度,以及其中的重叠。 不幸的是, palign
仅适用于立即计数,如果您使用较窄的存储以当前方式将模式的前几次迭代放入目标缓冲区,然后从那里重新加载到 XMM 或YMM 寄存器。 (并且多个 SF 档不能重叠它们的延迟。)
IDK 让 C# 的 JIT 像那样发出 asm 是多么容易,无论是使用Vector<>
intrinsics 还是Sse2.whatever
/ Avx.whater
; 除了 SO 答案之外,我没有将 C# 用于任何事情; 我只是想为您指明一个好的目标的方向。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.