Browse Source

Faster Jpeg Huffman Decoding. (#894)

* Read from underlying stream less often

* Update benchmark dependencies

* Experimental mango port

Currently broken

* Populate table, 64byte buffer

Still broken.

* Baseline, non RST works

* 15/19 baseline tests pass now.

* Optimize position change.

* 18/19 pass

* 19/19 baseline decoded

* Can now decode all images.

* Now faster and much cleaner.

* Cleanup

* Fix reader, update benchmarks

* Update dependencies

* Remove unused method

* No need to clean initial buffer

* Remove bounds check on ReadByte()

* Refactor from feedback
pull/903/head
James Jackson-South 7 years ago
committed by GitHub
parent
commit
2fcba54a3e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
  2. 61
      src/ImageSharp/Formats/Jpeg/Components/Decoder/FastACTable.cs
  3. 188
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
  4. 694
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  5. 172
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
  6. 1058
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ScanDecoder.cs
  7. 31
      src/ImageSharp/Formats/Jpeg/JpegConstants.cs
  8. 10
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  9. 3
      src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs
  10. 115
      src/ImageSharp/IO/DoubleBufferedStreamReader.cs
  11. 4
      src/ImageSharp/ImageSharp.csproj
  12. 17
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs
  13. 25
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs
  14. 42
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs
  15. 2
      tests/ImageSharp.Benchmarks/Codecs/MultiImageBenchmarkBase.cs
  16. 2
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  17. 4
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj

2
src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

@ -39,7 +39,7 @@
<AdditionalFiles Include="..\..\standards\stylecop.json" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta0008" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0008" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-rc.114" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup>

61
src/ImageSharp/Formats/Jpeg/Components/Decoder/FastACTable.cs

@ -1,61 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal unsafe struct FastACTable
{
/// <summary>
/// Gets the lookahead array.
/// </summary>
public fixed short Lookahead[512];
/// <summary>
/// Derives a lookup table for fast AC entropy scan decoding.
/// This can happen multiple times during progressive decoding but always outside mcu loops.
/// </summary>
/// <param name="huffmanTable">The AC Huffman table.</param>
public void Derive(ref HuffmanTable huffmanTable)
{
const int FastBits = ScanDecoder.FastBits;
ref short fastACRef = ref this.Lookahead[0];
ref byte huffmanLookaheadRef = ref huffmanTable.Lookahead[0];
ref byte huffmanValuesRef = ref huffmanTable.Values[0];
ref short huffmanSizesRef = ref huffmanTable.Sizes[0];
int i;
for (i = 0; i < (1 << FastBits); i++)
{
byte fast = Unsafe.Add(ref huffmanLookaheadRef, i);
Unsafe.Add(ref fastACRef, i) = 0;
if (fast < byte.MaxValue)
{
int rs = Unsafe.Add(ref huffmanValuesRef, fast);
int run = (rs >> 4) & 15;
int magbits = rs & 15;
int len = Unsafe.Add(ref huffmanSizesRef, fast);
if (magbits != 0 && len + magbits <= FastBits)
{
// Magnitude code followed by receive_extend code
int k = ((i << len) & ((1 << FastBits) - 1)) >> (FastBits - magbits);
int m = 1 << (magbits - 1);
if (k < m)
{
k += (int)((~0U << magbits) + 1);
}
// If the result is small enough, we can fit it in fastAC table
if (k >= -128 && k <= 127)
{
Unsafe.Add(ref fastACRef, i) = (short)((k << 8) + (run << 4) + (len + magbits));
}
}
}
}
}
}
}

188
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs

