Status: Needs more work | API Ref | Issue | Video
- Two types are being proposed here:
SequenceReader<T>: wraps aReadOnlySequence<T>SpanReader<T>: wraps aReadOnlySpan<T>
- The reason being that
SpanReader<T>can be faster asSequenceReader<T>because it has to do extra work when it runs out of the span. The mere existing of an extra call can cause the compiler to stack spill locals which in turn has regressed some micro benchmarks by 80%. - The downside of having two types is that the API surface bifurcates, i.e. some
APIs will take
SequenceReader<T>, some will takeSpanReader<T>. An author of a parsing API will have to decide. Supporting both would cause duplication as you can't funnel them (easily) through the same code path. - We could do these things:
- We can ship a third type that can wrap either a sequence or a span
- We can ship two types (one can wrap either a sequence or a span) and one that only wraps span.
- We can only ship a single type that wraps either a sequence or a span and ship a span-only one later if we discover we need the performance.
- None of this is accessible to languages that do not support
ref structs, such as Visual Basic.
Rewindseems a bit weird- Can we unify
AdvanceandRewindintoSeek()where the offset can be positive or negative. Advanceis perf critical because the actual reader methods that need to advance are extension methods.Peek(Span<T>)might be a performance trap as callers would want to avoid the cost of zeroing out the passed in scratch buffer as it's rarely used. Counter argument from @stephentoub:If
Peekreturned the number ofTs actually peeked, why would the span need to be zeroed out beforehand? (It looks like it's currently defined to return aReadOnlySpan<T>, and presumably that logically incorporates that by being of the correct length?)IsNextis a bit weird. Maybe we should we useStartsWithandStartsWithAny. Also, remove theadvancePastbool parameter.- Skip method:
SkipPast->AdvancePastTrySkipTo->AdvanceTo- We shouldn't name them
Advanceas they could conflict with the existingAdvance(long)method whenTislong.
- Should we remove the
unamangedconstrained and replace with a.ctorcheck?- Done for pinning and being able to take a pointer?
- Can we have some sample code that uses a
ReadOnlySpan<T>-based reader vs. aReadOnlySequence<T>-based reader?- We can then compare the per gains
- We can then compare the usability gains
- From @stephentoub:
And specifically (since I'm not sure I was clear in the meeting), here's what I was hoping to see. Given some common scenario that reads from / parses data from a pipeline:
- Use the BufferReader that only supports span, and have a single method (written as we would in production code as best we can) with all of the relevant boilerplate that handles interaction both with the pipeline and with managing sequences/calls to the span-based reader.
- Use the BufferReader that supports sequence, and have a single method (written as we would in production code as best we can) with all of the relevant boilerplate that handles interaction both with the pipeline and with the sequence-based reader.
I'd like to us to be able to look at the code the provides the same functionality and the benchmarks on it and compare side-by-side both usability and performance. If we could do the test as part of kestrel (e.g. two different runnable commits off of master) and look at plaintext benchmarks and what the kestrel code looks like both ways, even better.
If the version with the span-based reader is totally incomprehensible and provides only modest gains, then that leads us one way. If the version with the span-based reader is only mildly more cumbersome and provides significant gains, then that leads us another way. Etc. I just want to make sure we're all in agreement on the goals, seeing the same data, and then coming to a joint conclusion.
AtLastSegment. The logic outlined in the comment is wrong, you need to honor position, and not check whether there is next segment.Allocatorseems odd. If we need it, should we promote the delegate or move it elsewhere? We can't use aFuncdue to the generic instantiation issue withSpan<T>.- Should the allocator be passed in as argument? Storing it as a field might cause additional memory barrier
AreAnyNext->IsNextAny- Suggestion from last time was to rename
IsNexttoStartsWithbut the latter isn't clear whether it's relative to the current position vs. the buffer's start. - We may want to make
advancePastDelimiterdifferent methods to avoid the perf issue due to the extra branches. - The struct has to be passed by
refor you have bugs. Also, since it's large, passing it by value would also tank perf.
- All methods have a limit of reading 128
Ts- This allows stack allocated buffers to be used and seemed rationale
- Might be too small for specialized scenarios, such as many leading zeros
- We cannot really expose APIs with sizes as this would likely destroy perf
- No APIs can produce strings, we should probably add that as dealing with things going across buffers is hard and we don't want folks to go through an intermediary array.
- Can't we make this an instance method?
- Move to
SequenceMarshal