Почему GZip из ProtoBuf.NET больше, чем GZip файла значений, разделенных табуляцией? - PullRequest
1 голос
/ 03 ноября 2011

Недавно мы сравнили соответствующие размеры файлов одних и тех же табличных данных (например, одну таблицу, полдюжины столбцов, описывающих каталог продуктов), сериализованных с ProtoBuf.NET или с TSV (данные, разделенные табуляцией), оба файла сжаты с помощьюGZip позже (реализация по умолчанию .NET).

Я с удивлением заметил, что сжатая версия ProtoBuf.NET занимает намного больше места, чем текстовая версия (до 3 раз больше). Моя любимая теория заключается в том, что ProtoBuf не уважает семантику byte и, следовательно, не соответствует дереву сжатия частоты GZip;следовательно, относительно неэффективное сжатие.

Другая возможность состоит в том, что ProtoBuf фактически кодирует гораздо больше данных (например, для упрощения управления версиями схемы), поэтому сериализованные форматы не являются строго сопоставимыми с точки зрения информации.

Кто-нибудь наблюдает ту же проблему?Стоит ли вообще сжимать ProtoBuf?

Ответы [ 2 ]

5 голосов
/ 03 ноября 2011

Здесь возможен ряд факторов; во-первых, обратите внимание, что проводной формат буферов протокола использует прямое кодирование UTF-8 для строк; если в ваших данных преобладают строки, в конечном итоге им потребуется примерно столько же места, сколько для TSV.

Буферы протокола также предназначены для хранения структурированных данных, т.е. более сложных моделей, чем сценарий с одной таблицей. Это не сильно влияет на размер , но начните сравнивать с xml / json и т. Д. (Которые больше похожи по возможностям), и разница становится более очевидной.

Кроме того, поскольку буферы протокола довольно плотные (несмотря на UTF-8), в некоторых случаях сжатие может на самом деле увеличить - вы можете проверить, так ли это здесь.

В кратком примере сценария, который вы представляете, оба формата дают примерно одинаковые размеры - большого скачка нет:

protobuf-net, no compression: 2498720 bytes, write 34ms, read 72ms, chk 50000
protobuf-net, gzip: 1521215 bytes, write 234ms, read 146ms, chk 50000
tsv, no compression: 2492591 bytes, write 74ms, read 122ms, chk 50000
tsv, gzip: 1258500 bytes, write 238ms, read 169ms, chk 50000

TSV в этом случае незначительно меньше, но в конечном итоге TSV действительно является очень простым форматом (с очень ограниченными возможностями в отношении структурированных данных), поэтому неудивительно, что он быстрый.

Действительно, если все, что вы храните, это очень простая таблица, TSV - неплохой вариант, однако в конечном итоге это очень ограниченный формат. Я не могу воспроизвести ваш "гораздо больший" пример.

Помимо расширенной поддержки структурированных данных (и других функций), protobuf также уделяет большое внимание производительности обработки. Теперь, поскольку TSV довольно прост, преимущество здесь не будет массивным (но это заметно выше), но опять же: в отличие от xml, json или встроенного BinaryFormatter для теста против форматы со схожими функциями и разница очевидна.


Пример для чисел выше (обновлено для использования BufferedStream):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;
using ProtoBuf;
static class Program
{
    static void Main()
    {
        RunTest(12345, 1, new StringWriter()); // let everyone JIT etc
        RunTest(12345, 50000, Console.Out); // actual test
        Console.WriteLine("(done)");
        Console.ReadLine();
    }
    static void RunTest(int seed, int count, TextWriter cout)
    {

        var data = InventData(seed, count);

        byte[] raw;
        Catalog catalog;
        var write = Stopwatch.StartNew();
        using(var ms = new MemoryStream())
        {
            Serializer.Serialize(ms, data);
            raw = ms.ToArray();
        }
        write.Stop();

        var read = Stopwatch.StartNew();
        using(var ms = new MemoryStream(raw))
        {
            catalog = Serializer.Deserialize<Catalog>(ms);
        }
        read.Stop();

        cout.WriteLine("protobuf-net, no compression: {0} bytes, write {1}ms, read {2}ms, chk {3}", raw.Length, write.ElapsedMilliseconds, read.ElapsedMilliseconds, catalog.Products.Count);
        raw = null; catalog = null;

        write = Stopwatch.StartNew();
        using (var ms = new MemoryStream())   
        {
            using (var gzip = new GZipStream(ms, CompressionMode.Compress, true))
            using (var bs = new BufferedStream(gzip, 64 * 1024))
            {
                Serializer.Serialize(bs, data);
            } // need to close gzip to flush it (flush doesn't flush)
            raw = ms.ToArray();
        }
        write.Stop();

        read = Stopwatch.StartNew();
        using(var ms = new MemoryStream(raw))
        using(var gzip = new GZipStream(ms, CompressionMode.Decompress, true))
        {
            catalog = Serializer.Deserialize<Catalog>(gzip);
        }
        read.Stop();

        cout.WriteLine("protobuf-net, gzip: {0} bytes, write {1}ms, read {2}ms, chk {3}", raw.Length, write.ElapsedMilliseconds, read.ElapsedMilliseconds, catalog.Products.Count);
        raw = null; catalog = null;

        write = Stopwatch.StartNew();
        using (var ms = new MemoryStream())
        {
            using (var writer = new StreamWriter(ms))
            {
                WriteTsv(data, writer);
            }
            raw = ms.ToArray();
        }
        write.Stop();

        read = Stopwatch.StartNew();
        using (var ms = new MemoryStream(raw))
        using (var reader = new StreamReader(ms))
        {
            catalog = ReadTsv(reader);
        }
        read.Stop();

        cout.WriteLine("tsv, no compression: {0} bytes, write {1}ms, read {2}ms, chk {3}", raw.Length, write.ElapsedMilliseconds, read.ElapsedMilliseconds, catalog.Products.Count);
        raw = null; catalog = null;

        write = Stopwatch.StartNew();
        using (var ms = new MemoryStream())
        {
            using (var gzip = new GZipStream(ms, CompressionMode.Compress))
            using(var bs = new BufferedStream(gzip, 64 * 1024))
            using(var writer = new StreamWriter(bs))
            {
                WriteTsv(data, writer);
            }
            raw = ms.ToArray();
        }
        write.Stop();

        read = Stopwatch.StartNew();
        using(var ms = new MemoryStream(raw))
        using(var gzip = new GZipStream(ms, CompressionMode.Decompress, true))
        using(var reader = new StreamReader(gzip))
        {
            catalog = ReadTsv(reader);
        }
        read.Stop();

        cout.WriteLine("tsv, gzip: {0} bytes, write {1}ms, read {2}ms, chk {3}", raw.Length, write.ElapsedMilliseconds, read.ElapsedMilliseconds, catalog.Products.Count);
    }

