Skip to content

BufferedStreamReader Class

The BufferedStreamReader is a buffering mechanism for reading data from streams, allowing for fast reading of data while preserving stream-like semantics.

BufferedStreamReader is a high-performance replacement for BinaryReader with minimal error checking.

BufferedStreamReader is not thread-safe.

BufferedStreamReader cannot read values larger than buffer size it was initialised with.

BufferedStreamReader has minimal error checking at runtime.

Tip

Remember, always ensure the stream and reader are properly disposed of after use. The best practice is to use the using statement or try-finally block in C# to ensure resources are correctly released.

Performance

Amount of time taken to read 8 MiB of data (library version 9.0.0):

Legend:
- BinaryReader read via BinaryReader.
- BufferedStreamReader read via BufferedStreamReader.
- BufferedStreamReader read via BufferedStreamReader (ReadRaw method).
- NativePointer no stream; no copy; access existing data from array (baseline reference).

MemoryStream

Benchmarks on MemoryStream (near zero overhead) allow us to compare the performance against BinaryReader and array access (baseline reference).

Byte:

Method Mean Code Size
BinaryReader 15,452.7 us 171 B
BufferedStreamReader 6,298.7 us 703 B
BufferedStreamReader Raw 1,997.3 us 669 B
NativePointer 1,845.2 us 38 B

Int:

Method Mean Code Size
BinaryReader 3,253.2 us 637 B
BufferedStreamReader 1,708.7 us 981 B
BufferedStreamReader Raw 606.8 us 760 B
NativePointer 464.1 us 54 B

Long:

Method Mean Code Size
BinaryReader 1,665.2 us 639 B
BufferedStreamReader 890.8 us 709 B
BufferedStreamReader Raw 370.7 us 761 B
NativePointer 227.3 us 55 B

FileStream

Benchmarks on FileStream allow us to compare the performance against BinaryReader more closely.

Byte:

Method Mean Code Size
BinaryReader 21,309.7 us 169 B
BufferedStreamReader 6,731.9 us 1,052 B
BufferedStreamReader Raw 2,763.3 us 661 B

Int:

Method Mean Code Size
BinaryReader 22,279.2 us 640 B
BufferedStreamReader 2,219.4 us 1,487 B
BufferedStreamReader Raw 1,310.4 us 753 B

Long:

Method Mean Code Size
BinaryReader 13,025.8 us 642 B
BufferedStreamReader 1,425.6 us 1,068 B
BufferedStreamReader Raw 943.7 us 754 B

Properties

  • BaseStream: The stream this class was instantiated with.
  • BufferBytesAvailable: The remaining number of bytes that are currently buffered.
  • CurrentBufferSize: The total size of the current buffered data at this moment in time.
  • IsEndOfStream: This is true if end of stream was reached while refilling the internal buffer.
  • OnEndOfStream: This method is executed if a buffer refill does not fill the whole buffer, indicating end of stream was reached.

Constructors

  • BufferedStreamReader(TStream stream, int bufferSize = 65536): Constructs a BufferedStreamReader.

Methods

Just like with regular Stream APIs, less data can be returned than requested if end of stream was reached. Please note BufferedStreamReader does not throw in these scenarios.

Seek

public void Seek(long offset, SeekOrigin origin)

Seeks the underlying stream to a specified position.

Advance

public void Advance(long offset)

Advances the underlying stream by a specified number of bytes.
(This is equivalent to Seek(offset, SeekOrigin.Current))

Read

public T Read<T>() where T : unmanaged
public void Read<T>(out T value) where T : unmanaged

Reads an unmanaged, generic type from the stream.

ReadRaw

The returned values point to internal buffers. DO NOT MODIFY THE DATA!

public byte* ReadRaw(int length, out int available)
public T* ReadRaw<T>(int numItems, out int available) where T : unmanaged

Provides a pointer to the buffered data; buffering sufficient data if needed.

ReadRaw (with Output)

Variant of ReadRaw that copies the data to user's own destination.

public int ReadRaw<T>(Span<T> buffer) where T : unmanaged
public int ReadRaw<T>(T* buffer, int numItems) where T : unmanaged

Reads raw data from the stream, without conversion. The output is written to the supplied pointer or span.

ReadBytesUnbuffered

This method is useful for reading large blobs (e.g. a compressed file from an archive) without discarding the buffered data.

