From 76429a03c623688d338b5e1a45112b89de76527a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 29 Apr 2018 18:37:01 +1000 Subject: [PATCH] Faster multibyte reading and fix test --- .../Components/DoubleBufferedStreamReader.cs | 150 ++++++++++++++---- .../Codecs/Jpeg/DoubleBufferedStreams.cs | 53 ++++++- .../Jpg/DoubleBufferedStreamReaderTests.cs | 10 +- 3 files changed, 164 insertions(+), 49 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs index 458ddc4620..90f55bc5db 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/DoubleBufferedStreamReader.cs @@ -20,15 +20,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// public const int ChunkLength = 4096; + private const int ChunkLengthMinusOne = ChunkLength - 1; + + private const int ChunkLengthPlusOne = ChunkLength + 1; + private readonly Stream stream; private readonly IManagedByteBuffer buffer; - private readonly byte[] chunk; + private readonly byte[] bufferChunk; private int bytesRead; - private long position; + private int position; + + private int length; /// /// Initializes a new instance of the class. @@ -38,30 +44,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components public DoubleBufferedStreamReader(MemoryManager memoryManager, Stream stream) { this.stream = stream; - this.Length = stream.Length; + this.length = (int)stream.Length; this.buffer = memoryManager.AllocateCleanManagedByteBuffer(ChunkLength); - this.chunk = this.buffer.Array; + this.bufferChunk = this.buffer.Array; } /// /// Gets the length, in bytes, of the stream /// - public long Length { get; } + public long Length => this.length; /// /// Gets or sets the current position within the stream /// public long Position { - get - { - return this.position; - } + get => this.position; set { // Reset everything. It's easier than tracking. - this.position = value; + this.position = (int)value; this.bytesRead = ChunkLength; } } @@ -74,19 +77,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ReadByte() { - if (this.position >= this.Length) + if (this.position >= this.length) { return -1; } - if (this.position == 0 || this.bytesRead >= ChunkLength) + if (this.position == 0 || this.bytesRead > ChunkLengthMinusOne) { return this.ReadByteSlow(); } else { this.position++; - return this.chunk[this.bytesRead++]; + return this.bufferChunk[this.bytesRead++]; } } @@ -94,6 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// Skips the number of bytes in the stream /// /// The number of bytes to skip + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Skip(int count) { this.Position += count; @@ -118,35 +122,50 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// of bytes requested if that many bytes are not currently available, or zero (0) /// if the end of the stream has been reached. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Read(byte[] buffer, int offset, int count) { - int n = 0; - if (buffer.Length <= ChunkLength) + if (buffer.Length < ChunkLengthPlusOne) { if (this.position == 0 || count + this.bytesRead > ChunkLength) { - // Refill our buffer then copy. - this.stream.Seek(this.position, SeekOrigin.Begin); - this.stream.Read(this.chunk, 0, ChunkLength); - this.bytesRead = 0; + return this.ReadToChunkSlow(buffer, offset, count); } - Buffer.BlockCopy(this.chunk, this.bytesRead, buffer, offset, count); - this.position += count; - this.bytesRead += count; + int n = this.length - this.position; + if (n > count) + { + n = count; + } - n = Math.Min(count, (int)(this.Length - this.position)); - } - else - { - // Read to target but don't copy to our chunk. - this.stream.Seek(this.position, SeekOrigin.Begin); - n = this.stream.Read(buffer, offset, count); + if (n < 0) + { + n = 0; + } - this.Position += count; + if (n < 9) + { + int byteCount = n; + int read = this.bytesRead; + byte[] chunk = this.bufferChunk; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = chunk[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.bufferChunk, this.bytesRead, buffer, offset, n); + } + + this.position += n; + this.bytesRead += n; + + return n; } - return Math.Max(n, 0); + return this.ReadToBufferSlow(buffer, offset, count); } /// @@ -158,12 +177,75 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components [MethodImpl(MethodImplOptions.NoInlining)] private int ReadByteSlow() { - this.stream.Seek(this.position, SeekOrigin.Begin); - this.stream.Read(this.chunk, 0, ChunkLength); + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + this.stream.Read(this.bufferChunk, 0, ChunkLength); this.bytesRead = 0; this.position++; - return this.chunk[this.bytesRead++]; + return this.bufferChunk[this.bytesRead++]; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToChunkSlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + this.stream.Read(this.bufferChunk, 0, ChunkLength); + this.bytesRead = 0; + + int n = this.length - this.position; + if (n > count) + { + n = count; + } + + if (n < 0) + { + n = 0; + } + + if (n < 9) + { + int byteCount = n; + int read = this.bytesRead; + byte[] chunk = this.bufferChunk; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = chunk[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.bufferChunk, this.bytesRead, buffer, offset, n); + } + + this.position += n; + this.bytesRead += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + 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; } } } \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs index d178a4970c..1d76d58a51 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs @@ -12,17 +12,25 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg public class DoubleBufferedStreams { private byte[] buffer = CreateTestBytes(); + private byte[] chunk1 = new byte[2]; + private byte[] chunk2 = new byte[2]; private MemoryStream stream1; private MemoryStream stream2; - DoubleBufferedStreamReader reader; + private MemoryStream stream3; + private MemoryStream stream4; + DoubleBufferedStreamReader reader1; + DoubleBufferedStreamReader reader2; [GlobalSetup] public void CreateStreams() { this.stream1 = new MemoryStream(this.buffer); this.stream2 = new MemoryStream(this.buffer); - this.reader = new DoubleBufferedStreamReader(Configuration.Default.MemoryManager, this.stream2); + this.stream3 = new MemoryStream(this.buffer); + this.stream4 = new MemoryStream(this.buffer); + this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryManager, this.stream2); + this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryManager, this.stream2); } [GlobalCleanup] @@ -30,11 +38,14 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg { this.stream1?.Dispose(); this.stream2?.Dispose(); - this.reader?.Dispose(); + this.stream3?.Dispose(); + this.stream4?.Dispose(); + this.reader1?.Dispose(); + this.reader2?.Dispose(); } [Benchmark(Baseline = true)] - public int StandardStream() + public int StandardStreamReadByte() { int r = 0; Stream stream = this.stream1; @@ -48,10 +59,25 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg } [Benchmark] - public int DoubleBufferedStream() + public int StandardStreamRead() { int r = 0; - DoubleBufferedStreamReader reader = this.reader; + 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++) { @@ -61,6 +87,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg 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; + } + private static byte[] CreateTestBytes() { byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3]; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs index 61017ce9b0..bc099bdc5e 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DoubleBufferedStreamReaderTests.cs @@ -90,16 +90,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) { - if (o + 2 == expected.Length) - { - // We've reached the end of the stream - Assert.Equal(0, reader.Read(buffer, 0, 2)); - } - else - { - Assert.Equal(2, reader.Read(buffer, 0, 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);