    private static Catalog ReadTsv(StreamReader reader)
    {
        string line;
        List<Product> list = new List<Product>();
        while((line = reader.ReadLine()) != null)
        {
            string[] parts = line.Split('\t');
            var row = new Product();
            row.Id = int.Parse(parts[0]);
            row.Name = parts[1];
            row.QuantityAvailable = int.Parse(parts[2]);
            row.Price = decimal.Parse(parts[3]);
            row.Weight = int.Parse(parts[4]);
            row.Sku = parts[5];
            list.Add(row);
        }
        return new Catalog {Products = list};
    }
    private static void WriteTsv(Catalog catalog, StreamWriter writer)
    {
        foreach (var row in catalog.Products)
        {
            writer.Write(row.Id);
            writer.Write('\t');
            writer.Write(row.Name);
            writer.Write('\t');
            writer.Write(row.QuantityAvailable);
            writer.Write('\t');
            writer.Write(row.Price);
            writer.Write('\t');
            writer.Write(row.Weight);
            writer.Write('\t');
            writer.Write(row.Sku);
            writer.WriteLine();
        }
    }
    static Catalog InventData(int seed, int count)
    {
        string[] lipsum =
            @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
                .Split(' ');
        char[] skuChars = "0123456789abcdef".ToCharArray();
        Random rand = new Random(seed);
        var list = new List<Product>(count);
        int id = 0;
        for (int i = 0; i < count; i++)
        {
            var row = new Product();
            row.Id = id++;
            var name = new StringBuilder(lipsum[rand.Next(lipsum.Length)]);
            int wordCount = rand.Next(0,5);
            for (int j = 0; j < wordCount; j++)
            {
                name.Append(' ').Append(lipsum[rand.Next(lipsum.Length)]);
            }
            row.Name = name.ToString();
            row.QuantityAvailable = rand.Next(1000);
            row.Price = rand.Next(10000)/100M;
            row.Weight = rand.Next(100);
            char[] sku = new char[10];
            for(int j = 0 ; j < sku.Length ; j++)
                sku[j] = skuChars[rand.Next(skuChars.Length)];
            row.Sku = new string(sku);
            list.Add(row);
        }
        return new Catalog {Products = list};
    }
}
[ProtoContract]
public class Catalog
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public List<Product> Products { get; set; } 
}
[ProtoContract]
public class Product
{
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public string Name { get; set; }
    [ProtoMember(3)]
    public int QuantityAvailable { get; set;}
    [ProtoMember(4)]
    public decimal Price { get; set; }
    [ProtoMember(5)]
    public int Weight { get; set; }
    [ProtoMember(6)]
    public string Sku { get; set; }
}
3 голосов
/ 03 ноября 2011

GZip - это компрессор потока. Если вы не буферизуете данные должным образом, сжатие будет очень плохим, поскольку оно будет работать только на небольших блоках, что приведет к гораздо менее эффективному сжатию.

Попробуйте поместить BufferedStream между сериализатором и GZipStream с буфером правильного размера.

Пример. Сжатие последовательности Int32 1..100'000 с помощью BinaryWriter, выполняющего прямую запись в GZipStream, приведет к ~ 650 КБ, а при 64 КБ BufferedStream между получится только ~ 340 КБ сжатых данных.

...