public int ReadBytesUnbuffered(long offset, Span<byte> data)

Reads a specified amount of bytes at a specific offset from the underlying stream without resetting the buffers or advancing the read pointer.

ReadMarshalled

public T ReadMarshalled<T>()

Reads a value that requires marshalling from the stream.

Peek

public T Peek<T>() where T : unmanaged
public void Peek<T>(out T value) where T : unmanaged

Reads an unmanaged, generic type from the stream without incrementing the position.

PeekMarshalled

public T PeekMarshalled<T>()
public void PeekMarshalled<T>(out T value)

Reads a value that requires marshalling from the stream without advancing the position.

Methods (Endian Extensions)

PeekLittleEndian

public Int16 PeekLittleEndianInt16()
public Int16 PeekLittleEndian(out Int16 value)
public UInt16 PeekLittleEndianUInt16()
public UInt16 PeekLittleEndian(out UInt16 value)
public Int32 PeekLittleEndianInt32()
public Int32 PeekLittleEndian(out Int32 value)
public UInt32 PeekLittleEndianUInt32()
public UInt32 PeekLittleEndian(out UInt32 value)
public Int64 PeekLittleEndianInt64()
public Int64 PeekLittleEndian(out Int64 value)
public UInt64 PeekLittleEndianUInt64()
public UInt64 PeekLittleEndian(out UInt64 value)
public Single PeekLittleEndianSingle()
public Single PeekLittleEndian(out Single value)
public Double PeekLittleEndianDouble()
public Double PeekLittleEndian(out Double value)

Peeks a little endian value of the specified type from the stream without incrementing the position.

PeekBigEndian

public Int16 PeekBigEndianInt16()
public Int16 PeekBigEndian(out Int16 value)
public UInt16 PeekBigEndianUInt16()
public UInt16 PeekBigEndian(out UInt16 value)
public Int32 PeekBigEndianInt32()
public Int32 PeekBigEndian(out Int32 value)
public UInt32 PeekBigEndianUInt32()
public UInt32 PeekBigEndian(out UInt32 value)
public Int64 PeekBigEndianInt64()
public Int64 PeekBigEndian(out Int64 value)
public UInt64 PeekBigEndianUInt64()
public UInt64 PeekBigEndian(out UInt64 value)
public Single PeekBigEndianSingle()
public Single PeekBigEndian(out Single value)
public Double PeekBigEndianDouble()
public Double PeekBigEndian(out Double value)

Peeks a big endian value of the specified type from the stream without incrementing the position.

ReadLittleEndian

public Int16 ReadLittleEndianInt16()
public Int16 ReadLittleEndian(out Int16 value)
public UInt16 ReadLittleEndianUInt16()
public UInt16 ReadLittleEndian(out UInt16 value)
public Int32 ReadLittleEndianInt32()
public Int32 ReadLittleEndian(out Int32 value)
public UInt32 ReadLittleEndianUInt32()
public UInt32 ReadLittleEndian(out UInt32 value)
public Int64 ReadLittleEndianInt64()
public Int64 ReadLittleEndian(out Int64 value)
public UInt64 ReadLittleEndianUInt64()
public UInt64 ReadLittleEndian(out UInt64 value)
public Single ReadLittleEndianSingle()
public Single ReadLittleEndian(out Single value)
public Double ReadLittleEndianDouble()
public Double ReadLittleEndian(out Double value)

Reads a little endian value of the specified type from the stream and advances the position by the size of the type.

ReadBigEndian

public Int16 ReadBigEndianInt16()
public Int16 ReadBigEndian(out Int16 value)
public UInt16 ReadBigEndianUInt16()
public UInt16 ReadBigEndian(out UInt16 value)
public Int32 ReadBigEndianInt32()
public Int32 ReadBigEndian(out Int32 value)
public UInt32 ReadBigEndianUInt32()
public UInt32 ReadBigEndian(out UInt32 value)
public Int64 ReadBigEndianInt64()
public Int64 ReadBigEndian(out Int64 value)
public UInt64 ReadBigEndianUInt64()
public UInt64 ReadBigEndian(out UInt64 value)
public Single ReadBigEndianSingle()
public Single ReadBigEndian(out Single value)
public Double ReadBigEndianDouble()
public Double ReadBigEndian(out Double value)