@ -0,0 +1,188 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Used to buffer and track the bits read from the Huffman entropy encoded data.
/// </summary>
internal struct HuffmanScanBuffer
{
private readonly DoubleBufferedStreamReader stream;
// The entropy encoded code buffer.
private ulong data;
// The number of valid bits left to read in the buffer.
private int remain;
// Whether there is more data to pull from the stream for the current mcu.
private bool noMore;
public HuffmanScanBuffer(DoubleBufferedStreamReader stream)
{
this.stream = stream;
this.data = 0ul;
this.remain = 0;
this.Marker = JpegConstants.Markers.XFF;
this.MarkerPosition = 0;
this.BadMarker = false;
this.noMore = false;
this.Eof = false;
}
/// <summary>
/// Gets or sets the current, if any, marker in the input stream.
/// </summary>
public byte Marker { get; set; }
/// <summary>
/// Gets or sets the opening position of an identified marker.
/// </summary>
public long MarkerPosition { get; set; }
/// <summary>
/// Gets or sets a value indicating whether we have a bad marker, I.E. One that is not between RST0 and RST7
/// </summary>
public bool BadMarker { get; set; }
/// <summary>
/// Gets or sets a value indicating whether we have prematurely reached the end of the file.
/// </summary>
public bool Eof { get; set; }
[MethodImpl(InliningOptions.ShortMethod)]
public void CheckBits()
{
if (this.remain < 16)
{
this.FillBuffer();
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.data = 0ul;
this.remain = 0;
this.Marker = JpegConstants.Markers.XFF;
this.MarkerPosition = 0;
this.BadMarker = false;
this.noMore = false;
this.Eof = false;
}
[MethodImpl(InliningOptions.ShortMethod)]
public bool HasRestart()
{
byte m = this.Marker;
return m >= JpegConstants.Markers.RST0 && m <= JpegConstants.Markers.RST7;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void FillBuffer()
{
// Attempt to load at least the minimum number of required bits into the buffer.
// We fail to do so only if we hit a marker or reach the end of the input stream.
this.remain += 48;
this.data = (this.data << 48) | this.GetBytes();
}
[MethodImpl(InliningOptions.ShortMethod)]
public unsafe int DecodeHuffman(ref HuffmanTable h)
{
this.CheckBits();
int v = this.PeekBits(JpegConstants.Huffman.LookupBits);
int symbol = h.LookaheadValue[v];
int size = h.LookaheadSize[v];
if (size == JpegConstants.Huffman.SlowBits)
{
ulong x = this.data << (JpegConstants.Huffman.RegisterSize - this.remain);
while (x > h.MaxCode[size])
{
size++;
}
v = (int)(x >> (JpegConstants.Huffman.RegisterSize - size));
symbol = h.Values[h.ValOffset[size] + v];
}
this.remain -= size;
return symbol;
}
[MethodImpl(InliningOptions.ShortMethod)]
public int Receive(int nbits)
{
this.CheckBits();
return Extend(this.GetBits(nbits), nbits);
}
[MethodImpl(InliningOptions.ShortMethod)]
public int GetBits(int nbits) => (int)ExtractBits(this.data, this.remain -= nbits, nbits);
[MethodImpl(InliningOptions.ShortMethod)]
public int PeekBits(int nbits) => (int)ExtractBits(this.data, this.remain - nbits, nbits);
[MethodImpl(InliningOptions.ShortMethod)]
private static ulong ExtractBits(ulong value, int offset, int size) => (value >> offset) & (ulong)((1 << size) - 1);
[MethodImpl(InliningOptions.ShortMethod)]
private static int Extend(int v, int nbits) => v - ((((v + v) >> nbits) - 1) & ((1 << nbits) - 1));
[MethodImpl(InliningOptions.ShortMethod)]
private ulong GetBytes()
{
ulong temp = 0;
for (int i = 0; i < 6; i++)
{
int b = this.noMore ? 0 : this.stream.ReadByte();
if (b == -1)
{
// We've encountered the end of the file stream which means there's no EOI marker in the image
// or the SOS marker has the wrong dimensions set.
this.Eof = true;
b = 0;
}
// Found a marker.
if (b == JpegConstants.Markers.XFF)
{
this.MarkerPosition = this.stream.Position - 1;
int c = this.stream.ReadByte();
while (c == JpegConstants.Markers.XFF)
{
c = this.stream.ReadByte();
if (c == -1)
{
this.Eof = true;
c = 0;
break;
}
}
if (c != 0)
{
this.Marker = (byte)c;
this.noMore = true;
if (!this.HasRestart())
{
this.BadMarker = true;
}
}
}
temp = (temp << 8) | (ulong)(long)b;
}
return temp;
}
}
}

694
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -0,0 +1,694 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Decodes the Huffman encoded spectral scan.
/// Originally ported from <see href="https://github.com/t0rakka/mango"/>
/// with additional fixes for both performance and common encoding errors.
/// </summary>
internal class HuffmanScanDecoder
{
private readonly JpegFrame frame;
private readonly HuffmanTable[] dcHuffmanTables;
private readonly HuffmanTable[] acHuffmanTables;
private readonly DoubleBufferedStreamReader stream;
private readonly JpegComponent[] components;
// The restart interval.
private readonly int restartInterval;
// The number of interleaved components.
private readonly int componentsLength;
// The spectral selection start.
private readonly int spectralStart;
// The spectral selection end.
private readonly int spectralEnd;
// The successive approximation high bit end.
private readonly int successiveHigh;
// The successive approximation low bit end.
private readonly int successiveLow;
// How many mcu's are left to do.
private int todo;
// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero.
private int eobrun;
// The unzig data.
private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="frame">The image frame.</param>
/// <param name="dcHuffmanTables">The DC Huffman tables.</param>
/// <param name="acHuffmanTables">The AC Huffman tables.</param>
/// <param name="componentsLength">The length of the components. Different to the array length.</param>
/// <param name="restartInterval">The reset interval.</param>
/// <param name="spectralStart">The spectral selection start.</param>
/// <param name="spectralEnd">The spectral selection end.</param>
/// <param name="successiveHigh">The successive approximation bit high end.</param>
/// <param name="successiveLow">The successive approximation bit low end.</param>
public HuffmanScanDecoder(
DoubleBufferedStreamReader stream,
JpegFrame frame,
HuffmanTable[] dcHuffmanTables,
HuffmanTable[] acHuffmanTables,
int componentsLength,
int restartInterval,
int spectralStart,
int spectralEnd,
int successiveHigh,
int successiveLow)
{
this.dctZigZag = ZigZag.CreateUnzigTable();
this.stream = stream;
this.scanBuffer = new HuffmanScanBuffer(stream);
this.frame = frame;
this.dcHuffmanTables = dcHuffmanTables;
this.acHuffmanTables = acHuffmanTables;
this.components = frame.Components;
this.componentsLength = componentsLength;
this.restartInterval = restartInterval;
this.todo = restartInterval;
this.spectralStart = spectralStart;
this.spectralEnd = spectralEnd;
this.successiveHigh = successiveHigh;
this.successiveLow = successiveLow;
}
/// <summary>
/// Decodes the entropy coded data.
/// </summary>
public void ParseEntropyCodedData()
{
if (!this.frame.Progressive)
{
this.ParseBaselineData();
}
else
{
this.ParseProgressiveData();
}
if (this.scanBuffer.BadMarker)
{
this.stream.Position = this.scanBuffer.MarkerPosition;
}
}
private void ParseBaselineData()
{
if (this.componentsLength == 1)
{
this.ParseBaselineDataNonInterleaved();
}
else
{
this.ParseBaselineDataInterleaved();
}
}
private unsafe void ParseBaselineDataInterleaved()
{
// Interleaved
int mcu = 0;
int mcusPerColumn = this.frame.McusPerColumn;
int mcusPerLine = this.frame.McusPerLine;
// Pre-derive the huffman table to avoid in-loop checks.
for (int i = 0; i < this.componentsLength; i++)
{
int order = this.frame.ComponentOrder[i];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
dcHuffmanTable.Configure();
acHuffmanTable.Configure();
}
for (int j = 0; j < mcusPerColumn; j++)
{
for (int i = 0; i < mcusPerLine; i++)
{
// Scan an interleaved mcu... process components in order
int mcuRow = mcu / mcusPerLine;
int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < this.componentsLength; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
int h = component.HorizontalSamplingFactor;
int v = component.VerticalSamplingFactor;
// Scan out an mcu's worth of this component; that's just determined
// by the basic H and V specified for the component
for (int y = 0; y < v; y++)
{
int blockRow = (mcuRow * v) + y;
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(blockRow);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int x = 0; x < h; x++)
{
int blockCol = (mcuCol * h) + x;
this.DecodeBlockBaseline(
component,
ref Unsafe.Add(ref blockRef, blockCol),
ref dcHuffmanTable,
ref acHuffmanTable);
}
}
}
// After all interleaved components, that's an interleaved MCU,
// so now count down the restart interval
mcu++;
this.HandleRestart();
}
}
}
private unsafe void ParseBaselineDataNonInterleaved()
{
JpegComponent component = this.components[this.frame.ComponentOrder[0]];
int w = component.WidthInBlocks;
int h = component.HeightInBlocks;
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
dcHuffmanTable.Configure();
acHuffmanTable.Configure();
int mcu = 0;
for (int j = 0; j < h; j++)
{
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int i = 0; i < w; i++)
{
this.DecodeBlockBaseline(
component,
ref Unsafe.Add(ref blockRef, i),
ref dcHuffmanTable,
ref acHuffmanTable);
// Every data block is an MCU, so countdown the restart interval
mcu++;
this.HandleRestart();
}
}
}
private void CheckProgressiveData()
{
// Validate successive scan parameters.
// Logic has been adapted from libjpeg.
// See Table B.3 – Scan header parameter size and values. itu-t81.pdf
bool invalid = false;
if (this.spectralStart == 0)
{
if (this.spectralEnd != 0)
{
invalid = true;
}
}
else
{
// Need not check Ss/Se < 0 since they came from unsigned bytes.
if (this.spectralEnd < this.spectralStart || this.spectralEnd > 63)
{
invalid = true;
}
// AC scans may have only one component.
if (this.componentsLength != 1)
{
invalid = true;
}
}
if (this.successiveHigh != 0)
{
// Successive approximation refinement scan: must have Al = Ah-1.
if (this.successiveHigh - 1 != this.successiveLow)
{
invalid = true;
}
}
// TODO: How does this affect 12bit jpegs.
// According to libjpeg the range covers 8bit only?
if (this.successiveLow > 13)
{
invalid = true;
}
if (invalid)
{
JpegThrowHelper.ThrowBadProgressiveScan(this.spectralStart, this.spectralEnd, this.successiveHigh, this.successiveLow);
}
}
private void ParseProgressiveData()
{
this.CheckProgressiveData();
if (this.componentsLength == 1)
{
this.ParseProgressiveDataNonInterleaved();
}
else
{
this.ParseProgressiveDataInterleaved();
}
}
private void ParseProgressiveDataInterleaved()
{
// Interleaved
int mcu = 0;
int mcusPerColumn = this.frame.McusPerColumn;
int mcusPerLine = this.frame.McusPerLine;
// Pre-derive the huffman table to avoid in-loop checks.
for (int k = 0; k < this.componentsLength; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
}
for (int j = 0; j < mcusPerColumn; j++)
{
for (int i = 0; i < mcusPerLine; i++)
{
// Scan an interleaved mcu... process components in order
int mcuRow = mcu / mcusPerLine;
int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < this.componentsLength; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
int h = component.HorizontalSamplingFactor;
int v = component.VerticalSamplingFactor;
// Scan out an mcu's worth of this component; that's just determined
// by the basic H and V specified for the component
for (int y = 0; y < v; y++)
{
int blockRow = (mcuRow * v) + y;
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(blockRow);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int x = 0; x < h; x++)
{
if (buffer.Eof)
{
return;
}
int blockCol = (mcuCol * h) + x;
this.DecodeBlockProgressiveDC(
component,
ref Unsafe.Add(ref blockRef, blockCol),
ref dcHuffmanTable);
}
}
}
// After all interleaved components, that's an interleaved MCU,
// so now count down the restart interval
mcu++;
this.HandleRestart();
}
}
}
private unsafe void ParseProgressiveDataNonInterleaved()
{
JpegComponent component = this.components[this.frame.ComponentOrder[0]];
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
int w = component.WidthInBlocks;
int h = component.HeightInBlocks;
if (this.spectralStart == 0)
{
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
int mcu = 0;
for (int j = 0; j < h; j++)
{
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int i = 0; i < w; i++)
{
if (buffer.Eof)
{
return;
}
this.DecodeBlockProgressiveDC(
component,
ref Unsafe.Add(ref blockRef, i),
ref dcHuffmanTable);
// Every data block is an MCU, so countdown the restart interval
mcu++;
this.HandleRestart();
}
}
}
else
{
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
acHuffmanTable.Configure();
int mcu = 0;
for (int j = 0; j < h; j++)
{
Span<Block8x8> blockSpan = component.SpectralBlocks.GetRowSpan(j);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int i = 0; i < w; i++)
{
if (buffer.Eof)
{
return;
}
this.DecodeBlockProgressiveAC(
ref Unsafe.Add(ref blockRef, i),
ref acHuffmanTable);
// Every data block is an MCU, so countdown the restart interval
mcu++;
this.HandleRestart();
}
}
}
}
private void DecodeBlockBaseline(
JpegComponent component,
ref Block8x8 block,
ref HuffmanTable dcTable,
ref HuffmanTable acTable)
{
ref short blockDataRef = ref Unsafe.As<Block8x8, short>(ref block);
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref ZigZag zigzag = ref this.dctZigZag;
// DC
int t = buffer.DecodeHuffman(ref dcTable);
if (t != 0)
{
t = buffer.Receive(t);
}
t += component.DcPredictor;
component.DcPredictor = t;
blockDataRef = (short)t;
// AC
for (int i = 1; i < 64;)
{
int s = buffer.DecodeHuffman(ref acTable);
int r = s >> 4;
s &= 15;
if (s != 0)
{
i += r;
s = buffer.Receive(s);
Unsafe.Add(ref blockDataRef, zigzag[i++]) = (short)s;
}
else
{
if (r == 0)
{
break;
}
i += 16;
}
}
}
private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 block, ref HuffmanTable dcTable)
{
ref short blockDataRef = ref Unsafe.As<Block8x8, short>(ref block);
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
if (this.successiveHigh == 0)
{
// First scan for DC coefficient, must be first
int s = buffer.DecodeHuffman(ref dcTable);
if (s != 0)
{
s = buffer.Receive(s);
}
s += component.DcPredictor;
component.DcPredictor = s;
blockDataRef = (short)(s << this.successiveLow);
}
else
{
// Refinement scan for DC coefficient
buffer.CheckBits();
blockDataRef |= (short)(buffer.GetBits(1) << this.successiveLow);
}
}
private void DecodeBlockProgressiveAC(ref Block8x8 block, ref HuffmanTable acTable)
{
ref short blockDataRef = ref Unsafe.As<Block8x8, short>(ref block);
if (this.successiveHigh == 0)
{
// MCU decoding for AC initial scan (either spectral selection,
// or first pass of successive approximation).
if (this.eobrun != 0)
{
--this.eobrun;
return;
}
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref ZigZag zigzag = ref this.dctZigZag;
int start = this.spectralStart;
int end = this.spectralEnd;
int low = this.successiveLow;
for (int i = start; i <= end; ++i)
{
int s = buffer.DecodeHuffman(ref acTable);
int r = s >> 4;
s &= 15;
i += r;
if (s != 0)
{
s = buffer.Receive(s);
Unsafe.Add(ref blockDataRef, zigzag[i]) = (short)(s << low);
}
else
{
if (r != 15)
{
this.eobrun = 1 << r;
if (r != 0)
{
buffer.CheckBits();
this.eobrun += buffer.GetBits(r);
}
--this.eobrun;
break;
}
}
}
}
else
{
// Refinement scan for these AC coefficients
this.DecodeBlockProgressiveACRefined(ref blockDataRef, ref acTable);
}
}
private void DecodeBlockProgressiveACRefined(ref short blockDataRef, ref HuffmanTable acTable)
{
// Refinement scan for these AC coefficients
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
ref ZigZag zigzag = ref this.dctZigZag;
int start = this.spectralStart;
int end = this.spectralEnd;
int p1 = 1 << this.successiveLow;
int m1 = (-1) << this.successiveLow;
int k = start;
if (this.eobrun == 0)
{
for (; k <= end; k++)
{
int s = buffer.DecodeHuffman(ref acTable);
int r = s >> 4;
s &= 15;
if (s != 0)
{
buffer.CheckBits();
if (buffer.GetBits(1) != 0)
{
s = p1;
}
else
{
s = m1;
}
}
else
{
if (r != 15)
{
this.eobrun = 1 << r;
if (r != 0)
{
buffer.CheckBits();
this.eobrun += buffer.GetBits(r);
}
break;
}
}
do
{
ref short coef = ref Unsafe.Add(ref blockDataRef, zigzag[k]);
if (coef != 0)
{
buffer.CheckBits();
if (buffer.GetBits(1) != 0)
{
if ((coef & p1) == 0)
{
coef += (short)(coef >= 0 ? p1 : m1);
}
}
}
else
{
if (--r < 0)
{
break;
}
}
k++;
}
while (k <= end);
if ((s != 0) && (k < 64))
{
Unsafe.Add(ref blockDataRef, zigzag[k]) = (short)s;
}
}
}
if (this.eobrun > 0)
{
for (; k <= end; k++)
{
ref short coef = ref Unsafe.Add(ref blockDataRef, zigzag[k]);
if (coef != 0)
{
buffer.CheckBits();
if (buffer.GetBits(1) != 0)
{
if ((coef & p1) == 0)
{
coef += (short)(coef >= 0 ? p1 : m1);
}
}
}
}
--this.eobrun;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private void Reset()
{
for (int i = 0; i < this.components.Length; i++)
{
this.components[i].DcPredictor = 0;
}
this.eobrun = 0;
this.scanBuffer.Reset();
}
[MethodImpl(InliningOptions.ShortMethod)]
private bool HandleRestart()
{
if (this.restartInterval > 0 && (--this.todo) == 0)
{
this.todo = this.restartInterval;
if (this.scanBuffer.HasRestart())
{
this.Reset();
return true;
}
if (this.scanBuffer.Marker != JpegConstants.Markers.XFF)
{
this.stream.Position = this.scanBuffer.MarkerPosition;
this.Reset();
return true;
}
}
return false;
}
}
}

