C#Memory.Span异常慢

时间:2019-05-24 03:12:41

标签: c#

在尝试新的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的人都在追求速度优势,而通用对象字段似乎鼓励不使用它。

0 个答案:

没有答案