Reads a big endian value of the specified type from the stream and advances the position by the size of the type.

AsLittleEndian

Implements, IEndianedBufferStreamReader<TStream>. Use constraint where T : IEndianedBufferStreamReader<TStream> to write endian agnostic code without any overhead.

public LittleEndianBufferedStreamReader<TStream> AsLittleEndian();

Returns a reader that can be used to read little endian values from the stream.

AsBigEndian

Implements, IEndianedBufferStreamReader<TStream>. Use constraint where T : IEndianedBufferStreamReader<TStream> to write endian agnostic code without any overhead.

public BigEndianBufferedStreamReader<TStream> AsBigEndian();

Returns a reader that can be used to read big endian values from the stream.

Methods (Endian Struct Extensions)

The following methods are valid for structs which implement ICanReverseEndian

PeekLittleEndianStruct

public T PeekLittleEndianStruct<T>() where T : unmanaged, ICanReverseEndian
public void PeekLittleEndianStruct<T>(out T value) where T : unmanaged, ICanReverseEndian

Peeks a little endian unmanaged, generic type from the stream without incrementing the position.

PeekBigEndianStruct

public T PeekBigEndianStruct<T>() where T : unmanaged, ICanReverseEndian
public void PeekBigEndianStruct<T>(out T value) where T : unmanaged, ICanReverseEndian

Peeks a big endian unmanaged, generic type from the stream without incrementing the position.

ReadLittleEndianStruct

public T ReadLittleEndianStruct<T>() where T : unmanaged, ICanReverseEndian
public void ReadLittleEndianStruct<T>(out T value) where T : unmanaged, ICanReverseEndian

Reads a little endian unmanaged, generic type from the stream.

ReadBigEndianStruct

public T ReadBigEndianStruct<T>() where T : unmanaged, ICanReverseEndian
public void ReadBigEndianStruct<T>(out T value) where T : unmanaged, ICanReverseEndian

Reads a big endian unmanaged, generic type from the stream.

Examples

Creating a BufferedStreamReader

var stream = File.OpenRead("myFile.txt");
using var reader = new BufferedStreamReader<FileStream>(stream);

Reading an Integer from the Stream

int value = reader.Read<int>();
Console.WriteLine($"Read value: {value}");

Reading Raw Bytes

Do not modify the returned data! Copy it elsewhere first!

int length = 10;
int available;
byte* rawBytes = reader.ReadRaw(length, out available);

Reading Raw Structs

using var reader = new BufferedStreamReader<FileStream>(fileStream);
Span<Vector3> span = stackalloc Vector3[10];
int readItems = reader.ReadRaw(span);

Console.WriteLine($"{readItems} Vector3 items were read from the stream.");

In this example, we create a BufferedStreamReader using a FileStream and then declare a Span<Vector3> where Vector3 is a struct representing a 3D vector. We then read from the stream directly into the Span<Vector3>. After reading, we print out how many Vector3 items were read from the stream.

Seeking and Advancing Stream

var reader = new BufferedStreamReader<FileStream>(fileStream);
reader.Seek(100, SeekOrigin.Begin); // Seek to 100 bytes from the start
reader.Advance(50); // Advance 50 bytes from the current position

Console.WriteLine($"Current stream position is {reader.Position()}");

In this example, we first seek to 100 bytes from the start of the stream. Then we advance 50 bytes from the current position. After that, we print out the current stream position.

Reading Unmanaged Types

using var reader = new BufferedStreamReader<FileStream>(fileStream);
int value = reader.Read<int>(); // Reads an integer from the stream

Console.WriteLine($"The read integer value is {value}");

In this example, we read an integer directly from the stream and print it out.

Reading Large Raw Data

using var reader = new BufferedStreamReader<FileStream>(fileStream);
Span<byte> dataSpan = stackalloc byte[1024];
int bytesRead = reader.ReadBytesUnbuffered(200, dataSpan);

Console.WriteLine($"{bytesRead} bytes were read from the stream.");

In this example, we read 1024 bytes starting from 200 bytes offset into a Span<byte> without resetting the buffers or advancing the read pointer. After reading, we print out how many bytes were read from the stream.

Value of bytesRead may be less than length of dataSpan if end of stream was reached.

Peeking Data

using var reader = new BufferedStreamReader<FileStream>(fileStream);
int value = reader.Peek<int>(); // Peeks an integer from the stream