172
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs

@ -2,53 +2,60 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Represents a Huffman Table
/// Represents a Huffman coding table containing basic coding data plus tables for accellerated computation.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct HuffmanTable
{
private bool isDerived;
private readonly MemoryAllocator memoryAllocator;
private bool isConfigured;
#pragma warning disable IDE0044 // Add readonly modifier
private fixed byte codeLengths[17];
#pragma warning restore IDE0044 // Add readonly modifier
/// <summary>
/// Derived from the DHT marker. Sizes[k] = # of symbols with codes of length k bits; Sizes[0] is unused.
/// </summary>
public fixed byte Sizes[17];
/// <summary>
/// Gets the max code array.
/// Derived from the DHT marker. Contains the symbols, in order of incremental code length.
/// </summary>
public fixed uint MaxCode[18];
public fixed byte Values[256];
/// <summary>
/// Gets the value offset array.
/// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to
/// ensure <see cref="HuffmanScanBuffer.DecodeHuffman"/> terminates.
/// </summary>
public fixed int ValOffset[18];
public fixed ulong MaxCode[18];
/// <summary>
/// Gets the huffman value array.
/// Values[] offset for codes of length k ValOffset[k] = Values[] index of 1st symbol of code length
/// k, less the smallest code of length k; so given a code of length k, the corresponding symbol is
/// Values[code + ValOffset[k]].
/// </summary>
public fixed byte Values[256];
public fixed int ValOffset[19];
/// <summary>
/// Gets the lookahead array.
/// Contains the length of bits for the given k value.
/// </summary>
public fixed byte Lookahead[512];
public fixed byte LookaheadSize[JpegConstants.Huffman.LookupSize];
/// <summary>
/// Gets the sizes array
/// Lookahead table: indexed by the next <see cref="JpegConstants.Huffman.LookupBits"/> bits of
/// the input data stream. If the next Huffman code is no more
/// than <see cref="JpegConstants.Huffman.LookupBits"/> bits long, we can obtain its length and
/// the corresponding symbol directly from this tables.
///
/// The lower 8 bits of each table entry contain the number of
/// bits in the corresponding Huffman code, or <see cref="JpegConstants.Huffman.LookupBits"/> + 1
/// if too long. The next 8 bits of each entry contain the symbol.
/// </summary>
public fixed short Sizes[257];
public fixed byte LookaheadValue[JpegConstants.Huffman.LookupSize];
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanTable"/> struct.
@ -58,91 +65,104 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="values">The huffman values</param>
public HuffmanTable(MemoryAllocator memoryAllocator, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
{
this.isDerived = false;
this.isConfigured = false;
this.memoryAllocator = memoryAllocator;
Unsafe.CopyBlockUnaligned(ref this.codeLengths[0], ref MemoryMarshal.GetReference(codeLengths), (uint)codeLengths.Length);
Unsafe.CopyBlockUnaligned(ref this.Sizes[0], ref MemoryMarshal.GetReference(codeLengths), (uint)codeLengths.Length);
Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length);
}
/// <summary>
/// Expands the HuffmanTable into its derived form.
/// Expands the HuffmanTable into its readable form.
/// </summary>
public void Derive()
public void Configure()
{
if (this.isDerived)
if (this.isConfigured)
{
return;
}
const int Length = 257;
using (IMemoryOwner<short> huffcode = this.memoryAllocator.Allocate<short>(Length))
{
ref short huffcodeRef = ref MemoryMarshal.GetReference(huffcode.GetSpan());
ref byte codeLengthsRef = ref this.codeLengths[0];
int p, si;
Span<char> huffsize = stackalloc char[257];
Span<uint> huffcode = stackalloc uint[257];
uint code;
// Figure C.1: make table of Huffman code length for each symbol
ref short sizesRef = ref this.Sizes[0];
short x = 0;
for (short i = 1; i < 17; i++)
// Figure C.1: make table of Huffman code length for each symbol
p = 0;
for (int l = 1; l <= 16; l++)
{
int i = this.Sizes[l];
while (i-- != 0)
{
byte length = Unsafe.Add(ref codeLengthsRef, i);
for (short j = 0; j < length; j++)
{
Unsafe.Add(ref sizesRef, x++) = i;
}
huffsize[p++] = (char)l;
}
}
Unsafe.Add(ref sizesRef, x) = 0;
// Figure C.2: generate the codes themselves
int si = 0;
ref int valOffsetRef = ref this.ValOffset[0];
ref uint maxcodeRef = ref this.MaxCode[0];
huffsize[p] = (char)0;
uint code = 0;
int k;
for (k = 1; k < 17; k++)
// Figure C.2: generate the codes themselves
code = 0;
si = huffsize[0];
p = 0;
while (huffsize[p] != 0)
{
while (huffsize[p] == si)
{
// Compute delta to add to code to compute symbol id.
Unsafe.Add(ref valOffsetRef, k) = (int)(si - code);
if (Unsafe.Add(ref sizesRef, si) == k)
{
while (Unsafe.Add(ref sizesRef, si) == k)
{
Unsafe.Add(ref huffcodeRef, si++) = (short)code++;
}
}
huffcode[p++] = code;
code++;
}
code <<= 1;
si++;
}
// Figure F.15: generate decoding tables for bit-sequential decoding.
// Compute largest code + 1 for this size. preshifted as we need it later.
Unsafe.Add(ref maxcodeRef, k) = code << (16 - k);
code <<= 1;
// Figure F.15: generate decoding tables for bit-sequential decoding
p = 0;
for (int l = 1; l <= 16; l++)
{
if (this.Sizes[l] != 0)
{
int offset = p - (int)huffcode[p];
this.ValOffset[l] = offset;
p += this.Sizes[l];
this.MaxCode[l] = huffcode[p - 1]; // Maximum code of length l
this.MaxCode[l] <<= 64 - l; // Left justify
this.MaxCode[l] |= (1ul << (64 - l)) - 1;
}
else
{
this.MaxCode[l] = 0;
}
}
Unsafe.Add(ref maxcodeRef, k) = 0xFFFFFFFF;
this.ValOffset[18] = 0;
this.MaxCode[17] = ulong.MaxValue; // Ensures huff decode terminates
// Generate non-spec lookup tables to speed up decoding.
const int FastBits = ScanDecoder.FastBits;
ref byte lookaheadRef = ref this.Lookahead[0];
Unsafe.InitBlockUnaligned(ref lookaheadRef, 0xFF, 1 << FastBits); // Flag for non-accelerated
// Compute lookahead tables to speed up decoding.
// First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long";
// then we iterate through the Huffman codes that are short enough and
// fill in all the entries that correspond to bit sequences starting
// with that code.
ref byte lookupSizeRef = ref this.LookaheadSize[0];
Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize);
for (int i = 0; i < si; i++)
p = 0;
for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++)
{
for (int i = 1; i <= this.Sizes[length]; i++, p++)
{
int size = Unsafe.Add(ref sizesRef, i);
if (size <= FastBits)
// length = current code's length, p = its index in huffcode[] & huffval[].
// Generate left-justified code followed by all possible bit sequences
int lookbits = (int)(huffcode[p] << (JpegConstants.Huffman.LookupBits - length));
for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--)
{
int fastOffset = FastBits - size;
int fastCode = Unsafe.Add(ref huffcodeRef, i) << fastOffset;
int fastMax = 1 << fastOffset;
for (int left = 0; left < fastMax; left++)
{
Unsafe.Add(ref lookaheadRef, fastCode + left) = (byte)i;
}
this.LookaheadSize[lookbits] = (byte)length;
this.LookaheadValue[lookbits] = this.Values[p];
lookbits++;
}
}
}
this.isDerived = true;
this.isConfigured = true;
}
}
}

