Brotli в .NET
В .NET Core 2.1 Microsoft добавила поддержку Brotli-компрессии, предоставив разработчикам привычный контракт на базе Stream.
Идея проста: вы оборачиваете поток с несжатыми или сжатыми данными в BrotliStream и читаете или пишете данные, как обычно.
Пример: компрессия в файл
using var input = File.OpenRead("input.bin");
using var output = File.Create("output.br");
using var brotli = new BrotliStream(output, CompressionLevel.Optimal);
input.CopyTo(brotli); // пошла рудаЭто действительно удобный способ работы с файлами. Однако на практике он хорошо подходит только для файлов.
Если данные приходят из сети или уже находятся в памяти, вам придётся создавать MemoryStream, выполнять компрессию или декомпрессию через него, а затем извлекать результат.
Пример: компрессия в MemoryStream
byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var brotli = new BrotliStream(output, CompressionLevel.Optimal, true))
{
brotli.Write(data, 0, data.Length);
}
return output.ToArray();
}
Если использовать MemoryStream неаккуратно, количество аллокаций может оказаться неоправданно большим для такой простой операции, как компрессия или декомпрессия нескольких сотен байт. Особенно критично это на горячем пути сетевого приложения.
Где именно происходят аллокации
byte[] Compress(byte[] data)
{
// аллокация массива для MemoryStream
using var output = new MemoryStream();
// аллокация BrotliStream + внутренние буферы
using (var brotli = new BrotliStream(output, CompressionLevel.Optimal, true))
{
// возможные промежуточные аллокации при Write
brotli.Write(data, 0, data.Length);
}
// аллокация нового массива под результат
return output.ToArray();
}BrotliEncoder / BrotliDecoder
Для таких сценариев разработчики Microsoft добавили низкоуровневые классы BrotliEncoder и BrotliDecoder. Они работают напрямую со Span<T> и ReadOnlySpan<T> и, по крайней мере теоретически, не создают мусор для GC.
Их интерфейс следует неформализованному, но уже широко используемому паттерну, встречающемуся в System.Text.Encoding, Base64, BrotliEncoder и других API:
public OperationStatus Operation(
ReadOnlySpan<byte> source,
Span<byte> destination,
out int bytesConsumed,
out int bytesWritten,
bool isFinalBlock);К слову, вы можете проголосовать за формализацию этого интерфейса в официальном API в соответствующем PR (ставь лайк/реакцию первому комменту):
https://github.com/dotnet/runtime/issues/122358
Скрытое Zlib API
Несмотря на то что для Brotli появилось высокопроизводительное API, для GZip (и Zlib) его официально добавлять не стали.
Или всё-таки стали?
На самом деле это API существует с .NET 1.0. Оно эффективное, производительное - и полностью приватное. Самое неприятное в этой истории то, что команды Microsoft сами копируют исходники этих классов в свои проекты, вместо того чтобы протестировать и опубликовать их.

История Zlib в .NET
.NET распространяется с Zlib с самых ранних версий. Однако код никогда не находился в zlib.dll. Вместо этого использовалась библиотека clrcompression.dll, в которой жила Zlib версии 1.2.3 - со всеми её плюсами и багами.
Контекст z_stream_s и сигнатуры методов полностью повторяли оригинальный API Zlib.
Так продолжалось до .NET 3.x, когда нативный код переехал в System.IO.Compression.Native.dll. При этом контекст и методы нативного API были слегка переработаны и стали version-agnostic относительно версии Zlib, с которой был собран рантайм.
Оригинальная clrcompression.dll была окончательно удалена из поставки в .NET 6.
Добавление Zlib API в проект
Несмотря на нежелание Microsoft публиковать высокопроизводительное API для GZip в виде ZlibEncoder/ZlibDecoder, ничто не мешает сделать это самостоятельно.
Варианты:
реализовать API с нуля;
скопировать внутреннюю реализацию Microsoft и адаптировать её под контракт
BrotliEncoder/BrotliDecoder.
Благо System.IO.Compression.Native.dll уже загружена в процесс, и вы можете вызывать её напрямую через P/Invoke.
Я уже проделал эту работу и протестировал результат:
https://gist.github.com/deniszykov/89197480c20f537ce0c6c2a809b688ae
Использование
Использование полностью повторяет модель BrotliEncoder: вы создаёте энкодер, подаёте ему входные данные и извлекаете результат, проверяя статус на каждой итерации.
using var encoder = new ZlibEncoder(
CompressionLevel.Optimal,
ZlibEncoder.GZIP_WINDOW_BITS);
var input = dataBytes.AsSpan();
var buffer = ArrayPool<byte>.Shared.Rent(4096);
var output = Stream.Null;
var options = TransformOptions.Starting | TransformOptions.Final;
OperationStatus status = OperationStatus.NeedMoreData;
while (!(input.IsEmpty && status == OperationStatus.Done))
{
status = encoder.Compress(
input,
buffer,
out int consumed,
out int written,
options);
if (status == OperationStatus.InvalidData)
throw new InvalidOperationException();
if (consumed > 0)
{
options &= ~TransformOptions.Starting;
input = input.Slice(consumed);
}
if (written > 0)
{
output.Write(buffer, 0, written);
}
}
ArrayPool<byte>.Shared.Return(buffer);Альтернатива от ICSharpCode.SharpZipLib
Опытный разработчик заметит, что не на всех платформах доступна нативная библиотека System.IO.Compression.Native - например, в Mono или Blazor WebAssembly.
Разработчики Microsoft подстраховались ещё в .NET Framework, реализовав Inflater и DeflaterManaged (нейминг мое почтение), но, разумеется, они тоже приватные.
В итоге остаётся два варианта:
реализовать managed-версию самостоятельно;
воспользоваться готовым портом, например
ICSharpCode.SharpZipLibилиIonic.Zlib
Пример адаптации под тот же контракт:
https://gist.github.com/deniszykov/5cfee5f1f770a9792a74e4b3a1e0db55
Производительность
Главный вопрос — имеет ли смысл использовать встроенный Zlib, если существует полностью managed-альтернатива, работающая на всех платформах.
Ответ — смотрите на бенчмарки и решайте сами:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---------------|----------:|----------:|----------:|--------:|----------:|
| BrotliEncoder | 9.502 ms | 0.1828 ms | 0.1956 ms | - | 47 B |
| ZlibEncoder | 42.318 ms | 0.6362 ms | 0.5951 ms | - | 137 B |
| GZipEncoder† | 47.537 ms | 0.9470 ms | 0.9301 ms | 90.9091 | 1678379 B |
| Method | Mean | Error | StdDev | Allocated |
|---------------|-----------:|--------:|---------:|----------:|
| BrotliDecoder | 475.4 us | 9.49 us | 27.53 us | 32 B |
| ZlibDecoder | 650.1 us | 9.54 us | 8.46 us | 81 B |
| GZipDecoder† | 3.634.0 us | 5.24 us | 4.38 us | 33011 B |
† полностью managed реализация через ICSharpCode.SharpZipLib
В бенчмарке сжимались и разжимались 2 МБ случайных данных с уровнем сжатия 6 и максимальным размером битового окна.
Пропускная способность на моём железе
BrotliEncoder: 218.38 MiB/s
ZlibEncoder: 51.50 MiB/s
GZipEncoder: 44.45 MiB/s
BrotliDecoder: 4259.95 MiB/s
ZlibDecoder: 3275.47 MiB/s
GZipDecoder: 581.00 MiB/sПишите в комментариях насколько вам не понравилось как я проводил бенчмарк и как бы вы его провели.