mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
34 changed files with 1515 additions and 909 deletions
@ -0,0 +1,413 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A readonly stream that add a secondary level buffer in addition to native stream
|
|||
/// buffered reading to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal sealed class BufferedReadStream : Stream |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the underlying buffer.
|
|||
/// </summary>
|
|||
public const int BufferLength = 8192; |
|||
|
|||
private const int MaxBufferIndex = BufferLength - 1; |
|||
|
|||
private readonly byte[] readBuffer; |
|||
|
|||
private MemoryHandle readBufferHandle; |
|||
|
|||
private readonly unsafe byte* pinnedReadBuffer; |
|||
|
|||
// Index within our buffer, not reader position.
|
|||
private int readBufferIndex; |
|||
|
|||
// Matches what the stream position would be without buffering
|
|||
private long readerPosition; |
|||
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BufferedReadStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public BufferedReadStream(Stream stream) |
|||
{ |
|||
Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); |
|||
Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); |
|||
|
|||
// Ensure all underlying buffers have been flushed before we attempt to read the stream.
|
|||
// User streams may have opted to throw from Flush if CanWrite is false
|
|||
// (although the abstract Stream does not do so).
|
|||
if (stream.CanWrite) |
|||
{ |
|||
stream.Flush(); |
|||
} |
|||
|
|||
this.BaseStream = stream; |
|||
this.Position = (int)stream.Position; |
|||
this.Length = stream.Length; |
|||
|
|||
this.readBuffer = ArrayPool<byte>.Shared.Rent(BufferLength); |
|||
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin(); |
|||
unsafe |
|||
{ |
|||
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; |
|||
} |
|||
|
|||
// This triggers a full read on first attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get => this.readerPosition; |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
set |
|||
{ |
|||
// Only reset readBufferIndex if we are out of bounds of our working buffer
|
|||
// otherwise we should simply move the value by the diff.
|
|||
if (this.IsInReadBuffer(value, out long index)) |
|||
{ |
|||
this.readBufferIndex = (int)index; |
|||
this.readerPosition = value; |
|||
} |
|||
else |
|||
{ |
|||
// Base stream seek will throw for us if invalid.
|
|||
this.BaseStream.Seek(value, SeekOrigin.Begin); |
|||
this.readerPosition = value; |
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead { get; } = true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek { get; } = true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite { get; } = false; |
|||
|
|||
/// <summary>
|
|||
/// Gets the underlying stream.
|
|||
/// </summary>
|
|||
public Stream BaseStream |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int ReadByte() |
|||
{ |
|||
if (this.readerPosition >= this.Length) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
// Our buffer has been read.
|
|||
// We need to refill and start again.
|
|||
if (this.readBufferIndex > MaxBufferIndex) |
|||
{ |
|||
this.FillReadBuffer(); |
|||
} |
|||
|
|||
this.readerPosition++; |
|||
unsafe |
|||
{ |
|||
return this.pinnedReadBuffer[this.readBufferIndex++]; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Too big for our buffer. Read directly from the stream.
|
|||
if (count > BufferLength) |
|||
{ |
|||
return this.ReadToBufferDirectSlow(buffer, offset, count); |
|||
} |
|||
|
|||
// Too big for remaining buffer but less than entire buffer length
|
|||
// Copy to buffer then read from there.
|
|||
if (count + this.readBufferIndex > BufferLength) |
|||
{ |
|||
return this.ReadToBufferViaCopySlow(buffer, offset, count); |
|||
} |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
#if SUPPORTS_SPAN_STREAM
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(Span<byte> buffer) |
|||
{ |
|||
// Too big for our buffer. Read directly from the stream.
|
|||
int count = buffer.Length; |
|||
if (count > BufferLength) |
|||
{ |
|||
return this.ReadToBufferDirectSlow(buffer); |
|||
} |
|||
|
|||
// Too big for remaining buffer but less than entire buffer length
|
|||
// Copy to buffer then read from there.
|
|||
if (count + this.readBufferIndex > BufferLength) |
|||
{ |
|||
return this.ReadToBufferViaCopySlow(buffer); |
|||
} |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer); |
|||
} |
|||
#endif
|
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
// Reset the stream position to match reader position.
|
|||
Stream baseStream = this.BaseStream; |
|||
if (this.readerPosition != baseStream.Position) |
|||
{ |
|||
baseStream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
this.readerPosition = (int)baseStream.Position; |
|||
} |
|||
|
|||
// Reset to trigger full read on next attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
switch (origin) |
|||
{ |
|||
case SeekOrigin.Begin: |
|||
this.Position = offset; |
|||
break; |
|||
|
|||
case SeekOrigin.Current: |
|||
this.Position += offset; |
|||
break; |
|||
|
|||
case SeekOrigin.End: |
|||
this.Position = this.Length - offset; |
|||
break; |
|||
} |
|||
|
|||
return this.readerPosition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <exception cref="NotSupportedException">
|
|||
/// This operation is not supported in <see cref="BufferedReadStream"/>.
|
|||
/// </exception>
|
|||
public override void SetLength(long value) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <exception cref="NotSupportedException">
|
|||
/// This operation is not supported in <see cref="BufferedReadStream"/>.
|
|||
/// </exception>
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.isDisposed = true; |
|||
this.readBufferHandle.Dispose(); |
|||
ArrayPool<byte>.Shared.Return(this.readBuffer); |
|||
this.Flush(); |
|||
|
|||
base.Dispose(true); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private bool IsInReadBuffer(long newPosition, out long index) |
|||
{ |
|||
index = newPosition - this.readerPosition + this.readBufferIndex; |
|||
return index > -1 && index < BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private void FillReadBuffer() |
|||
{ |
|||
Stream baseStream = this.BaseStream; |
|||
if (this.readerPosition != baseStream.Position) |
|||
{ |
|||
baseStream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
// Read doesn't always guarantee the full returned length so read a byte
|
|||
// at a time until we get either our count or hit the end of the stream.
|
|||
int n = 0; |
|||
int i; |
|||
do |
|||
{ |
|||
i = baseStream.Read(this.readBuffer, n, BufferLength - n); |
|||
n += i; |
|||
} |
|||
while (n < BufferLength && i > 0); |
|||
|
|||
this.readBufferIndex = 0; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopyFast(Span<byte> buffer) |
|||
{ |
|||
int n = this.GetCopyCount(buffer.Length); |
|||
|
|||
// Just straight copy. MemoryStream does the same so should be fast enough.
|
|||
this.readBuffer.AsSpan(this.readBufferIndex, n).CopyTo(buffer); |
|||
|
|||
this.readerPosition += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) |
|||
{ |
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.readerPosition += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopySlow(Span<byte> buffer) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillReadBuffer(); |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillReadBuffer(); |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferDirectSlow(Span<byte> buffer) |
|||
{ |
|||
// Read to target but don't copy to our read buffer.
|
|||
Stream baseStream = this.BaseStream; |
|||
if (this.readerPosition != baseStream.Position) |
|||
{ |
|||
baseStream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
// Read doesn't always guarantee the full returned length so read a byte
|
|||
// at a time until we get either our count or hit the end of the stream.
|
|||
int count = buffer.Length; |
|||
int n = 0; |
|||
int i; |
|||
do |
|||
{ |
|||
i = baseStream.Read(buffer.Slice(n, count - n)); |
|||
n += i; |
|||
} |
|||
while (n < count && i > 0); |
|||
|
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Read to target but don't copy to our read buffer.
|
|||
Stream baseStream = this.BaseStream; |
|||
if (this.readerPosition != baseStream.Position) |
|||
{ |
|||
baseStream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
// Read doesn't always guarantee the full returned length so read a byte
|
|||
// at a time until we get either our count or hit the end of the stream.
|
|||
int n = 0; |
|||
int i; |
|||
do |
|||
{ |
|||
i = baseStream.Read(buffer, n + offset, count - n); |
|||
n += i; |
|||
} |
|||
while (n < count && i > 0); |
|||
|
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetCopyCount(int count) |
|||
{ |
|||
long n = this.Length - this.readerPosition; |
|||
if (n > count) |
|||
{ |
|||
return count; |
|||
} |
|||
|
|||
if (n < 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
return (int)n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private unsafe void CopyBytes(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Same as MemoryStream.
|
|||
if (count < 9) |
|||
{ |
|||
int byteCount = count; |
|||
int read = this.readBufferIndex; |
|||
byte* pinned = this.pinnedReadBuffer; |
|||
|
|||
while (--byteCount > -1) |
|||
{ |
|||
buffer[offset + byteCount] = pinned[read + byteCount]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,255 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A stream reader that add a secondary level buffer in addition to native stream buffered reading
|
|||
/// to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DoubleBufferedStreamReader : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the buffering chunk.
|
|||
/// </summary>
|
|||
public const int ChunkLength = 8192; |
|||
|
|||
private const int MaxChunkIndex = ChunkLength - 1; |
|||
|
|||
private readonly Stream stream; |
|||
|
|||
private readonly IManagedByteBuffer managedBuffer; |
|||
|
|||
private MemoryHandle handle; |
|||
|
|||
private readonly byte* pinnedChunk; |
|||
|
|||
private readonly byte[] bufferChunk; |
|||
|
|||
private readonly int length; |
|||
|
|||
private int chunkIndex; |
|||
|
|||
private int position; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DoubleBufferedStreamReader"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public DoubleBufferedStreamReader(MemoryAllocator memoryAllocator, Stream stream) |
|||
{ |
|||
this.stream = stream; |
|||
this.Position = (int)stream.Position; |
|||
this.length = (int)stream.Length; |
|||
this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(ChunkLength); |
|||
this.bufferChunk = this.managedBuffer.Array; |
|||
this.handle = this.managedBuffer.Memory.Pin(); |
|||
this.pinnedChunk = (byte*)this.handle.Pointer; |
|||
this.chunkIndex = ChunkLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length, in bytes, of the stream.
|
|||
/// </summary>
|
|||
public long Length => this.length; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current position within the stream.
|
|||
/// </summary>
|
|||
public long Position |
|||
{ |
|||
get => this.position; |
|||
|
|||
set |
|||
{ |
|||
// Only reset chunkIndex if we are out of bounds of our working chunk
|
|||
// otherwise we should simply move the value by the diff.
|
|||
int v = (int)value; |
|||
if (this.IsInChunk(v, out int index)) |
|||
{ |
|||
this.chunkIndex = index; |
|||
this.position = v; |
|||
} |
|||
else |
|||
{ |
|||
this.position = v; |
|||
this.stream.Seek(value, SeekOrigin.Begin); |
|||
this.chunkIndex = ChunkLength; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a byte from the stream and advances the position within the stream by one
|
|||
/// byte, or returns -1 if at the end of the stream.
|
|||
/// </summary>
|
|||
/// <returns>The unsigned byte cast to an <see cref="int"/>, or -1 if at the end of the stream.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int ReadByte() |
|||
{ |
|||
if (this.position >= this.length) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
if (this.chunkIndex > MaxChunkIndex) |
|||
{ |
|||
this.FillChunk(); |
|||
} |
|||
|
|||
this.position++; |
|||
return this.pinnedChunk[this.chunkIndex++]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Skips the number of bytes in the stream
|
|||
/// </summary>
|
|||
/// <param name="count">The number of bytes to skip.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Skip(int count) => this.Position += count; |
|||
|
|||
/// <summary>
|
|||
/// Reads a sequence of bytes from the current stream and advances the position within the stream
|
|||
/// by the number of bytes read.
|
|||
/// </summary>
|
|||
/// <param name="buffer">
|
|||
/// An array of bytes. When this method returns, the buffer contains the specified
|
|||
/// byte array with the values between offset and (offset + count - 1) replaced by
|
|||
/// the bytes read from the current source.
|
|||
/// </param>
|
|||
/// <param name="offset">
|
|||
/// The zero-based byte offset in buffer at which to begin storing the data read
|
|||
/// from the current stream.
|
|||
/// </param>
|
|||
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
|
|||
/// <returns>
|
|||
/// The total number of bytes read into the buffer. This can be less than the number
|
|||
/// of bytes requested if that many bytes are not currently available, or zero (0)
|
|||
/// if the end of the stream has been reached.
|
|||
/// </returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
if (count > ChunkLength) |
|||
{ |
|||
return this.ReadToBufferSlow(buffer, offset, count); |
|||
} |
|||
|
|||
if (count + this.chunkIndex > ChunkLength) |
|||
{ |
|||
return this.ReadToChunkSlow(buffer, offset, count); |
|||
} |
|||
|
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.position += n; |
|||
this.chunkIndex += n; |
|||
return n; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.handle.Dispose(); |
|||
this.managedBuffer?.Dispose(); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int GetPositionDifference(int p) => p - this.position; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private bool IsInChunk(int p, out int index) |
|||
{ |
|||
index = this.GetPositionDifference(p) + this.chunkIndex; |
|||
return index > -1 && index < ChunkLength; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
private void FillChunk() |
|||
{ |
|||
if (this.position != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
} |
|||
|
|||
this.stream.Read(this.bufferChunk, 0, ChunkLength); |
|||
this.chunkIndex = 0; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
private int ReadToChunkSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillChunk(); |
|||
|
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.position += n; |
|||
this.chunkIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
private int ReadToBufferSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Read to target but don't copy to our chunk.
|
|||
if (this.position != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
} |
|||
|
|||
int n = this.stream.Read(buffer, offset, count); |
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int GetCopyCount(int count) |
|||
{ |
|||
int n = this.length - this.position; |
|||
if (n > count) |
|||
{ |
|||
n = count; |
|||
} |
|||
|
|||
if (n < 0) |
|||
{ |
|||
n = 0; |
|||
} |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void CopyBytes(byte[] buffer, int offset, int count) |
|||
{ |
|||
if (count < 9) |
|||
{ |
|||
int byteCount = count; |
|||
int read = this.chunkIndex; |
|||
byte* pinned = this.pinnedChunk; |
|||
|
|||
while (--byteCount > -1) |
|||
{ |
|||
buffer[offset + byteCount] = pinned[read + byteCount]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Buffer.BlockCopy(this.bufferChunk, this.chunkIndex, buffer, offset, count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,153 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class DoubleBufferedStreams |
|||
{ |
|||
private readonly byte[] buffer = CreateTestBytes(); |
|||
private readonly byte[] chunk1 = new byte[2]; |
|||
private readonly byte[] chunk2 = new byte[2]; |
|||
|
|||
private MemoryStream stream1; |
|||
private MemoryStream stream2; |
|||
private MemoryStream stream3; |
|||
private MemoryStream stream4; |
|||
private DoubleBufferedStreamReader reader1; |
|||
private DoubleBufferedStreamReader reader2; |
|||
|
|||
[GlobalSetup] |
|||
public void CreateStreams() |
|||
{ |
|||
this.stream1 = new MemoryStream(this.buffer); |
|||
this.stream2 = new MemoryStream(this.buffer); |
|||
this.stream3 = new MemoryStream(this.buffer); |
|||
this.stream4 = new MemoryStream(this.buffer); |
|||
this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); |
|||
this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); |
|||
} |
|||
|
|||
[GlobalCleanup] |
|||
public void DestroyStreams() |
|||
{ |
|||
this.stream1?.Dispose(); |
|||
this.stream2?.Dispose(); |
|||
this.stream3?.Dispose(); |
|||
this.stream4?.Dispose(); |
|||
this.reader1?.Dispose(); |
|||
this.reader2?.Dispose(); |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public int StandardStreamReadByte() |
|||
{ |
|||
int r = 0; |
|||
Stream stream = this.stream1; |
|||
|
|||
for (int i = 0; i < stream.Length; i++) |
|||
{ |
|||
r += stream.ReadByte(); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int StandardStreamRead() |
|||
{ |
|||
int r = 0; |
|||
Stream stream = this.stream1; |
|||
byte[] b = this.chunk1; |
|||
|
|||
for (int i = 0; i < stream.Length / 2; i++) |
|||
{ |
|||
r += stream.Read(b, 0, 2); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int DoubleBufferedStreamReadByte() |
|||
{ |
|||
int r = 0; |
|||
DoubleBufferedStreamReader reader = this.reader1; |
|||
|
|||
for (int i = 0; i < reader.Length; i++) |
|||
{ |
|||
r += reader.ReadByte(); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int DoubleBufferedStreamRead() |
|||
{ |
|||
int r = 0; |
|||
DoubleBufferedStreamReader reader = this.reader2; |
|||
byte[] b = this.chunk2; |
|||
|
|||
for (int i = 0; i < reader.Length / 2; i++) |
|||
{ |
|||
r += reader.Read(b, 0, 2); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int SimpleReadByte() |
|||
{ |
|||
byte[] b = this.buffer; |
|||
int r = 0; |
|||
for (int i = 0; i < b.Length; i++) |
|||
{ |
|||
r += b[i]; |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
private static byte[] CreateTestBytes() |
|||
{ |
|||
var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return buffer; |
|||
} |
|||
} |
|||
|
|||
/* RESULTS (2019 April 24): |
|||
|
|||
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17763.437 (1809/October2018Update/Redstone5) |
|||
Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 2 physical cores |
|||
.NET Core SDK=2.2.202 |
|||
[Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT |
|||
Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0 |
|||
Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT |
|||
|
|||
IterationCount=3 LaunchCount=1 WarmupCount=3 |
|||
|
|||
| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |
|||
|----------------------------- |----- |-------- |---------:|-----------:|----------:|------:|--------:|------:|------:|------:|----------:| |
|||
| StandardStreamReadByte | Clr | Clr | 96.71 us | 5.9950 us | 0.3286 us | 1.00 | 0.00 | - | - | - | - | |
|||
| StandardStreamRead | Clr | Clr | 77.73 us | 5.2284 us | 0.2866 us | 0.80 | 0.00 | - | - | - | - | |
|||
| DoubleBufferedStreamReadByte | Clr | Clr | 23.17 us | 26.2354 us | 1.4381 us | 0.24 | 0.01 | - | - | - | - | |
|||
| DoubleBufferedStreamRead | Clr | Clr | 33.35 us | 3.4071 us | 0.1868 us | 0.34 | 0.00 | - | - | - | - | |
|||
| SimpleReadByte | Clr | Clr | 10.85 us | 0.4927 us | 0.0270 us | 0.11 | 0.00 | - | - | - | - | |
|||
| | | | | | | | | | | | | |
|||
| StandardStreamReadByte | Core | Core | 75.35 us | 12.9789 us | 0.7114 us | 1.00 | 0.00 | - | - | - | - | |
|||
| StandardStreamRead | Core | Core | 55.36 us | 1.4432 us | 0.0791 us | 0.73 | 0.01 | - | - | - | - | |
|||
| DoubleBufferedStreamReadByte | Core | Core | 21.47 us | 29.7076 us | 1.6284 us | 0.28 | 0.02 | - | - | - | - | |
|||
| DoubleBufferedStreamRead | Core | Core | 29.67 us | 2.5988 us | 0.1424 us | 0.39 | 0.00 | - | - | - | - | |
|||
| SimpleReadByte | Core | Core | 10.84 us | 0.7567 us | 0.0415 us | 0.14 | 0.00 | - | - | - | - | |
|||
*/ |
|||
} |
|||
@ -0,0 +1,279 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A readonly stream wrapper that add a secondary level buffer in addition to native stream
|
|||
/// buffered reading to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal sealed unsafe class BufferedReadStreamWrapper : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the underlying buffer.
|
|||
/// </summary>
|
|||
public const int BufferLength = 8192; |
|||
|
|||
private const int MaxBufferIndex = BufferLength - 1; |
|||
|
|||
private readonly Stream stream; |
|||
|
|||
private readonly byte[] readBuffer; |
|||
|
|||
private MemoryHandle readBufferHandle; |
|||
|
|||
private readonly byte* pinnedReadBuffer; |
|||
|
|||
// Index within our buffer, not reader position.
|
|||
private int readBufferIndex; |
|||
|
|||
// Matches what the stream position would be without buffering
|
|||
private long readerPosition; |
|||
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BufferedReadStreamWrapper"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public BufferedReadStreamWrapper(Stream stream) |
|||
{ |
|||
Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); |
|||
Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); |
|||
|
|||
// Ensure all underlying buffers have been flushed before we attempt to read the stream.
|
|||
// User streams may have opted to throw from Flush if CanWrite is false
|
|||
// (although the abstract Stream does not do so).
|
|||
if (stream.CanWrite) |
|||
{ |
|||
stream.Flush(); |
|||
} |
|||
|
|||
this.stream = stream; |
|||
this.Position = (int)stream.Position; |
|||
this.Length = stream.Length; |
|||
|
|||
this.readBuffer = ArrayPool<byte>.Shared.Rent(BufferLength); |
|||
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin(); |
|||
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; |
|||
|
|||
// This triggers a full read on first attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length, in bytes, of the stream.
|
|||
/// </summary>
|
|||
public long Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current position within the stream.
|
|||
/// </summary>
|
|||
public long Position |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get => this.readerPosition; |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
set |
|||
{ |
|||
// Only reset readBufferIndex if we are out of bounds of our working buffer
|
|||
// otherwise we should simply move the value by the diff.
|
|||
if (this.IsInReadBuffer(value, out long index)) |
|||
{ |
|||
this.readBufferIndex = (int)index; |
|||
this.readerPosition = value; |
|||
} |
|||
else |
|||
{ |
|||
// Base stream seek will throw for us if invalid.
|
|||
this.stream.Seek(value, SeekOrigin.Begin); |
|||
this.readerPosition = value; |
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public int ReadByte() |
|||
{ |
|||
if (this.readerPosition >= this.Length) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
// Our buffer has been read.
|
|||
// We need to refill and start again.
|
|||
if (this.readBufferIndex > MaxBufferIndex) |
|||
{ |
|||
this.FillReadBuffer(); |
|||
} |
|||
|
|||
this.readerPosition++; |
|||
return this.pinnedReadBuffer[this.readBufferIndex++]; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Too big for our buffer. Read directly from the stream.
|
|||
if (count > BufferLength) |
|||
{ |
|||
return this.ReadToBufferDirectSlow(buffer, offset, count); |
|||
} |
|||
|
|||
// Too big for remaining buffer but less than entire buffer length
|
|||
// Copy to buffer then read from there.
|
|||
if (count + this.readBufferIndex > BufferLength) |
|||
{ |
|||
return this.ReadToBufferViaCopySlow(buffer, offset, count); |
|||
} |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
public void Flush() |
|||
{ |
|||
// Reset the stream position to match reader position.
|
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
this.readerPosition = (int)this.stream.Position; |
|||
} |
|||
|
|||
// Reset to trigger full read on next attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
switch (origin) |
|||
{ |
|||
case SeekOrigin.Begin: |
|||
this.Position = offset; |
|||
break; |
|||
|
|||
case SeekOrigin.Current: |
|||
this.Position += offset; |
|||
break; |
|||
|
|||
case SeekOrigin.End: |
|||
this.Position = this.Length - offset; |
|||
break; |
|||
} |
|||
|
|||
return this.readerPosition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.isDisposed = true; |
|||
this.readBufferHandle.Dispose(); |
|||
ArrayPool<byte>.Shared.Return(this.readBuffer); |
|||
this.Flush(); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private bool IsInReadBuffer(long newPosition, out long index) |
|||
{ |
|||
index = newPosition - this.readerPosition + this.readBufferIndex; |
|||
return index > -1 && index < BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private void FillReadBuffer() |
|||
{ |
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
this.stream.Read(this.readBuffer, 0, BufferLength); |
|||
this.readBufferIndex = 0; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) |
|||
{ |
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.readerPosition += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillReadBuffer(); |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Read to target but don't copy to our read buffer.
|
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
int n = this.stream.Read(buffer, offset, count); |
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetCopyCount(int count) |
|||
{ |
|||
long n = this.Length - this.readerPosition; |
|||
if (n > count) |
|||
{ |
|||
return count; |
|||
} |
|||
|
|||
if (n < 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
return (int)n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private void CopyBytes(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Same as MemoryStream.
|
|||
if (count < 9) |
|||
{ |
|||
int byteCount = count; |
|||
int read = this.readBufferIndex; |
|||
byte* pinned = this.pinnedReadBuffer; |
|||
|
|||
while (--byteCount > -1) |
|||
{ |
|||
buffer[offset + byteCount] = pinned[read + byteCount]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,206 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.IO |
|||
{ |
|||
[Config(typeof(Config.ShortClr))] |
|||
public class BufferedStreams |
|||
{ |
|||
private readonly byte[] buffer = CreateTestBytes(); |
|||
private readonly byte[] chunk1 = new byte[2]; |
|||
private readonly byte[] chunk2 = new byte[2]; |
|||
|
|||
private MemoryStream stream1; |
|||
private MemoryStream stream2; |
|||
private MemoryStream stream3; |
|||
private MemoryStream stream4; |
|||
private MemoryStream stream5; |
|||
private MemoryStream stream6; |
|||
private BufferedReadStream bufferedStream1; |
|||
private BufferedReadStream bufferedStream2; |
|||
private BufferedReadStreamWrapper bufferedStreamWrap1; |
|||
private BufferedReadStreamWrapper bufferedStreamWrap2; |
|||
|
|||
[GlobalSetup] |
|||
public void CreateStreams() |
|||
{ |
|||
this.stream1 = new MemoryStream(this.buffer); |
|||
this.stream2 = new MemoryStream(this.buffer); |
|||
this.stream3 = new MemoryStream(this.buffer); |
|||
this.stream4 = new MemoryStream(this.buffer); |
|||
this.stream5 = new MemoryStream(this.buffer); |
|||
this.stream6 = new MemoryStream(this.buffer); |
|||
this.bufferedStream1 = new BufferedReadStream(this.stream3); |
|||
this.bufferedStream2 = new BufferedReadStream(this.stream4); |
|||
this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5); |
|||
this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6); |
|||
} |
|||
|
|||
[GlobalCleanup] |
|||
public void DestroyStreams() |
|||
{ |
|||
this.bufferedStream1?.Dispose(); |
|||
this.bufferedStream2?.Dispose(); |
|||
this.bufferedStreamWrap1?.Dispose(); |
|||
this.bufferedStreamWrap2?.Dispose(); |
|||
this.stream1?.Dispose(); |
|||
this.stream2?.Dispose(); |
|||
this.stream3?.Dispose(); |
|||
this.stream4?.Dispose(); |
|||
this.stream5?.Dispose(); |
|||
this.stream6?.Dispose(); |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int StandardStreamRead() |
|||
{ |
|||
int r = 0; |
|||
Stream stream = this.stream1; |
|||
byte[] b = this.chunk1; |
|||
|
|||
for (int i = 0; i < stream.Length / 2; i++) |
|||
{ |
|||
r += stream.Read(b, 0, 2); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int BufferedReadStreamRead() |
|||
{ |
|||
int r = 0; |
|||
BufferedReadStream reader = this.bufferedStream1; |
|||
byte[] b = this.chunk2; |
|||
|
|||
for (int i = 0; i < reader.Length / 2; i++) |
|||
{ |
|||
r += reader.Read(b, 0, 2); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int BufferedReadStreamWrapRead() |
|||
{ |
|||
int r = 0; |
|||
BufferedReadStreamWrapper reader = this.bufferedStreamWrap1; |
|||
byte[] b = this.chunk2; |
|||
|
|||
for (int i = 0; i < reader.Length / 2; i++) |
|||
{ |
|||
r += reader.Read(b, 0, 2); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public int StandardStreamReadByte() |
|||
{ |
|||
int r = 0; |
|||
Stream stream = this.stream2; |
|||
|
|||
for (int i = 0; i < stream.Length; i++) |
|||
{ |
|||
r += stream.ReadByte(); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int BufferedReadStreamReadByte() |
|||
{ |
|||
int r = 0; |
|||
BufferedReadStream reader = this.bufferedStream2; |
|||
|
|||
for (int i = 0; i < reader.Length; i++) |
|||
{ |
|||
r += reader.ReadByte(); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int BufferedReadStreamWrapReadByte() |
|||
{ |
|||
int r = 0; |
|||
BufferedReadStreamWrapper reader = this.bufferedStreamWrap2; |
|||
|
|||
for (int i = 0; i < reader.Length; i++) |
|||
{ |
|||
r += reader.ReadByte(); |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int ArrayReadByte() |
|||
{ |
|||
byte[] b = this.buffer; |
|||
int r = 0; |
|||
for (int i = 0; i < b.Length; i++) |
|||
{ |
|||
r += b[i]; |
|||
} |
|||
|
|||
return r; |
|||
} |
|||
|
|||
private static byte[] CreateTestBytes() |
|||
{ |
|||
var buffer = new byte[BufferedReadStream.BufferLength * 3]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return buffer; |
|||
} |
|||
} |
|||
|
|||
/* |
|||
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19041 |
|||
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores |
|||
.NET Core SDK=3.1.301 |
|||
[Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT |
|||
Job-LKLBOT : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT |
|||
Job-RSTMKF : .NET Core 2.1.19 (CoreCLR 4.6.28928.01, CoreFX 4.6.28928.04), X64 RyuJIT |
|||
Job-PZIHIV : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT |
|||
|
|||
IterationCount=3 LaunchCount=1 WarmupCount=3 |
|||
|
|||
| Method | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |
|||
|------------------------------- |-------------- |----------:|------------:|-----------:|------:|--------:|------:|------:|------:|----------:| |
|||
| StandardStreamRead | .NET 4.7.2 | 63.238 us | 49.7827 us | 2.7288 us | 0.66 | 0.13 | - | - | - | - | |
|||
| BufferedReadStreamRead | .NET 4.7.2 | 66.092 us | 0.4273 us | 0.0234 us | 0.69 | 0.11 | - | - | - | - | |
|||
| BufferedReadStreamWrapRead | .NET 4.7.2 | 26.216 us | 3.0527 us | 0.1673 us | 0.27 | 0.04 | - | - | - | - | |
|||
| StandardStreamReadByte | .NET 4.7.2 | 97.900 us | 261.7204 us | 14.3458 us | 1.00 | 0.00 | - | - | - | - | |
|||
| BufferedReadStreamReadByte | .NET 4.7.2 | 97.260 us | 1.2979 us | 0.0711 us | 1.01 | 0.15 | - | - | - | - | |
|||
| BufferedReadStreamWrapReadByte | .NET 4.7.2 | 19.170 us | 2.2296 us | 0.1222 us | 0.20 | 0.03 | - | - | - | - | |
|||
| ArrayReadByte | .NET 4.7.2 | 12.878 us | 11.1292 us | 0.6100 us | 0.13 | 0.02 | - | - | - | - | |
|||
| | | | | | | | | | | | |
|||
| StandardStreamRead | .NET Core 2.1 | 60.618 us | 131.7038 us | 7.2191 us | 0.78 | 0.10 | - | - | - | - | |
|||
| BufferedReadStreamRead | .NET Core 2.1 | 30.006 us | 25.2499 us | 1.3840 us | 0.38 | 0.02 | - | - | - | - | |
|||
| BufferedReadStreamWrapRead | .NET Core 2.1 | 29.241 us | 6.5020 us | 0.3564 us | 0.37 | 0.01 | - | - | - | - | |
|||
| StandardStreamReadByte | .NET Core 2.1 | 78.074 us | 15.8463 us | 0.8686 us | 1.00 | 0.00 | - | - | - | - | |
|||
| BufferedReadStreamReadByte | .NET Core 2.1 | 14.737 us | 20.1510 us | 1.1045 us | 0.19 | 0.01 | - | - | - | - | |
|||
| BufferedReadStreamWrapReadByte | .NET Core 2.1 | 13.234 us | 1.4711 us | 0.0806 us | 0.17 | 0.00 | - | - | - | - | |
|||
| ArrayReadByte | .NET Core 2.1 | 9.373 us | 0.6108 us | 0.0335 us | 0.12 | 0.00 | - | - | - | - | |
|||
| | | | | | | | | | | | |
|||
| StandardStreamRead | .NET Core 3.1 | 52.151 us | 19.9456 us | 1.0933 us | 0.65 | 0.03 | - | - | - | - | |
|||
| BufferedReadStreamRead | .NET Core 3.1 | 29.217 us | 0.2490 us | 0.0136 us | 0.36 | 0.01 | - | - | - | - | |
|||
| BufferedReadStreamWrapRead | .NET Core 3.1 | 32.962 us | 7.1382 us | 0.3913 us | 0.41 | 0.02 | - | - | - | - | |
|||
| StandardStreamReadByte | .NET Core 3.1 | 80.310 us | 45.0350 us | 2.4685 us | 1.00 | 0.00 | - | - | - | - | |
|||
| BufferedReadStreamReadByte | .NET Core 3.1 | 13.092 us | 0.6268 us | 0.0344 us | 0.16 | 0.00 | - | - | - | - | |
|||
| BufferedReadStreamWrapReadByte | .NET Core 3.1 | 13.282 us | 3.8689 us | 0.2121 us | 0.17 | 0.01 | - | - | - | - | |
|||
| ArrayReadByte | .NET Core 3.1 | 9.349 us | 2.9860 us | 0.1637 us | 0.12 | 0.00 | - | - | - | - | |
|||
*/ |
|||
} |
|||
@ -0,0 +1,278 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.IO; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO |
|||
{ |
|||
public class BufferedReadStreamTests |
|||
{ |
|||
[Fact] |
|||
public void BufferedStreamCanReadSingleByteFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
Assert.Equal(expected[0], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength, stream.Position); |
|||
Assert.Equal(1, reader.Position); |
|||
} |
|||
|
|||
// Position of the stream should be reset on disposal.
|
|||
Assert.Equal(1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSingleByteFromOffset() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
const int offset = 5; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
reader.Position = offset; |
|||
|
|||
Assert.Equal(expected[offset], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength + offset, stream.Position); |
|||
Assert.Equal(offset + 1, reader.Position); |
|||
} |
|||
|
|||
Assert.Equal(offset + 1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSubsequentSingleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
int i; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], reader.ReadByte()); |
|||
Assert.Equal(i + 1, reader.Position); |
|||
|
|||
if (i < BufferedReadStream.BufferLength) |
|||
{ |
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
} |
|||
else if (i >= BufferedReadStream.BufferLength && i < BufferedReadStream.BufferLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Assert.Equal(i, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadMultipleBytesFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[0], buffer[0]); |
|||
Assert.Equal(expected[1], buffer[1]); |
|||
|
|||
// We've read a whole chunk but increment by the buffer length in our reader.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
Assert.Equal(buffer.Length, reader.Position); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSubsequentMultipleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[o], buffer[0]); |
|||
Assert.Equal(expected[o + 1], buffer[1]); |
|||
Assert.Equal(o + 2, reader.Position); |
|||
|
|||
int offset = i * 2; |
|||
if (offset < BufferedReadStream.BufferLength) |
|||
{ |
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
} |
|||
else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
Span<byte> buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[o], buffer[0]); |
|||
Assert.Equal(expected[o + 1], buffer[1]); |
|||
Assert.Equal(o + 2, reader.Position); |
|||
|
|||
int offset = i * 2; |
|||
if (offset < BufferedReadStream.BufferLength) |
|||
{ |
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
} |
|||
else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanSkip() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
int skip = 50; |
|||
int plusOne = 1; |
|||
int skip2 = BufferedReadStream.BufferLength; |
|||
|
|||
// Skip
|
|||
reader.Skip(skip); |
|||
Assert.Equal(skip, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
|
|||
// Read
|
|||
Assert.Equal(expected[skip], reader.ReadByte()); |
|||
|
|||
// Skip Again
|
|||
reader.Skip(skip2); |
|||
|
|||
// First Skip + First Read + Second Skip
|
|||
int position = skip + plusOne + skip2; |
|||
|
|||
Assert.Equal(position, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
Assert.Equal(expected[position], reader.ReadByte()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamReadsSmallStream() |
|||
{ |
|||
// Create a stream smaller than the default buffer length
|
|||
using (MemoryStream stream = this.CreateTestStream(BufferedReadStream.BufferLength / 4)) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
const int offset = 5; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
reader.Position = offset; |
|||
|
|||
Assert.Equal(expected[offset], reader.ReadByte()); |
|||
|
|||
// We've read a whole length of the stream but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength / 4, stream.Position); |
|||
Assert.Equal(offset + 1, reader.Position); |
|||
} |
|||
|
|||
Assert.Equal(offset + 1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (int i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], reader.ReadByte()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3) |
|||
{ |
|||
var buffer = new byte[length]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new EvilStream(buffer); |
|||
} |
|||
|
|||
// Simulates a stream that can only return 1 byte at a time per read instruction.
|
|||
// See https://github.com/SixLabors/ImageSharp/issues/1268
|
|||
private class EvilStream : MemoryStream |
|||
{ |
|||
public EvilStream(byte[] buffer) |
|||
: base(buffer) |
|||
{ |
|||
} |
|||
|
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
return base.Read(buffer, offset, 1); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,176 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO |
|||
{ |
|||
public class DoubleBufferedStreamReaderTests |
|||
{ |
|||
private readonly MemoryAllocator allocator = Configuration.Default.MemoryAllocator; |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSingleByteFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
|
|||
Assert.Equal(expected[0], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
Assert.Equal(1, reader.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSingleByteFromOffset() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
const int offset = 5; |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
reader.Position = offset; |
|||
|
|||
Assert.Equal(expected[offset], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength + offset); |
|||
Assert.Equal(offset + 1, reader.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSubsequentSingleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
|
|||
for (int i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], reader.ReadByte()); |
|||
Assert.Equal(i + 1, reader.Position); |
|||
|
|||
if (i < DoubleBufferedStreamReader.ChunkLength) |
|||
{ |
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
} |
|||
else if (i >= DoubleBufferedStreamReader.ChunkLength && i < DoubleBufferedStreamReader.ChunkLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadMultipleBytesFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
|
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[0], buffer[0]); |
|||
Assert.Equal(expected[1], buffer[1]); |
|||
|
|||
// We've read a whole chunk but increment by the buffer length in our reader.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
Assert.Equal(buffer.Length, reader.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanReadSubsequentMultipleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
|
|||
for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[o], buffer[0]); |
|||
Assert.Equal(expected[o + 1], buffer[1]); |
|||
Assert.Equal(o + 2, reader.Position); |
|||
|
|||
int offset = i * 2; |
|||
if (offset < DoubleBufferedStreamReader.ChunkLength) |
|||
{ |
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength); |
|||
} |
|||
else if (offset >= DoubleBufferedStreamReader.ChunkLength && offset < DoubleBufferedStreamReader.ChunkLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, DoubleBufferedStreamReader.ChunkLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DoubleBufferedStreamReaderCanSkip() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
var reader = new DoubleBufferedStreamReader(this.allocator, stream); |
|||
|
|||
int skip = 50; |
|||
int plusOne = 1; |
|||
int skip2 = DoubleBufferedStreamReader.ChunkLength; |
|||
|
|||
// Skip
|
|||
reader.Skip(skip); |
|||
Assert.Equal(skip, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
|
|||
// Read
|
|||
Assert.Equal(expected[skip], reader.ReadByte()); |
|||
|
|||
// Skip Again
|
|||
reader.Skip(skip2); |
|||
|
|||
// First Skip + First Read + Second Skip
|
|||
int position = skip + plusOne + skip2; |
|||
|
|||
Assert.Equal(position, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
Assert.Equal(expected[position], reader.ReadByte()); |
|||
} |
|||
} |
|||
|
|||
private MemoryStream CreateTestStream() |
|||
{ |
|||
var buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new MemoryStream(buffer); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue