Nén chuỗi trong C#

Sự phát triển của các phần cứng hiện đại giúp tăng sức mạnh xử lý, băng thông mạng và không gian đĩa. Nhờ điều này, chúng ta có nên tận dụng tối đa những nguồn lực này mà không cần suy nghĩ? Ồ không! Chúng ta nên lưu ý đến việc sử dụng tài nguyên của mình và cách nó ảnh hưởng đến việc chạy ứng dụng tổng thể của chúng ta.

Bài đăng này sẽ chỉ ra cách chúng ta có thể sử dụng các thuật toán nén trong namespace System.IO.Compression để nén và giải nén một giá trị string. Nén các giá trị sẽ dẫn đến giảm đáng kể số lượng byte.

Nén là gì?

Nén trong vật lý là sự giảm kích thước bằng cách dùng các lực đè lên một vật. Về mặt dữ liệu, nén sẽ chuyển đổi dữ liệu sang một định dạng nhỏ hơn mà không làm mất thông tin có thể nhận thấy được.

Nén dữ liệu sử dụng các thuật toán để mã hóa thông tin hiện có thành những bit nhỏ nhất có thể. Các thuật toán khác nhau có mức độ hiệu quả khác nhau nhưng thường có sự đánh đổi về thời gian nén hoặc yêu cầu xử lý CPU để đạt được kết quả mong muốn. Trong khoa học máy tính, đây là sự đánh đổi giữa sự phức tạp không-thời gian .

Các nhà phát triển nên đánh giá các yếu tố sau khi chọn một thuật toán nén dữ liệu:

  • Thời gian: Mất bao lâu để nén dữ liệu của tôi?
  • Dung lượng: Tôi tiết kiệm được bao nhiêu dung lượng khi nén dữ liệu?
  • Chất lượng: Nén có làm mất dữ liệu không? Thông thường có thể chấp nhận được đối với âm thanh và video có mức độ mất thông tin.

Các thuật toán nén dữ liệu có sẵn cho các nhà phát triển .NET là gì?

Thuật toán nén dữ liệu trong .NET

Khi sử dụng .NET 5, các nhà phát triển có quyền truy cập vào namespace System.IO.Compression có hai thuật toán GZip Brotli để nén dữ liệu.

Gzip là một thuật toán nén dữ liệu không mất dữ liệu. Thuật toán bao gồm kiểm tra dự phòng để phát hiện hỏng dữ liệu. Người dùng Linux có thể đã quen thuộc với tiện ích mở rộng .gz này, vì nó thường được sử dụng trong Unix.

Người sáng tạo đã tối ưu hóa Gzip cho dữ liệu chưa được nén. Do đó, việc nén dữ liệu đã được nén bằng Gzip có thể tăng kích thước so với kích thước đã nén ban đầu.

Brotli là một thuật toán nén dữ liệu không mất dữ liệu khác được phát triển tại Google và phù hợp nhất để nén văn bản. Như bạn có thể đã đoán, Brotli lý tưởng để phân phối nội dung web, chủ yếu hoạt động trên HTML, JavaScript và CSS.

Brotli được coi là người kế nhiệm của Gzip và hầu hết các trình duyệt web lớn đều hỗ trợ nó. Nó cũng cung cấp khả năng nén dữ liệu tốt hơn nhiều so với người tiền nhiệm của nó, Gzip.

Sử dụng tính năng nén trong C#

May mắn thay, các nhà phát triển .NET có quyền truy cập vào cả hai thuật toán nén dữ liệu được đề cập ở trên dưới dạng GZipStream BrotliStream. Cả hai lớp đều có API và đầu vào giống hệt nhau.

var value = "hello world";
var level = CompressionLevel.Fastest;

var bytes = Encoding.Unicode.GetBytes(value);
await using var input = new MemoryStream(bytes);
await using var output = new MemoryStream();

// GZipStream with BrotliStream
await using var stream = new GZipStream(output, level);

await input.CopyToAsync(stream);

var result = output.ToArray();
var resultString = Convert.ToBase64String(result);

Chúng ta cũng có thể tạo các phương thức mở rộng để làm cho các thuật toán nén này dễ sử dụng hơn trong codebase của chúng ta.

