[LOADED_DOCS: 2 files, no new loads]

Optimize AcBinary: add IsTrustedSingleSegment fast-path

Introduce static abstract IsTrustedSingleSegment to IBinaryInputBase and implement in all input types. Update EnsureAvailable to leverage JIT specialization for array-backed inputs, eliminating per-read overhead. Add detailed XML docs on performance trade-offs. No breaking changes; improves deserialization efficiency and clarity.
This commit is contained in:
Loretta 2026-05-10 16:19:07 +02:00
parent 81bc41c118
commit eb4b6e7f8f
6 changed files with 67 additions and 0 deletions

View File

@ -651,9 +651,29 @@ public static partial class AcBinaryDeserializer
#endregion #endregion
/// <summary>
/// Ensures <paramref name="length"/> bytes are readable from the current <c>_buffer[_position]</c>
/// position, advancing to the next segment via <see cref="IBinaryInputBase.TryAdvanceSegment"/> when
/// the current buffer is exhausted.
/// <para><b>JIT specialization fast-path</b>: when <c>TInput.IsTrustedSingleSegment</c> is the
/// constant <c>true</c> (e.g. <see cref="ArrayBinaryInput"/>), the entire method body is eliminated
/// at JIT time — bounds-check + segment-advance both vanish. Per-read overhead drops to ~0 ns.
/// Trade-off: corrupt-wire detection downgrades from <see cref="AcBinaryDeserializationException"/>
/// to a generic <see cref="System.IndexOutOfRangeException"/>; acceptable for trusted byte[] inputs
/// where the buffer is already validated by the caller.</para>
/// <para>For non-trusted inputs (<see cref="SequenceBinaryInput"/>, AsyncPipeReaderInputAdapter), the
/// guard's <c>if (false) return;</c> form is dead-code-eliminated — the bounds-check and segment-advance
/// keep their original behaviour with zero added overhead.</para>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureAvailable(int length) private void EnsureAvailable(int length)
{ {
// Trusted-single-segment fast path — JIT folds the constant per TInput specialization:
// ArrayBinaryInput → if (true) return; → method body entirely eliminated
// SequenceBinaryInput → if (false) return; → guard eliminated, bounds-check kept
// AsyncPipeReaderInput → if (false) return; → guard eliminated, bounds-check kept
if (TInput.IsTrustedSingleSegment) return;
if (_position > _bufferLength - length) if (_position > _bufferLength - length)
{ {
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length)) if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length))

View File

@ -12,6 +12,13 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public struct ArrayBinaryInput : IBinaryInputBase public struct ArrayBinaryInput : IBinaryInputBase
{ {
/// <summary>
/// Single-buffer input — entire payload loaded upfront, no cross-segment advance needed.
/// JIT-constant <c>true</c> → <c>BinaryDeserializationContext.EnsureAvailable</c> body is
/// dead-code-eliminated for this specialization (per-read bounds-check overhead drops to ~0 ns).
/// </summary>
public static bool IsTrustedSingleSegment => true;
private readonly byte[] _data; private readonly byte[] _data;
private readonly int _offset; private readonly int _offset;
private readonly int _length; private readonly int _length;

View File

@ -91,6 +91,13 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public sealed class AsyncPipeReaderInput : IBinaryInputBase, IDisposable public sealed class AsyncPipeReaderInput : IBinaryInputBase, IDisposable
{ {
/// <summary>
/// Streaming input — chunks arrive over time via <see cref="Feed"/>; the read path must block / advance
/// on chunk-arrival via <see cref="TryAdvanceSegment"/>. JIT-constant <c>false</c> → context's
/// <c>EnsureAvailable</c> bounds-check + segment-advance kept (essential for streaming-handoff).
/// </summary>
public static bool IsTrustedSingleSegment => false;
private byte[] _buffer; private byte[] _buffer;
private int _writePos; private int _writePos;
private int _readPos; // consumer reports consumed position here private int _readPos; // consumer reports consumed position here

View File

@ -35,6 +35,13 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
internal readonly struct AsyncPipeReaderInputAdapter : IBinaryInputBase internal readonly struct AsyncPipeReaderInputAdapter : IBinaryInputBase
{ {
/// <summary>
/// Streaming input — chunks arrive over time via <see cref="AsyncPipeReaderInput.Feed"/>; the read path
/// must block / advance on chunk-arrival. JIT-constant <c>false</c> → <c>EnsureAvailable</c> bounds-check
/// + <see cref="TryAdvanceSegment"/> kept (essential for streaming-handoff).
/// </summary>
public static bool IsTrustedSingleSegment => false;
private readonly AsyncPipeReaderInput _input; private readonly AsyncPipeReaderInput _input;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -15,6 +15,22 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public interface IBinaryInputBase public interface IBinaryInputBase
{ {
/// <summary>
/// Compile-time JIT-constant flag — true when the input is guaranteed to be a single contiguous buffer
/// loaded entirely upfront (no cross-segment advance needed, no streaming-handoff needed). Used by
/// <c>BinaryDeserializationContext.EnsureAvailable</c> to skip the bounds-check-and-segment-advance
/// path on trusted inputs: <c>if (TInput.IsTrustedSingleSegment) return;</c> folds to a no-op via JIT
/// generic-struct specialization, eliminating the per-read overhead.
/// <para>Set <c>true</c> when: <see cref="ArrayBinaryInput"/> (single byte[] buffer, fully loaded).</para>
/// <para>Set <c>false</c> when: <see cref="SequenceBinaryInput"/> (multi-segment ROS — cross-segment advance required),
/// AsyncPipeReaderInputAdapter (streaming — chunk-arrival handoff required), or any other input that may
/// dynamically advance buffers during deserialization.</para>
/// <para><b>Trade-off in trusted mode</b>: corrupt-wire detection downgrades from a domain-specific
/// <c>AcBinaryDeserializationException</c> to a generic <c>IndexOutOfRangeException</c>. Acceptable
/// for byte[]-API consumers — the buffer is already validated by the caller.</para>
/// </summary>
static abstract bool IsTrustedSingleSegment { get; }
/// <summary> /// <summary>
/// Provides the initial buffer, starting position, and buffer length. /// Provides the initial buffer, starting position, and buffer length.
/// Called once before deserialization begins. /// Called once before deserialization begins.

View File

@ -18,6 +18,16 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public struct SequenceBinaryInput : IBinaryInputBase public struct SequenceBinaryInput : IBinaryInputBase
{ {
/// <summary>
/// Multi-segment input — cross-segment advance via <see cref="TryAdvanceSegment"/> is required even on
/// nominally single-segment sequences (the wire shape isn't known statically here; multi-segment is the
/// safe assumption). JIT-constant <c>false</c> → <c>EnsureAvailable</c> bounds-check + advance kept.
/// <para>Note: callers that have a single-segment array-backed sequence are redirected to the byte[] overload
/// at the entry point (see <c>AcBinaryDeserializer.Deserialize(ReadOnlySequence&lt;byte&gt;)</c>), which
/// uses <see cref="ArrayBinaryInput"/> and thus benefits from the trusted-single-segment fast path.</para>
/// </summary>
public static bool IsTrustedSingleSegment => false;
private ReadOnlySequence<byte> _sequence; private ReadOnlySequence<byte> _sequence;
private SequencePosition _nextPosition; private SequencePosition _nextPosition;