3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【.NET 9.0-rc.1】Brotli 圧縮形式がサポートされました

Posted at

はじめに

.NET 9.0-rc.1 が公開され、Brotli 圧縮形式がサポートされました。

Brotli https://ja.wikipedia.org/wiki/Brotli は比較的最近(2017 年)に登場した圧縮形式です。

  • GZip より 19% 圧縮率が高い(ファイルサイズが小さくなる)
  • GZip より 483% 圧縮時間が長い(圧縮に時間がかかる)
  • GZip より 17% 解凍時間が長い(解凍に時間がかかる)

数値はご参考程度の触れ込みです。圧縮時間がとても長い性質があるため、主な用途は web リソースのように読み取りメインのリソースになりそうです。もともとの開発経緯も web フォントの圧縮を目的にしてのことだそうです。

使い方

using System.IO.Compression;

ReadOnlySpan<byte> want = "祇園精舍の鐘の声、諸行無常の響きあり。娑羅双樹の花の色、盛者必衰の理をあらはす。驕れる人も久しからず、ただ春の夜の夢のごとし。猛き者もつひにはほろびぬ、ひとへに風の前の塵に同じ。"u8;
var memory = new MemoryStream();
using (var brotli = new BrotliStream(memory, CompressionMode.Compress, true))
{
    brotli.Write(want);
}

Assert.Equal(267, want.Length);
Assert.Equal(188, memory.Length); // 圧縮率 70.4%

memory.Position = 0;
using (var brotli = new BrotliStream(memory, CompressionMode.Decompress, true))
{
    var got = (stackalloc byte[want.Length]);
    _ = brotli.Read(got);
    Assert.Equal(want, got);
}

パフォーマンス比較

例のページ https://www.example.com/ を utf8 形式のテキストで圧縮してみました。
圧縮レベルが4種類存在し、それぞれを見てみます。

圧縮サイズの比較(もともとのサイズ: 1300 bytes)

圧縮方法 Brotli 圧縮率 GZip 圧縮率
SmallestSize(最小) 445 34.2% 650 50.0%
Optimal(最適) 574 44.2% 658 50.6%
Fastest(最速) 706 54.3% 837 64.3%
NoCompression(未圧縮) 767 59.0% 1323 101.8%

圧縮のスコアの比較

Test Score % CG0
BrotliCompressionTime (4)
SmallestSize 52 100.0% 0
Optimal 510 980.8% 0
Fastest 11,314 21,757.7% 0
NoCompression 11,929 22,940.4% 0
GZipCompressionTime (4)
SmallestSize 1,908 100.0% 0
Optimal 3,348 175.5% 0
Fastest 7,274 381.2% 0
NoCompression 14,000 733.8% 0

解凍のスコアの比較

Test Score % CG0
BrotliDecompressionTime (4)
SmallestSize 9,377 100.0% 0
Optimal 9,414 100.4% 0
Fastest 11,214 119.6% 0
NoCompression 10,771 114.9% 0
GZipDecompressionTime (4)
SmallestSize 7,606 100.0% 0
Optimal 8,663 113.9% 0
Fastest 8,508 111.9% 0
NoCompression 16,106 211.8% 0

実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。

  • Brotli は GZip と比較して圧縮ファイルのサイズが少し小さくなる
  • Brotli は GZip と比較して圧縮がとても遅い
  • Brotli は GZip と比較して解凍が少し早い
  • Brotli は圧縮レベルによって圧縮にかかる時間が大きく変わる

おわりに

Brotli は圧縮がとても遅いため用途は限定されますが、より小さなファイルサイズを実現します。
複数ファイルをまとめたりフォルダ階層を持ったりはできないため、Zip 圧縮に置き換わるものではありません。
C# で見た場合 System.IO.Stream を継承するクラスで実装されているため、既存のコードにも馴染みやすそうです。

テストコード
using System.IO.Compression;
using Xunit;

public class _BrotliPerformanceTest
{
    static ReadOnlySpan<byte> ExampleText =>
"""
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
        
    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>    
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
"""u8;

    [Fact]
    void HowToUse()
    {
        ReadOnlySpan<byte> want = "祇園精舍の鐘の声、諸行無常の響きあり。娑羅双樹の花の色、盛者必衰の理をあらはす。驕れる人も久しからず、ただ春の夜の夢のごとし。猛き者もつひにはほろびぬ、ひとへに風の前の塵に同じ。"u8;
        var memory = new MemoryStream();
        using (var brotli = new BrotliStream(memory, CompressionMode.Compress, true))
        {
            brotli.Write(want);
        }

        Assert.Equal(267, want.Length);
        Assert.Equal(188, memory.Length); // 圧縮率 70.4%

        memory.Position = 0;
        using (var brotli = new BrotliStream(memory, CompressionMode.Decompress, true))
        {
            var got = (stackalloc byte[want.Length]);
            _ = brotli.Read(got);
            Assert.Equal(want, got);
        }
    }