1058
src/ImageSharp/Formats/Jpeg/Components/Decoder/ScanDecoder.cs

File diff suppressed because it is too large

31
src/ImageSharp/Formats/Jpeg/JpegConstants.cs

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public static readonly IEnumerable<string> FileExtensions = new[] { "jpg", "jpeg", "jfif" };
/// <summary>
/// Contains marker specific constants
/// Contains marker specific constants.
/// </summary>
// ReSharper disable InconsistentNaming
internal static class Markers
@ -219,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <summary>
/// Contains Adobe specific constants
/// Contains Adobe specific constants.
/// </summary>
internal static class Adobe
{
@ -238,5 +238,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
public const byte ColorTransformYcck = 2;
}
/// <summary>
/// Contains Huffman specific constants.
/// </summary>
internal static class Huffman
{
/// <summary>
/// The size of the huffman decoder register.
/// </summary>
public const int RegisterSize = 64;
/// <summary>
/// If the next Huffman code is no more than this number of bits, we can obtain its length
/// and the corresponding symbol directly from this tables.
/// </summary>
public const int LookupBits = 8;
/// <summary>
/// If a Huffman code is this number of bits we cannot use the lookup table to determine its value.
/// </summary>
public const int SlowBits = LookupBits + 1;
/// <summary>
/// The size of the lookup table.
/// </summary>
public const int LookupSize = 1 << LookupBits;
}
}
}

