|
using System; |
|
using System.Buffers; |
|
using System.Buffers.Text; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using BenchmarkDotNet.Attributes; |
|
using BenchmarkDotNet.Attributes.Jobs; |
|
using BenchmarkDotNet.Running; |
|
|
|
namespace SpanDemo |
|
{ |
|
static class Program |
|
{ |
|
static void Main() |
|
{ |
|
BenchmarkRunner.Run<SumIntStrings>(); |
|
} |
|
} |
|
|
|
[CoreJob] |
|
[ClrJob] |
|
public class SumIntStrings |
|
{ |
|
private List<string[]> _testData; |
|
private const int NUM_COLS = 14; |
|
|
|
[GlobalSetup] |
|
public void Setup() |
|
{ |
|
_testData = new List<string[]>(); |
|
var theRandom = new Random(); |
|
for (int i = 0; i < 10000; i++) |
|
{ |
|
_testData.Add(Enumerable.Range(0, NUM_COLS).Select(_ => theRandom.Next().ToString()).ToArray()); |
|
} |
|
} |
|
|
|
[Benchmark] |
|
public int OldWay() |
|
{ |
|
int result = 0; |
|
foreach (var line in BaselineParser.GetLineParsers(_testData)) |
|
{ |
|
for (int i = 0; i < NUM_COLS; i++) |
|
{ |
|
result += line.TryParseInt(i) ?? 0; |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
[Benchmark] |
|
public int WithSpan() |
|
{ |
|
int result = 0; |
|
using (var lineSource = new Depiecifier(_testData)) |
|
{ |
|
foreach (var line in lineSource) |
|
{ |
|
for (int i = 0; i < NUM_COLS; i++) |
|
{ |
|
result += line.TryParseInt(i) ?? 0; |
|
} |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
} |
|
|
|
|
|
static class BaselineParser |
|
{ |
|
public static IEnumerable<BaslineLineParser> GetLineParsers(IEnumerable<string[]> lines) |
|
{ |
|
return lines.Select(line => new BaslineLineParser(line)); |
|
} |
|
|
|
public class BaslineLineParser |
|
{ |
|
private string[] Pieces { get; } |
|
|
|
public BaslineLineParser(string[] line) => Pieces = line; |
|
|
|
public int? TryParseInt(int col) |
|
{ |
|
if (col < Pieces.Length) |
|
{ |
|
if (int.TryParse(Pieces[col], out var result)) |
|
{ |
|
return result; |
|
} |
|
} |
|
return null; |
|
} |
|
} |
|
} |
|
|
|
public class Depiecifier : IDisposable |
|
{ |
|
private byte[] _byteBuffer; |
|
private int[] _indexBuffer; |
|
readonly IEnumerable<string[]> _lineCollection; |
|
|
|
public Depiecifier(IEnumerable<string[]> lineCollection) |
|
{ |
|
_lineCollection = lineCollection; |
|
_byteBuffer = ArrayPool<byte>.Shared.Rent(65536); |
|
_indexBuffer = ArrayPool<int>.Shared.Rent(512); |
|
} |
|
|
|
public DepieceEnumerator GetEnumerator() => new DepieceEnumerator(_byteBuffer, _indexBuffer, _lineCollection.GetEnumerator()); |
|
|
|
public ref struct DepieceEnumerator |
|
{ |
|
private readonly byte[] _byteBuffer; |
|
private readonly int[] _indexBuffer; |
|
|
|
private ReadOnlySpan<byte> _lineBytes; |
|
private ReadOnlySpan<int> _lineIndices; |
|
|
|
private readonly IEnumerator<string[]> _lineEnumerator; |
|
|
|
public DepieceEnumerator(byte[] byteBuffer, int[] indexBuffer, IEnumerator<string[]> lineEnumerator) |
|
{ |
|
_byteBuffer = byteBuffer; |
|
_indexBuffer = indexBuffer; |
|
_lineEnumerator = lineEnumerator; |
|
_lineBytes = default; |
|
_lineIndices = default; |
|
indexBuffer[0] = 0; |
|
} |
|
|
|
public SpanLineParser Current => new SpanLineParser(in _lineBytes, in _lineIndices); |
|
|
|
|
|
public bool MoveNext() |
|
{ |
|
if (_lineEnumerator.MoveNext()) |
|
{ |
|
var linePieces = _lineEnumerator.Current; |
|
var byteSpan = (Span<byte>)_byteBuffer; |
|
int bytesIdx = 0; |
|
for (int i = 0; i < linePieces.Length; i++) |
|
{ |
|
_indexBuffer[i] = bytesIdx; |
|
var piecesSpan = linePieces[i].AsSpan(); |
|
foreach (var piece in piecesSpan) |
|
{ |
|
byteSpan[bytesIdx++] = (byte)piece; |
|
} |
|
|
|
byteSpan[bytesIdx++] = (byte)'\t'; |
|
} |
|
_lineBytes = byteSpan.Slice(0, bytesIdx); |
|
_lineIndices = _indexBuffer.AsSpan(0, linePieces.Length); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
public readonly ref struct SpanLineParser |
|
{ |
|
private readonly ReadOnlySpan<byte> _span; |
|
private readonly ReadOnlySpan<int> _colStarts; |
|
|
|
|
|
public SpanLineParser(in ReadOnlySpan<byte> memory, in ReadOnlySpan<int> colStarts) |
|
{ |
|
_span = memory; |
|
_colStarts = colStarts; |
|
|
|
} |
|
|
|
public int? TryParseInt(int col) |
|
{ |
|
if ((uint)col < (uint)_colStarts.Length) |
|
{ |
|
//Doesn't chop off trailing \t, to save a touch some math cost for parsing that doesn't care |
|
var parseSpan = _span.Slice(_colStarts[col]); |
|
|
|
Utf8Parser.TryParse(parseSpan, out int result, out _); |
|
return result; |
|
} |
|
|
|
return null; |
|
} |
|
} |
|
|
|
} |
|
|
|
public void Dispose() |
|
{ |
|
ArrayPool<byte>.Shared.Return(_byteBuffer); |
|
_byteBuffer = null; |
|
ArrayPool<int>.Shared.Return(_indexBuffer); |
|
_indexBuffer = null; |
|
} |
|
} |
|
} |