Skip to content

Commit 7777571

Browse files
paulirwinclaude
andcommitted
Inline ReadIntXX slow path; split ReadBytes into ref-byte core
The slow path of ReadInt16/32/64 routed through base.ReadIntXX, which called back into our ReadByte 2-8 times via virtual dispatch. Replace with stackalloc + ReadBytes + BinaryPrimitives.ReadXxxBigEndian so the JIT can devirtualize the inner reads. ReadBytes(byte[]) and ReadBytes(Span<byte>) now share a private ReadBytesCore(ref byte, int) that takes a raw ref to skip the per-call Span ctor and per-iteration Slice/GetReference. Adds a small-copy switch (1/2/4/8) so the int slow paths avoid CopyBlockUnaligned entry overhead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3cec899 commit 7777571

1 file changed

Lines changed: 88 additions & 13 deletions

File tree

src/Lucene.Net/Store/MMapDirectory.cs

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO.MemoryMappedFiles;
88
using System.Linq;
99
using System.Runtime.CompilerServices;
10+
using System.Runtime.InteropServices;
1011
using System.Threading;
1112
using SCG = System.Collections.Generic;
1213

@@ -520,7 +521,13 @@ public override short ReadInt16()
520521
position = pos + 2;
521522
return v;
522523
}
523-
return base.ReadInt16();
524+
// Slow path: 2 bytes straddle a chunk boundary. Fill via
525+
// ReadBytes (which handles the crossing) and decode big-endian
526+
// to match the fast path. Avoids the 2× virtcall round-trip
527+
// through base.ReadInt16 -> ReadByte.
528+
Span<byte> buf = stackalloc byte[2];
529+
ReadBytes(buf);
530+
return BinaryPrimitives.ReadInt16BigEndian(buf);
524531
}
525532

526533
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -534,7 +541,10 @@ public override int ReadInt32()
534541
position = pos + 4;
535542
return v;
536543
}
537-
return base.ReadInt32();
544+
// Slow path: see ReadInt16.
545+
Span<byte> buf = stackalloc byte[4];
546+
ReadBytes(buf);
547+
return BinaryPrimitives.ReadInt32BigEndian(buf);
538548
}
539549

540550
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -548,44 +558,109 @@ public override long ReadInt64()
548558
position = pos + 8;
549559
return v;
550560
}
551-
return base.ReadInt64();
561+
// Slow path: see ReadInt16.
562+
Span<byte> buf = stackalloc byte[8];
563+
ReadBytes(buf);
564+
return BinaryPrimitives.ReadInt64BigEndian(buf);
552565
}
553566

554567
public override void ReadBytes(byte[] b, int offset, int len)
555568
{
556-
ReadBytes(new Span<byte>(b, offset, len));
569+
if (b is null)
570+
{
571+
throw new ArgumentNullException(nameof(b));
572+
}
573+
if ((uint)offset > (uint)b.Length || (uint)len > (uint)(b.Length - offset))
574+
{
575+
throw new ArgumentOutOfRangeException(nameof(offset),
576+
$"offset/len out of range: offset={offset}, len={len}, b.Length={b.Length}");
577+
}
578+
if (len == 0) return;
579+
580+
ReadBytesCore(ref b[offset], len);
557581
}
558582

559583
public override void ReadBytes(Span<byte> destination)
584+
{
585+
int len = destination.Length;
586+
if (len == 0) return;
587+
588+
ReadBytesCore(ref MemoryMarshal.GetReference(destination), len);
589+
}
590+
591+
// Shared inner loop for both ReadBytes overloads. Takes a raw
592+
// ref + length so the byte[] path doesn't pay for a Span ctor +
593+
// GetReference round-trip, and the per-iteration slice/GetReference
594+
// pair is replaced with a single Unsafe.Add.
595+
//
596+
// The byte[] overload is responsible for its own bounds checking
597+
// (Span's ctor checks for free; here we have to do it manually).
598+
private void ReadBytesCore(ref byte destination, int length)
560599
{
561600
if (Volatile.Read(ref instanceClosed) != 0)
562601
{
563602
throw AlreadyClosedException.Create(this.GetType().FullName, "Already disposed: " + this);
564603
}
565604

566-
int len = destination.Length;
567-
if (len == 0) return;
568-
569605
long pos = position;
570-
if (pos + len > length)
606+
if (pos + length > this.length)
571607
{
572608
throw EOFException.Create("read past EOF: " + this);
573609
}
574610

611+
int remaining = length;
575612
int dstOff = 0;
576-
while (len > 0)
613+
614+
while (remaining > 0)
577615
{
578616
if (pos >= currentEnd)
579617
{
580618
EnsureCurrentChunk(pos);
581619
}
582-
int inChunk = (int)Math.Min(currentEnd - pos, (long)len);
620+
621+
long available = currentEnd - pos;
622+
int inChunk = (int)(available < remaining ? available : remaining);
623+
583624
ref byte src = ref Unsafe.AsRef<byte>(readBase + pos);
584-
ref byte dst = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(destination.Slice(dstOff));
585-
Unsafe.CopyBlockUnaligned(ref dst, ref src, (uint)inChunk);
625+
ref byte dst = ref Unsafe.Add(ref destination, dstOff);
626+
627+
// Small-copy fast path (≤ 8 bytes). Unsafe.CopyBlockUnaligned
628+
// has nontrivial entry overhead for tiny copies; using a
629+
// sized read/write avoids it for the common short-read case
630+
// (e.g., the slow path of ReadInt16/Int32/Int64).
631+
if (inChunk <= 8)
632+
{
633+
switch (inChunk)
634+
{
635+
case 8:
636+
Unsafe.WriteUnaligned(ref dst, Unsafe.ReadUnaligned<ulong>(ref src));
637+
break;
638+
case 4:
639+
Unsafe.WriteUnaligned(ref dst, Unsafe.ReadUnaligned<uint>(ref src));
640+
break;
641+
case 2:
642+
Unsafe.WriteUnaligned(ref dst, Unsafe.ReadUnaligned<ushort>(ref src));
643+
break;
644+
case 1:
645+
dst = src;
646+
break;
647+
default:
648+
// 3, 5, 6, 7 — uncommon; fall through to byte loop.
649+
for (int i = 0; i < inChunk; i++)
650+
{
651+
Unsafe.Add(ref dst, i) = Unsafe.Add(ref src, i);
652+
}
653+
break;
654+
}
655+
}
656+
else
657+
{
658+
Unsafe.CopyBlockUnaligned(ref dst, ref src, (uint)inChunk);
659+
}
660+
586661
pos += inChunk;
587662
dstOff += inChunk;
588-
len -= inChunk;
663+
remaining -= inChunk;
589664
}
590665
position = pos;
591666
}

0 commit comments

Comments
 (0)