10
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -59,11 +59,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private HuffmanTable[] acHuffmanTables;
/// <summary>
/// The fast AC tables used for entropy decoding
/// </summary>
private FastACTable[] fastACTables;
/// <summary>
/// The reset interval determined by RST markers
/// </summary>
@ -269,7 +264,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
const int maxTables = 4;
this.dcHuffmanTables = new HuffmanTable[maxTables];
this.acHuffmanTables = new HuffmanTable[maxTables];
this.fastACTables = new FastACTable[maxTables];
}
// Break only when we discover a valid EOI marker.
@ -385,7 +379,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Frame = null;
this.dcHuffmanTables = null;
this.acHuffmanTables = null;
this.fastACTables = null;
}
/// <summary>
@ -936,12 +929,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int spectralEnd = this.temp[1];
int successiveApproximation = this.temp[2];
var sd = new ScanDecoder(
var sd = new HuffmanScanDecoder(
this.InputStream,
this.Frame,
this.dcHuffmanTables,
this.acHuffmanTables,
this.fastACTables,
selectorsCount,
this.resetInterval,
spectralStart,

3
src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs

@ -20,9 +20,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowBadQuantizationTable() => throw new ImageFormatException("Bad Quantization Table index.");
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowBadHuffmanCode() => throw new ImageFormatException("Bad Huffman code.");
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowBadSampling() => throw new ImageFormatException("Bad sampling factor.");

115
src/ImageSharp/IO/DoubleBufferedStreamReader.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
@ -13,24 +14,28 @@ namespace SixLabors.ImageSharp.IO
/// 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 class DoubleBufferedStreamReader : IDisposable
internal sealed unsafe class DoubleBufferedStreamReader : IDisposable
{
/// <summary>
/// The length, in bytes, of the buffering chunk.
/// </summary>
public const int ChunkLength = 4096;
public const int ChunkLength = 8192;
private const int ChunkLengthMinusOne = ChunkLength - 1;
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 bytesRead;
private int chunkIndex;
private int position;
@ -44,8 +49,11 @@ namespace SixLabors.ImageSharp.IO
this.stream = stream;
this.Position = (int)stream.Position;
this.length = (int)stream.Length;
this.managedBuffer = memoryAllocator.AllocateManagedByteBuffer(ChunkLength, AllocationOptions.Clean);
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>
@ -62,10 +70,20 @@ namespace SixLabors.ImageSharp.IO
set
{
// Reset everything. It's easier than tracking.
this.position = (int)value;
this.stream.Seek(this.position, SeekOrigin.Begin);
this.bytesRead = ChunkLength;
// 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;
}
}
}
@ -74,7 +92,7 @@ namespace SixLabors.ImageSharp.IO
/// 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(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public int ReadByte()
{
if (this.position >= this.length)
@ -82,24 +100,21 @@ namespace SixLabors.ImageSharp.IO
return -1;
}
if (this.position == 0 || this.bytesRead > ChunkLengthMinusOne)
if (this.chunkIndex > MaxChunkIndex)
{
return this.ReadByteSlow();
this.FillChunk();
}
this.position++;
return this.bufferChunk[this.bytesRead++];
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(MethodImplOptions.AggressiveInlining)]
public void Skip(int count)
{
this.Position += count;
}
/// <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
@ -120,36 +135,46 @@ namespace SixLabors.ImageSharp.IO
/// 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(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public int Read(byte[] buffer, int offset, int count)
{
if (buffer.Length > ChunkLength)
if (count > ChunkLength)
{
return this.ReadToBufferSlow(buffer, offset, count);
}
if (this.position == 0 || count + this.bytesRead > ChunkLength)
if (count + this.chunkIndex > ChunkLength)
{
return this.ReadToChunkSlow(buffer, offset, count);
}
int n = this.GetCount(count);
int n = this.GetCopyCount(count);
this.CopyBytes(buffer, offset, n);
this.position += n;
this.bytesRead += n;
this.chunkIndex += n;
return n;
}
/// <inheritdoc/>
public void Dispose()
{
this.handle.Dispose();
this.managedBuffer?.Dispose();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private int ReadByteSlow()
[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)
{
@ -157,34 +182,25 @@ namespace SixLabors.ImageSharp.IO
}
this.stream.Read(this.bufferChunk, 0, ChunkLength);
this.bytesRead = 0;
this.position++;
return this.bufferChunk[this.bytesRead++];
this.chunkIndex = 0;
}
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(InliningOptions.ColdPath)]
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.FillChunk();
this.stream.Read(this.bufferChunk, 0, ChunkLength);
this.bytesRead = 0;
int n = this.GetCount(count);
int n = this.GetCopyCount(count);
this.CopyBytes(buffer, offset, n);
this.position += n;
this.bytesRead += n;
this.chunkIndex += n;
return n;
}
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(InliningOptions.ColdPath)]
private int ReadToBufferSlow(byte[] buffer, int offset, int count)
{
// Read to target but don't copy to our chunk.
@ -195,11 +211,12 @@ namespace SixLabors.ImageSharp.IO
int n = this.stream.Read(buffer, offset, count);
this.Position += n;
return n;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetCount(int count)
[MethodImpl(InliningOptions.ShortMethod)]
private int GetCopyCount(int count)
{
int n = this.length - this.position;
if (n > count)
@ -215,23 +232,23 @@ namespace SixLabors.ImageSharp.IO
return n;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyBytes(byte[] buffer, int offset, int count)
{
if (count < 9)
{
int byteCount = count;
int read = this.bytesRead;
byte[] chunk = this.bufferChunk;
int read = this.chunkIndex;
byte* pinned = this.pinnedChunk;
while (--byteCount > -1)
{
buffer[offset + byteCount] = chunk[read + byteCount];
buffer[offset + byteCount] = pinned[read + byteCount];
}
}
else
{
Buffer.BlockCopy(this.bufferChunk, this.bytesRead, buffer, offset, count);
Buffer.BlockCopy(this.bufferChunk, this.chunkIndex, buffer, offset, count);
}
}
}

4
src/ImageSharp/ImageSharp.csproj

@ -38,8 +38,8 @@
<ItemGroup>
<AdditionalFiles Include="..\..\standards\stylecop.json" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61" PrivateAssets="All" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0007" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-rc.114" PrivateAssets="All" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-dev000101" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
</ItemGroup>

17
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs

@ -47,5 +47,22 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
decoder.Dispose();
}
}
// RESULTS (2019 April 23):
//
// BenchmarkDotNet=v0.11.3, 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
//
// | Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |---------------------------- |----- |-------- |--------------------- |---------:|---------:|----------:|------:|--------:|---------:|------:|------:|----------:|
// | 'System.Drawing FULL' | Clr | Clr | Jpg/b(...)f.jpg [28] | 18.69 ms | 8.273 ms | 0.4535 ms | 1.00 | 0.00 | 343.7500 | - | - | 757.89 KB |
// | JpegDecoderCore.ParseStream | Clr | Clr | Jpg/b(...)f.jpg [28] | 15.76 ms | 4.266 ms | 0.2339 ms | 0.84 | 0.03 | - | - | - | 11.83 KB |
// | | | | | | | | | | | | | |
// | 'System.Drawing FULL' | Core | Core | Jpg/b(...)f.jpg [28] | 17.68 ms | 2.711 ms | 0.1486 ms | 1.00 | 0.00 | 343.7500 | - | - | 757.04 KB |
// | JpegDecoderCore.ParseStream | Core | Core | Jpg/b(...)f.jpg [28] | 14.27 ms | 3.671 ms | 0.2012 ms | 0.81 | 0.00 | - | - | - | 11.76 KB |
}
}

