我正在使用System.Buffers.Binary.BinaryPrimitives
以精确的方式将值写入字节数组。没有来自MS的示例,我可以看到几种方法来完成它,但我不确定其中一种是否比另一种更好。原则上,创建大量Span<byte>
对象的需要似乎不太理想?
考虑这个简单的例子:
//writes these values in this order to a new 16-byte buffer
byte[] PopulateBuffer(int i1,int i2,Int16 s1,Int16 s2)
{
var buffer = new byte[16]; //padded based on external protocol
var span = new Span<byte>(buffer);
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(0,4),i1);
BinaryPrimitives.WriteInt32LittleEndian(span.Slice(4,4),i2);
BinaryPrimitives.WriteInt16LittleEndian(span.Slice(8,2),s1);
BinaryPrimitives.WriteInt16LittleEndian(span.Slice(10,2),s2);
return buffer;
}
我在这里示例化了5个Span
对象。与老式的通过位移位手动获取字节的方法相比,这看起来确实很混乱,但实际上开销很大吗?有没有更好的方法来使用这个类?
2条答案
按热度按时间xqkwcwgp1#
TL;DR:从下面的结果来看,基于
Span
的方法似乎比替代方法要快得多。注意,
Span<T>
是一个值类型,JIT在看穿它方面做得相当不错。我创建了一个简化测试:
哪个JIT用于:
正如您所看到的,这两个版本的复杂性差别很小,只是使用
BinaryPrimitives
的版本有一些范围检查(这不是坏事)。请注意,JIT现在是多层的,我认为SharpLab只显示第一层的结果,所以如果它在热路径上,这可能会得到很好的改善。
SharpLab链接
我还使用BenchmarkDotNet运行了一个基准测试:
结果如下:
也许令人惊讶的是,基于
Span
的方法比位操作快得多,这可能是因为x86是little-endian,BinaryPrimitives
意识到它可以直接将值位块传输到数组中,而无需单独提取和分配每个字节,但BE变体也显示出相当大的差异。byqmnocz2#
Span
s是一个ref struct
类型。创建span将创建非常简洁的对象,引用原始数组和您声明的范围的开始到结束位置。Span
、ReadOnlySpan
和Memory
是专门为更好地处理序列(尤其是内存/字节序列)而引入的。你可以把Spans看作数组段指针。创建、复制和访问这种指针结构的成本相对较低。当操作它时,你仍然在原始的底层数组上操作。(不使用额外的数组示例。)
你提到了位移位,我想你的意思是作为
BinaryPrimitives.WriteInt32LittleEndian
的替代品。如果不使用 * WriteInt 32 * 方法,而是手动提取四个字节,并按索引将它们设置到数组中,则会投入多个位操作,并可能破坏线性向量操作,而这些操作可以通过SIMD指令、分支预测和缓存进行CPU优化。
很难预测哪种方法性能更好,为了确定性能差异,您必须专门测试您的用例。
通常,使用Span不是一个昂贵的操作,使用标准库提供的方法更可取[而不是手动复制它们的行为]。