Console.WriteLine($"The peeked integer value is {value}. Stream did not advance.");

Using Customized Buffer Size

The default buffer size of 64KBytes should be sufficient for most use cases, including file reads.

int bufferSize = 8192; // 8 KB
using var reader = new BufferedStreamReader<FileStream>(fileStream, bufferSize);

Console.WriteLine($"Custom buffer size of {bufferSize} bytes is used for the reader.");

In this example, a custom buffer size is set when creating a BufferedStreamReader. This allows you to tune the buffer size to match your specific use case, potentially improving performance.

Combining Multiple Operations

var reader = new BufferedStreamReader<FileStream>(fileStream);
reader.Seek(100, SeekOrigin.Begin); 
int value = reader.Read<int>(); 
reader.Advance(50); 
Vector3 vector = reader.Read<Vector3>();

Console.WriteLine($"Read integer value: {value}, Vector3: {vector.X}, {vector.Y}, {vector.Z}.");

In this final example, multiple operations are combined. First, the reader seeks to a specific position in the stream. Then, an integer is read, the reader advances a certain number of bytes, and finally, a Vector3 struct is read. The results are then printed to the console.

Examples (Extensions)

Writing Endian Agnostic Code

Shows you how to write code that works with both Big and Little Endian data.

using var reader = new BufferedStreamReader<FileStream>(fileStream);

// Read in Big Endian
Read(reader.AsBigEndian());
// or... as Little Endian
Read(reader.AsLittleEndian());

private void Read<TReader>(TReader reader) where TReader : IEndianedBufferStreamReader
{
    // Some neat parsing code here.
    var i16 = reader.ReadInt16();
    var i32 = reader.PeekInt32();
}

This approach allows you to write code which uses the same logic for both big and little endian.
You can either pass reader.AsLittleEndian() or reader.AsBigEndian() to your parsing code. Either way, your reader will be devirtualized and you'll be able to parse away with 0 overhead.

Reading Big/Little Endian Primitives

using var reader = new BufferedStreamReader<FileStream>(fileStream);

// Little Endian
Int16 littleEndianInt16 = reader.PeekLittleEndianInt16();
Console.WriteLine($"The read Int16 value at offset 0 in Little Endian is {littleEndianInt16}");

// Big Endian
Int16 bigEndianInt16 = reader.PeekBigEndianInt16();
Console.WriteLine($"The read Int16 value at offset 0 in Big Endian is {bigEndianInt16}");

Examples (Endian Struct Extensions)

CustomStruct must implement ICanReverseEndian

Example:

public struct CustomStruct : ICanReverseEndian
{
    public int Value1;
    public int Value2;
    public int Value3;

    // ICanReverseEndian
    public void ReverseEndian()
    {
        Value1 = Endian.Reverse(Value1);
        Value2 = Endian.Reverse(Value2);
        Value3 = Endian.Reverse(Value3);
    }
}

Reading a Little Endian Struct

using var reader = new BufferedStreamReader<FileStream>(fileStream);
var value = reader.ReadLittleEndianStruct<CustomStruct>();

Console.WriteLine($"The read CustomStruct value is {value}");

In this example, a CustomStruct is read directly from the stream using little endian byte order.

Reading a Big Endian Struct

using var reader = new BufferedStreamReader<FileStream>(fileStream);
var value = reader.ReadBigEndianStruct<CustomStruct>();

Console.WriteLine($"The read CustomStruct value is {value}");

In this example, a CustomStruct is read directly from the stream using big endian byte order.

Peeking a Little Endian Struct

using var reader = new BufferedStreamReader<FileStream>(fileStream);
var value = reader.PeekLittleEndianStruct<CustomStruct>();

Console.WriteLine($"The peeked CustomStruct value is {value}. Stream did not advance.");

In this example, a CustomStruct is peeked from the stream without advancing the stream position, using little endian byte order.

Peeking a Big Endian Struct

using var reader = new BufferedStreamReader<FileStream>(fileStream);
var value = reader.PeekBigEndianStruct<CustomStruct>();

Console.WriteLine($"The peeked CustomStruct value is {value}. Stream did not advance.");

In this example, a CustomStruct is peeked from the stream without advancing the stream position, using big endian byte order.

CustomStruct must be an unmanaged type and implement ICanReverseEndian interface.