public static class Compression
{
    public static async Task<CompressionResult> ToGzipAsync(this string value, CompressionLevel level = CompressionLevel.Fastest)
    {
        var bytes = Encoding.Unicode.GetBytes(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = new GZipStream(output, level);

        await input.CopyToAsync(stream);
        
        var result = output.ToArray();

        return new CompressionResult(
            new CompressionValue(value, bytes.Length),
            new CompressionValue(Convert.ToBase64String(result), result.Length),
            level,
            "Gzip");
    }
    
    public static async Task<CompressionResult> ToBrotliAsync(this string value, CompressionLevel level = CompressionLevel.Fastest)
    {
        var bytes = Encoding.Unicode.GetBytes(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = new BrotliStream(output, level);

        await input.CopyToAsync(stream);
        await stream.FlushAsync();
        
        var result = output.ToArray();

        return new CompressionResult(
            new CompressionValue(value, bytes.Length),
            new CompressionValue(Convert.ToBase64String(result), result.Length),
            level,
            "Brotli"
        );
    }

    public static async Task<string> FromGzipAsync(this string value)
    {
        var bytes = Convert.FromBase64String(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = new GZipStream(input, CompressionMode.Decompress);

        await stream.CopyToAsync(output);
        await stream.FlushAsync();
        
        return Encoding.Unicode.GetString(output.ToArray());
    }

    public static async Task<string> FromBrotliAsync(this string value)
    {
        var bytes = Convert.FromBase64String(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = new BrotliStream(input, CompressionMode.Decompress);

        await stream.CopyToAsync(output);
        
        return Encoding.Unicode.GetString(output.ToArray());
    }
}

public record CompressionResult(
    CompressionValue Original,
    CompressionValue Result,
    CompressionLevel Level,
    string Kind
)
{
    public int Difference =>
        Original.Size - Result.Size;

    public decimal Percent =>
      Math.Abs(Difference / (decimal) Original.Size);
}

public record CompressionValue(
    string Value,
    int Size
);

Bây giờ chúng ta có thể sử dụng chúng để nén bất kỳ chuỗi nào.

var comedyOfErrors = await File.ReadAllTextAsync("the-comedy-of-errors.txt");

var compressions = new[]
{
    await comedyOfErrors.ToGzipAsync(),
    await comedyOfErrors.ToBrotliAsync()
};

var table = new Table()
    .MarkdownBorder()
    .Title("compression in bytes")
    .ShowHeaders()
    .AddColumns("kind", "level", "before", "after", "difference", "% reduction");

foreach (var result in compressions)
{
    table
        .AddRow(
            result.Kind,
            result.Level.ToString(),
            result.Original.Size.ToString("N0"),
            result.Result.Size.ToString("N0"),
            result.Difference.ToString("N0"),
            result.Percent.ToString("P")
        );
}

AnsiConsole.Render(table);
Loại Xếp loại Trước Sau Giảm Tỷ lệ nén
Gzip Fastest 186,500 30,310 156,190 83.75 %
Brotli Fastest 186,500 49,424 137,076 73.50 %

Trong ví dụ này, tôi tải vở kịch The Comedy of Errors của Shakespeare từ một tệp văn bản và nén nó. Điều thú vị là nén Gzip tốt hơn Brotli trong trường hợp này.

Sử dụng BrotliEncoder

Namespace System.IO.Compression cũng có một lớp BrotliEncoder mà chúng ta có thể sử dụng để nén các chuỗi. Để sử dụng nó một cách hiệu quả, chúng ta sẽ cần tham chiếu đến gói nuget System.Memory. Gói bổ sung cho phép chúng tôi dịch các mảng hiện có thành các kiểu Span rõ ràng hoặc ngầm định.

// compression
var source = Encoding.Unicode.GetBytes(comedyOfErrors);
var memory = new byte[source.Length];
var encoded = BrotliEncoder.TryCompress(
    source,
    memory,
    out var encodedBytes
);

Console.WriteLine($"compress bytes: {encodedBytes}");

// decompression
var target = new byte[memory.Length];
BrotliDecoder.TryDecompress(memory, target, out var decodedBytes);
Console.WriteLine($"decompress bytes: {decodedBytes}");

var value = Encoding.Unicode.GetString(target);

Điều thú vị là, khi sử dụng BrotliEncoder, chúng tôi nhận được kết quả là 33,090 byte, hiệu quả hơn so với sử dụng BrotliStream trực tiếp có kích thước là 49,424 byte.

Cập nhật và sửa lỗi

Một thành viên trong cộng đồng nhận thấy rằng tôi đã không đạt được mức nén tối ưu nhất bằng thuật toán nén Brotli của mình. Theo cách nói của bạn: “Khi CompressionLevel.Optimal đang được sử dụng, luồng nén đích nên được flush trước khi cố gắng trích xuất các byte từ luồng bên dưới.”

private static async Task<CompressionResult> ToCompressedStringAsync(
    string value,
    CompressionLevel level,
    string algorithm,
    Func<Stream, Stream> createCompressionStream)
{
    var bytes = Encoding.Unicode.GetBytes(value);
    await using var input = new MemoryStream(bytes);
    await using var output = new MemoryStream();
    await using var stream = createCompressionStream(output);

    await input.CopyToAsync(stream);
    
    // calling to flush the stream first to get optimal
    // compression results
    await output.FlushAsync();
    var result = output.ToArray();

    return new CompressionResult(
        new(value, bytes.Length),
        new(Convert.ToBase64String(result), result.Length),
        level,
        algorithm);
}

Với mẫu mã cập nhật đang được cập nhật như sau:

public static class Compression
{
    private static async Task<CompressionResult> ToCompressedStringAsync(
    string value,
    CompressionLevel level,
    string algorithm,
    Func<Stream, Stream> createCompressionStream)
    {
        var bytes = Encoding.Unicode.GetBytes(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = createCompressionStream(output);
        
        await input.CopyToAsync(stream);
        await stream.FlushAsync();

        var result = output.ToArray();

        return new CompressionResult(
            new(value, bytes.Length),
            new(Convert.ToBase64String(result), result.Length),
            level,
            algorithm);
    }

    public static async Task<CompressionResult> ToGzipAsync(this string value, CompressionLevel level = CompressionLevel.Fastest)
        => await ToCompressedStringAsync(value, level, "GZip", s => new GZipStream(s, level));
    
    public static async Task<CompressionResult> ToBrotliAsync(this string value, CompressionLevel level = CompressionLevel.Fastest)
        => await ToCompressedStringAsync(value, level, "Brotli", s => new BrotliStream(s, level));

    private static async Task<string> FromCompressedStringAsync(string value, Func<Stream, Stream> createDecompressionStream)
    {
        var bytes = Convert.FromBase64String(value);
        await using var input = new MemoryStream(bytes);
        await using var output = new MemoryStream();
        await using var stream = createDecompressionStream(input);

        await stream.CopyToAsync(output);
        await output.FlushAsync();            

        return Encoding.Unicode.GetString(output.ToArray());
    }

    public static async Task<string> FromGzipAsync(this string value)
        => await FromCompressedStringAsync(value, s => new GZipStream(s, CompressionMode.Decompress));

    public static async Task<string> FromBrotliAsync(this string value)
        => await FromCompressedStringAsync(value, s => new BrotliStream(s, CompressionMode.Decompress));
}

public record CompressionResult(
    CompressionValue Original,
    CompressionValue Result,
    CompressionLevel Level,
    string Kind
)
{
    public int Difference =>
        Original.Size - Result.Size;

    public decimal Percent =>
      Math.Abs(Difference / (decimal) Original.Size);
}

public record CompressionValue(
    string Value,
    int Size
);

Kết quả sau khi thay đổi mã phản ánh đầu ra chính xác hơn.

┌────────┬─────────┬─────────┬────────┬────────────┬─────────────┐
│ kind   │ level   │ before  │ after  │ difference │ % reduction │
├────────┼─────────┼─────────┼────────┼────────────┼─────────────┤
│ GZip   │ Fastest │ 180,098 │ 52,272 │ 127,826    │ 70.976%     │
│ GZip   │ Optimal │ 180,098 │ 41,175 │ 138,923    │ 77.137%     │
│ Brotli │ Fastest │ 180,098 │ 48,408 │ 131,690    │ 73.121%     │
│ Brotli │ Optimal │ 180,098 │ 32,833 │ 147,265    │ 81.769%     │
└────────┴─────────┴─────────┴────────┴────────────┴─────────────┘
compress bytes: 32832
decompress bytes: 180098

Tuy nhiên, rất ấn tượng khi thấy tỷ lệ nén trên 70%.

Phần kết luận

Nén dữ liệu là một phần không thể thiếu trong phát triển phần mềm hiện đại; trong hầu hết các trường hợp, nén là một tính năng cấp thấp của máy chủ web hoặc framework.

Chúng ta chỉ cần kích hoạt nó và nhận được lợi ích của tải trọng nhỏ hơn và giảm sử dụng băng thông. Thật tuyệt vời khi biết rằng chúng ta có thể tận dụng namespace System.IO.Compression để nén bất kỳ dữ liệu nào mà chúng ta chọn theo cách thủ công. Và như mọi khi, hãy đảm bảo flush luồng của bạn.

Tôi hy vọng bạn thấy bài viết này hữu ích và cảm ơn bạn đã đọc.

Lập Trình C#.NET Framework
Bài Viết Liên Quan:
Loạt bài: Khám phá .NET 6
Trung Nguyen 02/04/2022
Loạt bài: Khám phá .NET 6

Trong loạt bài này, tôi sẽ xem xét một số

Các thành viên static abstract trong interface C# 10
Trung Nguyen 20/02/2022
Các thành viên static abstract trong interface C# 10

Ngôn ngữ C# đã bật các bộ tăng áp liên

Tạo tập tin Zip với .NET 5
Trung Nguyen 11/11/2021
Tạo tập tin Zip với .NET 5

Trong bài viết này, chúng ta sẽ tìm hiểu lớp tiện ích ZipFile trong C#, cách nén tập tin và thư mục, cùng với giải nén tập tin zip.

Đọc và ghi file Excel trong C#
Trung Nguyen 29/10/2021
Đọc và ghi file Excel trong C#

Bài viết này sẽ giới thiệu cách đơn giản nhất mà tôi đã tìm thấy để đọc và ghi file Excel bằng C# sử dụng ExcelMapper.