    [Fact]
    void HowToUseGZip()
    {
        ReadOnlySpan<byte> want = "祇園精舍の鐘の声、諸行無常の響きあり。娑羅双樹の花の色、盛者必衰の理をあらはす。驕れる人も久しからず、ただ春の夜の夢のごとし。猛き者もつひにはほろびぬ、ひとへに風の前の塵に同じ。"u8;
        var memory = new MemoryStream();
        using (var brotli = new GZipStream(memory, CompressionMode.Compress, true))
        {
            brotli.Write(want);
        }

        Assert.Equal(267, want.Length);
        Assert.Equal(212, memory.Length); // 圧縮率 79.4%

        memory.Position = 0;
        using (var brotli = new GZipStream(memory, CompressionMode.Decompress, true))
        {
            var got = (stackalloc byte[want.Length]);
            _ = brotli.Read(got);
            Assert.Equal(want, got);
        }
    }

    [Fact]
    void BrotliCompressionBytes()
    {
        Assert.Equal(1300, ExampleText.Length);
        ReadOnlySpan<(CompressionLevel level, int size)> tests =
        [
            (CompressionLevel.SmallestSize, 445),
            (CompressionLevel.Optimal, 574),
            (CompressionLevel.Fastest, 706),
            (CompressionLevel.NoCompression, 767),
        ];
        var memory = new MemoryStream();
        foreach (var (level, size) in tests)
        {
            memory.SetLength(0);
            memory.Position = 0;
            using (var brotli = new BrotliStream(memory, level, true))
            {
                brotli.Write(ExampleText);
            }
            Assert.Equal(size, memory.Length);
        }
    }

    [Fact]
    void GZipCompressionBytes()
    {
        Assert.Equal(1300, ExampleText.Length);

        ReadOnlySpan<(CompressionLevel level, int size)> tests =
        [
            (CompressionLevel.SmallestSize, 650),
            (CompressionLevel.Optimal, 658),
            (CompressionLevel.Fastest, 837),
            (CompressionLevel.NoCompression, 1323),
        ];
        var memory = new MemoryStream();
        foreach (var (level, size) in tests)
        {
            memory.SetLength(0);
            memory.Position = 0;
            using (var gzip = new GZipStream(memory, level, true))
            {
                gzip.Write(ExampleText);
            }
            Assert.Equal(size, memory.Length);
        }
    }

    static void BrotliCompressionTime(Performance p)
    {
        ReadOnlySpan<CompressionLevel> testCases = [
            CompressionLevel.SmallestSize,
            CompressionLevel.Optimal,
            CompressionLevel.Fastest,
            CompressionLevel.NoCompression
        ];
        var memory = new MemoryStream();
        foreach (var n in testCases)
        {
            p.AddTest(n.ToString(), () =>
            {
                memory.SetLength(0);
                memory.Position = 0;
                using (var brotli = new BrotliStream(memory, n, true))
                {
                    brotli.Write(ExampleText);
                }
            });
        }
    }

    static void GZipCompressionTime(Performance p)
    {
        ReadOnlySpan<CompressionLevel> testCases = [
            CompressionLevel.SmallestSize,
            CompressionLevel.Optimal,
            CompressionLevel.Fastest,
            CompressionLevel.NoCompression
        ];
        var memory = new MemoryStream();
        foreach (var n in testCases)
        {
            p.AddTest(n.ToString(), () =>
            {
                memory.SetLength(0);
                memory.Position = 0;
                using (var gzip = new GZipStream(memory, n, true))
                {
                    gzip.Write(ExampleText);
                }
            });
        }
    }

    static void BrotliDecompressionTime(Performance p)
    {
        ReadOnlySpan<CompressionLevel> testCases = [
            CompressionLevel.SmallestSize,
            CompressionLevel.Optimal,
            CompressionLevel.Fastest,
            CompressionLevel.NoCompression
        ];
        foreach (var n in testCases)
        {
            var compressed = new MemoryStream();
            using (var brotli = new BrotliStream(compressed, n, true))
            {
                brotli.Write(ExampleText);
            }

            p.AddTest(n.ToString(), () =>
            {
                compressed.Position = 0;
                using (var brotli = new BrotliStream(compressed, CompressionMode.Decompress, true))
                {
                    var buf = (stackalloc byte[16]);
                    while (brotli.Read(buf) > 0) ;
                }
            });
        }
    }

    static void GZipDecompressionTime(Performance p)
    {
        ReadOnlySpan<CompressionLevel> testCases = [
            CompressionLevel.SmallestSize,
            CompressionLevel.Optimal,
            CompressionLevel.Fastest,
            CompressionLevel.NoCompression
        ];
        var memory = new MemoryStream();
        foreach (var n in testCases)
        {
            var compressed = new MemoryStream();
            using (var gzip = new GZipStream(compressed, n, true))
            {
                gzip.Write(ExampleText);
            }

            p.AddTest(n.ToString(), () =>
            {
                compressed.Position = 0;
                using (var gzip = new GZipStream(compressed, CompressionMode.Decompress, true))
                {
                    var buf = (stackalloc byte[16]);
                    while (gzip.Read(buf) > 0) ;
                }
            });
        }
    }
}

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?