Skip to content

Commit

Permalink
Simplify stream decompress logic (#71)
Browse files Browse the repository at this point in the history
Motivation
----------
We can get some small gains in stream decompression while also making
the logic a bit simpler.

Modifications
-------------
- Drop the local var copies of the class fields, just update the fields
directly.
- Replace exceptions with throw helpers
- Optimize the short circuit when we're out of data
- Add an Overview benchmark to provide a release performance overview
- Add an additional grouping by method in VersionComparisonConfig so
that it works with Overview
- Make BlockCompressHtml agree with Overview by only compressing 64KB
  • Loading branch information
brantburnett authored Mar 6, 2023
1 parent 8398f22 commit 1080310
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Snappier.Benchmarks/BlockCompressHtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public void LoadToMemory()
using var resource =
typeof(BlockCompressHtml).Assembly.GetManifestResourceStream("Snappier.Benchmarks.TestData.html");

byte[] input = new byte[resource!.Length];
byte[] input = new byte[65536]; // Just test the first 64KB
int inputLength = resource!.Read(input, 0, input.Length);
_input = input.AsMemory(0, inputLength);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public IEnumerable<BenchmarkCase> GetSummaryOrder(ImmutableArray<BenchmarkCase>

public string GetLogicalGroupKey(ImmutableArray<BenchmarkCase> allBenchmarksCases,
BenchmarkCase benchmarkCase) =>
$"{benchmarkCase.Job.Environment.Runtime.MsBuildMoniker}-Pgo={(PgoColumn.IsPgo(benchmarkCase) ? "Y" : "N")}";
$"{benchmarkCase.Job.Environment.Runtime.MsBuildMoniker}-Pgo={(PgoColumn.IsPgo(benchmarkCase) ? "Y" : "N")}-{benchmarkCase.Descriptor.MethodIndex}";

public IEnumerable<IGrouping<string, BenchmarkCase>> GetLogicalGroupOrder(
IEnumerable<IGrouping<string, BenchmarkCase>> logicalGroups,
Expand Down
89 changes: 89 additions & 0 deletions Snappier.Benchmarks/Overview.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.IO;
using System.IO.Compression;
using BenchmarkDotNet.Attributes;
using Snappier.Internal;

namespace Snappier.Benchmarks
{
public class Overview
{
private MemoryStream _htmlStream;
private Memory<byte> _htmlMemory;

private ReadOnlyMemory<byte> _compressed;
private MemoryStream _compressedStream;

private Memory<byte> _outputBuffer;
private byte[] _streamOutputBuffer;
private MemoryStream _streamOutput;

[GlobalSetup]
public void LoadToMemory()
{
_htmlStream = new MemoryStream();
using var resource =
typeof(DecompressHtml).Assembly.GetManifestResourceStream("Snappier.Benchmarks.TestData.html");
resource!.CopyTo(_htmlStream);
_htmlStream.Position = 0;

byte[] input = new byte[65536]; // Just test the first 64KB
// ReSharper disable once PossibleNullReferenceException
int inputLength = _htmlStream.Read(input, 0, input.Length);
_htmlMemory = input.AsMemory(0, inputLength);

byte[] compressed = new byte[Snappy.GetMaxCompressedLength(inputLength)];
int compressedLength = Snappy.Compress(_htmlMemory.Span, compressed);

_compressed = compressed.AsMemory(0, compressedLength);

_compressedStream = new MemoryStream();
using var resource2 =
typeof(DecompressHtml).Assembly.GetManifestResourceStream("Snappier.Benchmarks.TestData.html_x_4.snappy");
// ReSharper disable once PossibleNullReferenceException
resource2.CopyTo(_compressedStream);

_outputBuffer = new byte[Snappy.GetMaxCompressedLength(inputLength)];
_streamOutputBuffer = new byte[16384];
_streamOutput = new MemoryStream();
}

[Benchmark]
public int BlockCompress64KbHtml()
{
using var compressor = new SnappyCompressor();

return compressor.Compress(_htmlMemory.Span, _outputBuffer.Span);
}

[Benchmark]
public void BlockDecompress64KbHtml()
{
using var decompressor = new SnappyDecompressor();

decompressor.Decompress(_compressed.Span);
}

[Benchmark]
public void StreamCompressHtml()
{
_htmlStream.Position = 0;
_streamOutput.Position = 0;
using var stream = new SnappyStream(_streamOutput, CompressionMode.Compress, true);

_htmlStream.CopyTo(stream, 16384);
stream.Flush();
}

[Benchmark]
public void StreamDecompressHtml()
{
_compressedStream.Position = 0;
using var stream = new SnappyStream(_compressedStream, CompressionMode.Decompress, true);

while (stream.Read(_streamOutputBuffer, 0, _streamOutputBuffer.Length) > 0)
{
}
}
}
}
83 changes: 39 additions & 44 deletions Snappier/Internal/SnappyStreamDecompressor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,16 @@ public int Decompress(Span<byte> buffer)
{
Debug.Assert(_decompressor != null);

Constants.ChunkType? chunkType = _chunkType;
int chunkSize = _chunkSize;
int chunkBytesProcessed = _chunkBytesProcessed;

ReadOnlySpan<byte> input = _input.Span;

// Cache this to use later to calculate the total bytes written
int originalBufferLength = buffer.Length;

while (buffer.Length > 0
&& (input.Length > 0 || (chunkType == Constants.ChunkType.CompressedData && _decompressor.AllDataDecompressed)))
&& (input.Length > 0 || (_chunkType == Constants.ChunkType.CompressedData && _decompressor.AllDataDecompressed)))
{
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (chunkType)
switch (_chunkType)
{
case null:
// Not in a chunk, read the chunk type and size
Expand All @@ -52,32 +48,32 @@ public int Decompress(Span<byte> buffer)
if (rawChunkHeader == 0)
{
// Not enough data, get some more
break;
goto exit;
}

chunkType = (Constants.ChunkType) (rawChunkHeader & 0xff);
chunkSize = unchecked((int)(rawChunkHeader >> 8));
chunkBytesProcessed = 0;
_chunkType = (Constants.ChunkType) (rawChunkHeader & 0xff);
_chunkSize = unchecked((int)(rawChunkHeader >> 8));
_chunkBytesProcessed = 0;
_scratchLength = 0;
_chunkCrc = 0;
break;

case Constants.ChunkType.CompressedData:
{
if (chunkBytesProcessed < 4)
if (_chunkBytesProcessed < 4)
{
_decompressor.Reset();

if (!ReadChunkCrc(ref input, ref chunkBytesProcessed))
if (!ReadChunkCrc(ref input))
{
// Incomplete CRC
break;
goto exit;
}

if (input.Length == 0)
{
// No more data
break;
goto exit;
}
}

Expand All @@ -88,15 +84,15 @@ public int Decompress(Span<byte> buffer)
if (input.Length == 0)
{
// No more data to give
break;
goto exit;
}

int availableChunkBytes = Math.Min(input.Length, chunkSize - chunkBytesProcessed);
int availableChunkBytes = Math.Min(input.Length, _chunkSize - _chunkBytesProcessed);
Debug.Assert(availableChunkBytes > 0);

_decompressor.Decompress(input.Slice(0, availableChunkBytes));

chunkBytesProcessed += availableChunkBytes;
_chunkBytesProcessed += availableChunkBytes;
input = input.Slice(availableChunkBytes);
}

Expand All @@ -110,12 +106,12 @@ public int Decompress(Span<byte> buffer)
if (_decompressor.EndOfFile)
{
// Completed reading the chunk
chunkType = null;
_chunkType = null;

uint crc = Crc32CAlgorithm.ApplyMask(_chunkCrc);
if (_expectedChunkCrc != crc)
{
throw new InvalidDataException("Chunk CRC mismatch.");
ThrowHelper.ThrowInvalidDataException("Chunk CRC mismatch.");
}
}

Expand All @@ -124,41 +120,41 @@ public int Decompress(Span<byte> buffer)

case Constants.ChunkType.UncompressedData:
{
if (chunkBytesProcessed < 4)
if (_chunkBytesProcessed < 4)
{
if (!ReadChunkCrc(ref input, ref chunkBytesProcessed))
if (!ReadChunkCrc(ref input))
{
// Incomplete CRC
break;
goto exit;
}

if (input.Length == 0)
{
// No more data
break;
goto exit;
}
}

int chunkBytes = unchecked(Math.Min(Math.Min(buffer.Length, input.Length),
chunkSize - chunkBytesProcessed));
_chunkSize - _chunkBytesProcessed));

input.Slice(0, chunkBytes).CopyTo(buffer);

_chunkCrc = Crc32CAlgorithm.Append(_chunkCrc, buffer.Slice(0, chunkBytes));

buffer = buffer.Slice(chunkBytes);
input = input.Slice(chunkBytes);
chunkBytesProcessed += chunkBytes;
_chunkBytesProcessed += chunkBytes;

if (chunkBytesProcessed >= chunkSize)
if (_chunkBytesProcessed >= _chunkSize)
{
// Completed reading the chunk
chunkType = null;
_chunkType = null;

uint crc = Crc32CAlgorithm.ApplyMask(_chunkCrc);
if (_expectedChunkCrc != crc)
{
throw new InvalidDataException("Chunk CRC mismatch.");
ThrowHelper.ThrowInvalidDataException("Chunk CRC mismatch.");
}
}

Expand All @@ -167,31 +163,30 @@ public int Decompress(Span<byte> buffer)

default:
{
if (chunkType < Constants.ChunkType.SkippableChunk)
if (_chunkType < Constants.ChunkType.SkippableChunk)
{
throw new InvalidDataException($"Unknown chunk type {(int) chunkType:x}");
ThrowHelper.ThrowInvalidDataException($"Unknown chunk type {(int) _chunkType:x}");
}

int chunkBytes = Math.Min(input.Length, chunkSize - chunkBytesProcessed);
int chunkBytes = Math.Min(input.Length, _chunkSize - _chunkBytesProcessed);

input = input.Slice(chunkBytes);
chunkBytesProcessed += chunkBytes;
_chunkBytesProcessed += chunkBytes;

if (chunkBytesProcessed >= chunkSize)
if (_chunkBytesProcessed >= _chunkSize)
{
// Completed reading the chunk
chunkType = null;
_chunkType = null;
}

break;
}
}
}

_chunkType = chunkType;
_chunkSize = chunkSize;
_chunkBytesProcessed = chunkBytesProcessed;

// We use a label and goto exit to avoid an unnecessary comparison on the while loop clause before
// exiting the loop in cases where we know we're done processing data.
exit:
_input = _input.Slice(_input.Length - input.Length);
return originalBufferLength - buffer.Length;
}
Expand Down Expand Up @@ -248,26 +243,26 @@ private uint ReadChunkHeader(ref ReadOnlySpan<byte> buffer)
/// _scratch for subsequent reads. Should not be called if chunkByteProcessed >= 4.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ReadChunkCrc(ref ReadOnlySpan<byte> input, ref int chunkBytesProcessed)
private bool ReadChunkCrc(ref ReadOnlySpan<byte> input)
{
Debug.Assert(chunkBytesProcessed < 4);
Debug.Assert(_chunkBytesProcessed < 4);

if (chunkBytesProcessed == 0 && input.Length >= 4)
if (_chunkBytesProcessed == 0 && input.Length >= 4)
{
// Common fast path

_expectedChunkCrc = BinaryPrimitives.ReadUInt32LittleEndian(input);
input = input.Slice(4);
chunkBytesProcessed += 4;
_chunkBytesProcessed += 4;
return true;
}

// Copy to scratch
int crcBytesAvailable = Math.Min(input.Length, 4 - chunkBytesProcessed);
int crcBytesAvailable = Math.Min(input.Length, 4 - _chunkBytesProcessed);
input.Slice(0, crcBytesAvailable).CopyTo(_scratch.AsSpan(_scratchLength));
_scratchLength += crcBytesAvailable;
input = input.Slice(crcBytesAvailable);
chunkBytesProcessed += crcBytesAvailable;
_chunkBytesProcessed += crcBytesAvailable;

if (_scratchLength >= 4)
{
Expand Down

0 comments on commit 1080310

Please sign in to comment.