77using System . IO . MemoryMappedFiles ;
88using System . Linq ;
99using System . Runtime . CompilerServices ;
10+ using System . Runtime . InteropServices ;
1011using System . Threading ;
1112using 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