在尝试新的Span<byte>
和Memory<byte>
功能时,我发现与其他与字节数组进行交互的方法相比,使用Memory<byte>
解析二进制数据的速度比我预期的要慢得多。 / p>
我建立了一个基准测试套件,它使用多种方法从数组中读取单个整数,并发现内存是最慢的。正如预期的那样,它比Span慢,但令人惊讶的是,它也比直接使用阵列以及我希望内存在内部使用的本地版本慢。
// Suite of tests comparing various ways to read an offset int from an array
public class BinaryTests
{
static byte[] arr = new byte[] { 0, 1, 2, 3, 4 };
static Memory<byte> mem = arr.AsMemory();
static HomegrownMemory memTest = new HomegrownMemory(arr);
[Benchmark]
public int StraightArrayBitConverter()
{
return BitConverter.ToInt32(arr, 1);
}
[Benchmark]
public int MemorySlice()
{
return BinaryPrimitives.ReadInt32LittleEndian(mem.Slice(1).Span);
}
[Benchmark]
public int MemorySliceToSize()
{
return BinaryPrimitives.ReadInt32LittleEndian(mem.Slice(1, 4).Span);
}
[Benchmark]
public int MemorySpanSlice()
{
return BinaryPrimitives.ReadInt32LittleEndian(mem.Span.Slice(1));
}
[Benchmark]
public int MemorySpanSliceToSize()
{
return BinaryPrimitives.ReadInt32LittleEndian(mem.Span.Slice(1, 4));
}
[Benchmark]
public int HomegrownMemorySlice()
{
return BinaryPrimitives.ReadInt32LittleEndian(memTest.Slice(1).Span);
}
[Benchmark]
public int HomegrownMemorySliceToSize()
{
return BinaryPrimitives.ReadInt32LittleEndian(memTest.Slice(1, 4).Span);
}
[Benchmark]
public int HomegrownMemorySpanSlice()
{
return BinaryPrimitives.ReadInt32LittleEndian(memTest.Span.Slice(1));
}
[Benchmark]
public int HomegrownMemorySpanSliceToSize()
{
return BinaryPrimitives.ReadInt32LittleEndian(memTest.Span.Slice(1, 4));
}
[Benchmark]
public int SpanSlice()
{
return BinaryPrimitives.ReadInt32LittleEndian(arr.AsSpan().Slice(1));
}
[Benchmark]
public int SpanSliceToSize()
{
return BinaryPrimitives.ReadInt32LittleEndian(arr.AsSpan().Slice(1, 4));
}
}
// Personal "implementation" of Memory<T>, for testing
struct HomegrownMemory
{
byte[] _arr;
int _startPos;
int _length;
public HomegrownMemory(byte[] b)
{
this._arr = b;
this._startPos = 0;
this._length = b.Length;
}
public Span<byte> Span => _arr.AsSpan(start: _startPos, length: _length);
public HomegrownMemory Slice(int start)
{
return new HomegrownMemory()
{
_arr = _arr,
_startPos = _startPos + start,
_length = _length - start
};
}
public HomegrownMemory Slice(int start, int length)
{
return new HomegrownMemory()
{
_arr = _arr,
_startPos = _startPos + start,
_length = length
};
}
}
以下是上述代码的BenchmarkNet结果:
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.765 (1803/April2018Update/Redstone4)
Intel Core i7-4790K CPU 4.00GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
Frequency=3984652 Hz, Resolution=250.9629 ns, Timer=TSC
.NET Core SDK=2.1.700-preview-009618
[Host] : .NET Core 2.1.11 (CoreCLR 4.6.27617.04, CoreFX 4.6.27617.02), 64bit RyuJIT
DefaultJob : .NET Core 2.1.11 (CoreCLR 4.6.27617.04, CoreFX 4.6.27617.02), 64bit RyuJIT
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------------------- |----------:|----------:|----------:|------:|------:|------:|----------:|
| StraightArrayBitConverter | 1.0832 ns | 0.0323 ns | 0.0270 ns | - | - | - | - |
| MemorySlice | 5.8882 ns | 0.0654 ns | 0.0612 ns | - | - | - | - |
| MemorySliceToSize | 6.0191 ns | 0.0983 ns | 0.0919 ns | - | - | - | - |
| MemorySpanSlice | 5.0230 ns | 0.0626 ns | 0.0555 ns | - | - | - | - |
| MemorySpanSliceToSize | 5.0189 ns | 0.0335 ns | 0.0313 ns | - | - | - | - |
| HomegrownMemorySlice | 3.9217 ns | 0.0419 ns | 0.0392 ns | - | - | - | - |
| HomegrownMemorySliceToSize | 1.5233 ns | 0.0199 ns | 0.0186 ns | - | - | - | - |
| HomegrownMemorySpanSlice | 0.8301 ns | 0.0243 ns | 0.0227 ns | - | - | - | - |
| HomegrownMemorySpanSliceToSize | 0.8303 ns | 0.0223 ns | 0.0208 ns | - | - | - | - |
| SpanSlice | 0.6891 ns | 0.0241 ns | 0.0214 ns | - | - | - | - |
| SpanSliceToSize | 0.6804 ns | 0.0174 ns | 0.0163 ns | - | - | - | - |
除了Memory<T>
的计时比我预期的要慢之外,所有这些计时对我来说都是很有意义的。
据我了解,Memory<T>
只是Span<T>
的一种实现,它可以存在于堆中,例如..不是引用结构。
我希望它的执行速度比Span慢,但至少要比Straight Array实现快一些。我使用本地版本获得的结果是我期望从Memory<T>
在这里,关于Memory<T>
的用例是否缺少一些基本知识,或者它想实现什么?看到这些结果后,我的理解似乎有些不对。
编辑: 在Cowen发表评论之后,我找到了Memory源代码并进行了查看。在检索范围时,它确实做了很多事情,特别是检查并强制转换其通用对象字段以找出其类型,以便正确进行强制转换。
令我惊讶的是,他们没有使用不同的Memory选项和/或提供Memory工厂来构造具有更强类型内部数据字段的类。相反,他们选择了一个字段,该字段必须经常检查/投射才能到达Span,这是我认为应该/将在使用过程中不断发生的事情。
我仍然很好奇他们为什么如此设计内存,更重要的是,以这种方式设计了哪些用例。我觉得很多使用Span / Memory的人都在追求速度优势,而通用对象字段似乎鼓励不使用它。