25
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg_ImageSpecific.cs

@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
public ShortClr()
{
this.Add(
//Job.Clr.WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3),
// Job.Clr.WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3),
Job.Core.WithLaunchCount(1).WithWarmupCount(2).WithIterationCount(3)
);
}
@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
private byte[] jpegBytes;
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage);
[Params(
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr,
TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr,
@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
using (var memoryStream = new MemoryStream(this.jpegBytes))
{
using (var image = Image.Load<Rgba32>(memoryStream, new JpegDecoder(){ IgnoreMetadata = true}))
using (var image = Image.Load<Rgba32>(memoryStream, new JpegDecoder() { IgnoreMetadata = true }))
{
return new CoreSize(image.Width, image.Height);
}
@ -115,5 +115,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
// | | | | | | | | | | |
// 'Decode Jpeg - System.Drawing' | Jpg/issues/issue750-exif-tranform.jpg | 95.192 ms | 3.1762 ms | 0.1795 ms | 1.00 | 0.00 | 1750.0000 | - | - | 5492.63 KB |
// 'Decode Jpeg - ImageSharp' | Jpg/issues/issue750-exif-tranform.jpg | 230.158 ms | 48.8128 ms | 2.7580 ms | 2.42 | 0.02 | 312.5000 | 312.5000 | 312.5000 | 58834.66 KB |
// RESULTS (2019 April 23):
//
//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
// Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
//
//| Method | TestImage | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
//|------------------------------- |--------------------- |-----------:|-----------:|-----------:|------:|--------:|----------:|------:|------:|------------:|
//| 'Decode Jpeg - System.Drawing' | Jpg/b(...)e.jpg [21] | 6.957 ms | 9.618 ms | 0.5272 ms | 1.00 | 0.00 | 93.7500 | - | - | 205.83 KB |
//| 'Decode Jpeg - ImageSharp' | Jpg/b(...)e.jpg [21] | 18.348 ms | 8.876 ms | 0.4865 ms | 2.65 | 0.23 | - | - | - | 14.49 KB |
//| | | | | | | | | | | |
//| 'Decode Jpeg - System.Drawing' | Jpg/b(...)f.jpg [28] | 18.687 ms | 11.632 ms | 0.6376 ms | 1.00 | 0.00 | 343.7500 | - | - | 757.04 KB |
//| 'Decode Jpeg - ImageSharp' | Jpg/b(...)f.jpg [28] | 41.990 ms | 25.514 ms | 1.3985 ms | 2.25 | 0.10 | - | - | - | 15.48 KB |
//| | | | | | | | | | | |
//| 'Decode Jpeg - System.Drawing' | Jpg/i(...)e.jpg [43] | 477.265 ms | 732.126 ms | 40.1303 ms | 1.00 | 0.00 | 3000.0000 | - | - | 7403.76 KB |
//| 'Decode Jpeg - ImageSharp' | Jpg/i(...)e.jpg [43] | 348.545 ms | 91.480 ms | 5.0143 ms | 0.73 | 0.06 | - | - | - | 35177.21 KB |
}
}

42
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs

@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
private MemoryStream stream2;
private MemoryStream stream3;
private MemoryStream stream4;
DoubleBufferedStreamReader reader1;
DoubleBufferedStreamReader reader2;
private DoubleBufferedStreamReader reader1;
private DoubleBufferedStreamReader reader2;
[GlobalSetup]
public void CreateStreams()
@ -102,6 +102,19 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
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()
{
byte[] buffer = new byte[DoubleBufferedStreamReader.ChunkLength * 3];
@ -111,4 +124,29 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
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 | - | - | - | - |
}

2
tests/ImageSharp.Benchmarks/Codecs/MultiImageBenchmarkBase.cs

@ -254,7 +254,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
protected void ForEachSystemDrawingImage(Func<System.Drawing.Bitmap, MemoryStream, object> operation)
{
using (MemoryStream workStream = new MemoryStream())
using (var workStream = new MemoryStream())
{
this.ForEachSystemDrawingImage(

2
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net472</TargetFrameworks>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<RootNamespace>SixLabors.ImageSharp.Benchmarks</RootNamespace>

4
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -27,8 +27,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.9.2" />
<PackageReference Include="System.Drawing.Common" Version="4.5.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.12.0" />
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />

Loading…
Cancel
Save