#gzip #protocol-buffers #protobuf-net
#gzip #буферы протокола #protobuf-net
Вопрос:
Недавно мы сравнили соответствующие размеры файлов одних и тех же табличных данных (например, одна таблица, полдюжины столбцов, описывающих каталог продуктов), сериализованных с помощью ProtoBuf.NET или с помощью TSV (данные, разделенные табуляцией), оба файла впоследствии сжимаются с помощью GZip (по умолчанию .ЧИСТАЯ реализация).
Я был удивлен, заметив, что сжатый ProtoBuf.Сетевая версия занимает намного больше места, чем текстовая версия (в 3 раза больше). Моя любимая теория заключается в том, что ProtoBuf не учитывает byte
семантику и, следовательно, не соответствует дереву сжатия частоты GZip; следовательно, относительно неэффективное сжатие.
Другая возможность заключается в том, что ProtoBuf кодирует, на самом деле, намного больше данных (например, для облегчения управления версиями схемы), поэтому сериализованные форматы не являются строго сопоставимыми с информационной точки зрения.
Кто-нибудь наблюдает ту же проблему? Стоит ли вообще сжимать ProtoBuf?
Комментарии:
1. re «не учитывает семантику байта» — можете ли вы уточнить, что вы имеете в виду? проводной формат protobuf не выполняет никаких суббайтовых операций, если вы это имеете в виду, поэтому проблем с выравниванием быть не должно. Мне было бы интересно посмотреть образец здесь, чтобы я мог сформировать точный ответ … (в противном случае я как бы догадываюсь о ваших данных)
2. Извините, мое недопонимание тогда исправило вопрос.
Ответ №1:
Здесь возможен ряд факторов; во-первых, обратите внимание, что формат проводных буферов протокола использует прямую кодировку 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; }
}
Ответ №2:
GZip — это потоковый компрессор. В случае, если вы не буферизуете данные должным образом, сжатие будет очень плохим, поскольку оно будет работать только с небольшими блоками, что приведет к гораздо менее эффективному сжатию.
Попробуйте поместить BufferedStream между вашим сериализатором и GZipStream с буфером правильного размера.
Пример: сжатие последовательности Int32 1 ..100’000 с помощью BinaryWriter, непосредственно записывающего в GZipStream, приведет к ~ 650 КБ, в то время как с 64 кб BufferedStream между ними приведет только к ~ 340 КБ сжатых данных.
Комментарии:
1. хех; добавил BufferStream в мой пример, и это не имело … ровно никакого значения; p
2. Интересно, тогда я предполагаю, что вы правильно буферизуете уже на внутренней стороне записи (что в любом случае имеет смысл). Потому что даже для 4 МБ совершенно случайных данных буфер в 1-2 КБ приведет к почти точному размеру входных данных, в то время как при записи в поток в блоках 4b вы получите на 50% больший результат.
3. Он выполняет много внутренней буферизации (
byte[]
большую часть времени для удаления абстракций с необработанным размером), сбрасывая, когда это необходимо