Я уже на раз говорил, о том что сериализация в дотнете сделана очень не оптимально. Данная тема является научным доказательством правоты моих слов. Итак, я сделал тест в котором создается массив структур размеров в сто тысяч элементов (ну, шоб було чё мерить) который сиреализуется с помощью:
* BinaryFormatter-а.
* Рукопашной бинарной сериализации.
* SoapFormatter-а.
При этом выводится время сериализации и размер полученных данных.
Но и это еще не все! (с) Какая-то реклама.
Я нарыл какой-то левый ActivX-контрол — ActiveZip (как я подключал его к нету это отдельная песня ...) и зазиповал им полученные данные. Этот тупой активыкс умеет зиповать только в файл. Ну, да это мелочи.
Вот результаты смелого эксперимента советских ученых (время в миллисекундах, размер в байтах):
BinaryFormatter serialization time elapsed: 1 132, Length is: 5 041 592
Binary manual formatted serialization time elapsed: 60, Length is: 3 641 406
SoapFormatter serialization time elapsed: 3 415, Length is: 12 285 041
SoapFormatter.Length / BinaryManual.Length = 3.37
SoapFormatter.Time / BinaryManual.Time = 56.92 <-- !!! Поглядите на это
BinaryFormatter zip time elapsed: 1 472,
BinaryManual zip time elapsed: 971,
SoapFormatter zip time elapsed: 1 182,
А вот размеры зипов:
testBinaryFormatter.zip — 1 690 884
testBinaryManual.zip — 1 209 830
testSoapFormatter.zip — 1 875 791
Скажу честно, если говорить о размерах данных, то я думал о бинарном форматере несколько хуже. Бинарный форматер умудряется создать всего на четверть более пухлый чем следовало бы. Но по скорости форматеры дотнета не выдерживают никакой критики. Разница в десятки раз (около 20 между бинарными и 60 (!) между соап и бинарным). В общем поубивал бы.
И это притом, что сеарилизацией по сути дела заведует сам компилятор!
Зипование несколько скрашивает грустную ситуацию, но все же. Даже зипованый файл на 40-50 процентов больше идеального. Плюс больше время за зазиповку.
Вы только представьте! За время сериализации BinaryFormatter-ом можно успеть не только сериализовать, но и зазиповать данные, уменьшив их в 3 раза! в А ведь при зиповке данные еще к тому же записывались на диск!
Соап форматер по времени вообще убийственен.
Выводы
Сначала немного разбавим темные краски. Все же в этом тесте сериализовались относительно большие объемы данных. Обычно приходится иметь дело с данными в десять, а то и в сто раз меньшими по объему. Учитывая, что процессоры более-менее шустрые на маленьких объемах можно стерпеть и 60-и кратные тормоза. Однако на сервере, учитывая, что каждый пользователь умножает объем обрабатываемой информации, это может стать серьезной проблемой. Причем не зная приведенных мной данных, скорее всего тормоза будут списаны на JIT-компиляцию и другие инсинуации.
Ремоутинг и комплюс... Именно их, данная проблема задевает в первую очередь. Дело в том, что при маршалинге данные сериализуются. А значит время, потраченное на этот процесс, замедляет работу ремоутинга. Раздутый результат сериализации только усугубляет ситуацию.
DataSet. Отдельной проблемой является DataSet. Дело в том, что DataSet всегда сериализуются в XML. Даже если его попросить сериализоваться в бинари-форматер, он все равно сериализуются в XML, а только потом получившуюся текстовую строку записывает в форматер.
Кто виноват? И что делать?
Ну, виноват известно кто... Мокрософт. На такую важную вещь как сериализация можно было бы потратить больше усилий и средств.
А вот что делать? Дотнет 1.1 не решает проблему. Ждать же более новой версии дотнета еще очень долго, да и скорее всего проблема в нем снова не будет решена. Единственное что может заставить МС заняться сериализацией — это каой-нить конкурент. Например, Сан может заявить, что Ява сериализует объекты в ХХХ раз быстрее... И тогда Буквально через пол года дотнетная сериализаци станет круче паровоза. Однако верится в это с трудом. Так что нужно брать все в свои руки.
Пути... В первую очередь можно написать ручную сериализацию для наиболее часто используемых объектов хранящих/передающих большие объемы данных. DataSet просто напрашивается на, то чтобы первым надругались над ним. Вернее не над ним, а на одной из его составных — над DataTable. Ведь именно этот класс хранит реальные данные.
1. В идеале нужно сделать код автоматической сериализации для произвольного объекта. С моей (сильно абстрактной) колокольни это видится так:
Пишется небольшой класс, который для каждого типа на лету создает (с помощью System.Reflection.Emit) код сериализации. Это позволит, потратив немного времени на начальном этапе получить практически идеальную (по всем параметрам) сериализацию.
2. Вопреки моде и рекламе стараться передавать по сети бинарные данные. Причем не грех будет и попытаться сжать данные (но делать это нужно осторожно, так как процесс этот ресурсоемкий). Тот же DataSet можно сериализовать в бинарный вид даже при передаче по http. При этом можно использовать или формат base64, или (что еще лучше) стандарт DIME (позволяющий делать бинарные вложения в http-запрос). Красота и читаемость XML-я в данном случае совершенно недужна. Признайтесь честно часто вы просматриваете сетевые пакеты? В конце концов, всегда можно превратить данные в XML и полюбоваться на них. А вот на диске можно хранить и XML. Если его много, лучше не пользоваться соап-форматером. Найдите где-нибудь намного более эффективных SAX-парсер и разбирайте данные им. Однако занятие это не легкое. И если проблем с производительностью нет, лучше не искать проблем на свою голову. Бесполезно пытаться ускорить процесс разбора XML-я и DOM-парсерами. По крайней мере, тот, что входит в дотнет работает еще медленнее, чем соап-форматер.
Ну, и последний вопрос. Стоит ли ломать копья? Шестидесяти кратная разница в скорости может убедить кого угодно. Особенно если пользователи уже начинают подвывать. Например, похожая проблема назревает в rsdn@houme. Несколько хоумщиков одновременно выбирающие сообщения за день могут серьезно и довольно на долго затормозить сервер. А ведь страдают от этого в основном онлайнщики. Ведь хоумщик в это время пьют чай.
SP
Пользуясь, случаем передаю привет АВК и всем кто считает что сериализация в нете сделана нормально.
Код теста (C#):
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters.Soap;
// Это ком-объект-обертка над ActiveZip сделанная на VB6. Без нее он валится. :(
// Возможно дело в том, что этот компонент хочет лежать на форме.using TestCom;
namespace SerializeTest
{
/// <summary>
/// Подопытная структура. Из нее создается массив которы
/// нужно сериализовать.
/// </summary>
[Serializable]
public struct TestData
{
// Данные...public int _i1;
public int _i2;
public double _f1;
public string _s1;
/// <summary>
/// Ручная сериализация.
/// Сериализует содержимое массива структур TestData в стрим.
/// </summary>
/// <param name="ms">Стрим куда сериализуются данные</param>
/// <param name="ary">Массив</param>public static void Write(MemoryStream ms, TestData[] ary)
{
BinaryWriter bw = new BinaryWriter(ms);
Int32 iLen = ary.Length;
bw.Write(iLen);
for(int i = 0; i < iLen; i++)
{
bw.Write(ary[i]._i1);
bw.Write(ary[i]._i2);
bw.Write(ary[i]._f1);
bw.Write(ary[i]._s1);
}
}
/// <summary>
/// Ручная сериализация.
/// Создает массив структур TestData и считывает в него информацию
/// из стрима.
/// </summary>
/// <param name="ms">Стрим в котором находится сериализованное
/// представление массива структур (созданное с помощю метда Write).</param>
/// <returns></returns>public static TestData[] Read(MemoryStream ms)
{
BinaryReader br = new BinaryReader(ms);
Int32 iLen = br.ReadInt32();
TestData[] ary = new TestData[iLen];
for(int i = 0; i < iLen; i++)
{
ary[i]._i1 = br.ReadInt32();
ary[i]._i2 = br.ReadInt32();
ary[i]._f1 = br.ReadDouble();
ary[i]._s1 = br.ReadString();
}
return ary;
}
/// <summary>
/// Создает и инициализирует массив структур TestData
/// </summary>
/// <param name="ArrayLen">Количество элементов в массиве</param>
/// <returns>Заполненный массив</returns>public static TestData[] InitErray(int ArrayLen)
{
TestData[] aryTd = new TestData[ArrayLen];
for(int i = 0; i < ArrayLen; i++)
{
aryTd[i]._f1 = i * 1.3;
aryTd[i]._i1 = i;
aryTd[i]._i2 = i * 2;
aryTd[i]._s1 = "Стр" + i.ToString() + " " + (i * 234).ToString();
}
return aryTd;
}
}
/// <summary>
/// Summary description for Class1.
/// </summary>class Class1
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
int start; // Используется для вычисления времени выполнения
// Стрим для сериализации через BinaryFormatter
MemoryStream msBf = new MemoryStream(10*1000*1000);
// Стрим для ручной сериализации.
MemoryStream msBm = new MemoryStream(10*1000*1000);
// Стрим для сериализации через SoapFormatter
MemoryStream msSf = new MemoryStream(10*1000*1000);
BinaryFormatter bf = new BinaryFormatter();
SoapFormatter sf = new SoapFormatter();
// Массив структур который будет сериализоваться
TestData[] aryTestData = TestData.InitErray(100*1000);
// Сериализация BinaryFormatter-ом
start = Environment.TickCount;
bf.Serialize(msBf, aryTestData);
Console.WriteLine(
"BinaryFormatter serialization time elapsed: {0:### ### ###}, "
+ "Length is: {1:### ### ###}",
Environment.TickCount - start, msBf.Length);
// Сериализация врукопашную
start = Environment.TickCount;
TestData.Write(msBm, aryTestData);
int BinaryManualTime = Environment.TickCount - start;
Console.WriteLine(
"Binary manual formatted serialization time elapsed: {0:### ### ###}, "
+ "Length is: {1:### ### ###}",
BinaryManualTime, msBm.Length);
// Сериализация SoapFormatter-ом
start = Environment.TickCount;
sf.Serialize(msSf, aryTestData);
int SoapFormatterTime = Environment.TickCount - start;
Console.WriteLine(
"SoapFormatter serialization time elapsed: {0:### ### ###}, "
+ "Length is: {1:### ### ###}",
SoapFormatterTime, msSf.Length);
// Выводим разницу между бинарной и соапной сериализацией.
Console.WriteLine("SoapFormatter.Length / BinaryManual.Length = {0:0.00}",
msSf.Length * 1.0 / msBm.Length);
Console.WriteLine("SoapFormatter.Time / BinaryManual.Time = {0:0.00}",
SoapFormatterTime * 1.0 / BinaryManualTime);
TestCom.Class1Class test = new TestCom.Class1Class();
//////////////////////////////////////////////////////////////////////////
// Зипуем...string zipName;
string binName;
FileStream fs;
// Зипуем данные полученные BinaryFormatter-ом
start = Environment.TickCount;
zipName = Environment.CurrentDirectory + @"\testBinaryFormatter.zip";
binName = Environment.CurrentDirectory + @"\testBinaryFormatter";
fs = new FileStream(binName, FileMode.Create);
msBf.WriteTo(fs);
fs.Close();
test.ZipData(zipName, binName);
Console.WriteLine(
"BinaryFormatter zip time elapsed: {0:### ### ###}, ",
Environment.TickCount - start);
// Зипуем данные полученные вручную.
start = Environment.TickCount;
zipName = Environment.CurrentDirectory + @"\testBinaryManual.zip";
binName = Environment.CurrentDirectory + @"\testBinaryManual";
fs = new FileStream(binName, FileMode.Create);
msBm.WriteTo(fs);
fs.Close();
test.ZipData(zipName, binName);
Console.WriteLine(
"BinaryManual zip time elapsed: {0:### ### ###}, ",
Environment.TickCount - start);
// Зипуем данные полученные SoapFormatter-ом
start = Environment.TickCount;
zipName = Environment.CurrentDirectory + @"\testSoapFormatter.zip";
binName = Environment.CurrentDirectory + @"\testSoapFormatter";
fs = new FileStream(binName, FileMode.Create);
msSf.WriteTo(fs);
fs.Close();
test.ZipData(zipName, binName);
Console.WriteLine(
"SoapFormatter zip time elapsed: {0:### ### ###}, ",
Environment.TickCount - start);
Console.ReadLine();
}
}
}
Обертака для ActiveZip (VB6):
' Class1Option Explicit
Public Sub ZipData(ByVal ZipFilename As String, ByVal AddFilename As String)
Form1.ZipData ZipFilename, AddFilename
Unload Form1
End Sub' Form1 свойство Visible этой формы выставлено в false (шобы ее небыло вдно).
' Возможно я и перемудрил. Может быть достаточно было бросить этот кривой объект
' на WinForms-ую форму. Теперь пределываьть уже влом.Option Explicit
Public Sub ZipData(ByVal ZipFilename As String, ByVal AddFilename As String)
ActiveZip1.CreateZip ZipFilename, ""Dim iLen As Long
ActiveZip1.AddFile AddFilename, ""
ActiveZip1.PreservePaths = False
ActiveZip1.Close
End Sub
... << RSDN@Home 1.0 beta 6a >>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Я нарыл какой-то левый ActivX-контрол — ActiveZip (как я подключал его к нету это отдельная песня ...) и зазиповал им полученные данные. Этот тупой активыкс умеет зиповать только в файл.
Могу кинуть обёрнутый в .NET zlib.
VD>Кто виноват? И что делать?
А просто написать свой форматтер и использовать его в Remoting разве не получится?
... << RSDN@Home 1.0 beta 6a >>
Если нам не помогут, то мы тоже никого не пощадим.
Здравствуйте, IT, Вы писали:
IT>Здравствуйте, VladD2, Вы писали:
VD>>Я нарыл какой-то левый ActivX-контрол — ActiveZip (как я подключал его к нету это отдельная песня ...) и зазиповал им полученные данные. Этот тупой активыкс умеет зиповать только в файл.
IT>Могу кинуть обёрнутый в .NET zlib.
Давай. На мыло, плиз. И сам zlib тоже. Прикрутим (по позже) к хоуму.
VD>>Кто виноват? И что делать?
IT>А просто написать свой форматтер и использовать его в Remoting разве не получится?
Я не разбирался с форматерами. Возможно и можно. Но это будет не "просто".
... << RSDN@Home 1.0 beta 6a >>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
IT>>Могу кинуть обёрнутый в .NET zlib.
VD>Давай. На мыло, плиз. И сам zlib тоже.
Ok, сейчас поищу.
VD>Прикрутим (по позже) к хоуму.
Если хоуму выставить чего-нибудь через ремоутинг, то можно будет прикрутить как раз нормальный форматтер.
IT>>А просто написать свой форматтер и использовать его в Remoting разве не получится?
VD>Я не разбирался с форматерами. Возможно и можно. Но это будет не "просто".
Проще наверное будет как раз с этим разобраться.
... << RSDN@Home 1.0 beta 6a >>
Если нам не помогут, то мы тоже никого не пощадим.
Здравствуйте, IT, Вы писали:
VD>>Прикрутим (по позже) к хоуму.
IT>Если хоуму выставить чего-нибудь через ремоутинг, то можно будет прикрутить как раз нормальный форматтер.
Так что — забиваем тогда на сервис и переходим на ремоутинг?
VD>Пишется небольшой класс, который для каждого типа на лету создает (с помощью System.Reflection.Emit) код сериализации. Это позволит, потратив немного времени на начальном этапе получить практически идеальную (по всем параметрам) сериализацию.
Такие утилиты вообще-то положено по кругу пускать, чтобы лучше различать тенденцию и начальные выбросы. Хотя в данном случае особых выбросов нету.
Добавил XML Serialization.
BinaryFormatter serialization time elapsed: 1 752, Length is: 5 041 578
Binary manual formatted serialization time elapsed: 80, Length is: 3 641 406
SoapFormatter serialization time elapsed: 4 817, Length is: 12 285 027
XmlSerializerTime serialization time elapsed: 2 734, Length is: 13 295 651 (chars)
SoapFormatter.Length / BinaryManual.Length = 3.37
SoapFormatter.Time / BinaryManual.Time = 60.21
Пришлось убрать ZIP, так как у меня его нету. В результате получается, что XML Serialization примерно посредине между Binary и SOAP. А по объёму чуть больше SOAP.
Здравствуйте, mihailik, Вы писали:
M>Добавил XML Serialization.
Дык пость код суда. Я прогоню на той же машине... Можно даже малюсенькую статейку состряпать.
M>Пришлось убрать ZIP, так как у меня его нету. В результате получается, что XML Serialization примерно посредине между Binary и SOAP. А по объёму чуть больше SOAP.
Все равно скорость даже у бинари никуда не годится.
... << RSDN@Home 1.0 beta 6a >>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Все равно скорость даже у бинари никуда не годится.
Эх Влад, не все так там просто. Форматтер != сериалайзер. У форматтера куча наворотов что бы это все в ремоутинге работало и кучу типов маршалило, так что написать свой форматтер будет не очень легко.
Здравствуйте, AndrewVK, Вы писали:
AVK>Эх Влад, не все так там просто. Форматтер != сериалайзер. У форматтера куча наворотов что бы это все в ремоутинге работало и кучу типов маршалило, так что написать свой форматтер будет не очень легко.
Кто бы спорил. Но сделать ручную сериализацию для того же Датасета можно. И относительно не сложно.
... << RSDN@Home 1.0 beta 6a >>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, AndrewVK, Вы писали:
AVK>Эх Влад, не все так там просто. Форматтер != сериалайзер. У форматтера куча наворотов что бы это все в ремоутинге работало и кучу типов маршалило, так что написать свой форматтер будет не очень легко.
VD>Кто бы спорил. Но сделать ручную сериализацию для того же Датасета можно. И относительно не сложно.
N>Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, AndrewVK, Вы писали:
AVK>Эх Влад, не все так там просто. Форматтер != сериалайзер. У форматтера куча наворотов что бы это все в ремоутинге работало и кучу типов маршалило, так что написать свой форматтер будет не очень легко.
VD>Кто бы спорил. Но сделать ручную сериализацию для того же Датасета можно. И относительно не сложно.
VD>>Все равно скорость даже у бинари никуда не годится.
AVK>Эх Влад, не все так там просто. Форматтер != сериалайзер. У форматтера куча наворотов что бы это все в ремоутинге работало и кучу типов маршалило, так что написать свой форматтер будет не очень легко.
Да, разные там MethodReturnMessage. Видимо, поэтому XML Serialization быстрее SoapFormatter получается.
Не корректно сравнивать Formatter-ы и рукопашнюю бинарную сериализацию (по крайней мере в том виде, в котором она сделана у тебя).
стандартные Formatter-ы поддерживают:
1. смену версий классов (добавление, удаление полей)
2. сохранение объектного графа с произвольной структурой
Измение механизма serialize-ации для отдельных подчастей сохраняемого объектного графа может приводить к глюкам,
например, это видно на следующем примере:
public class Row
{
}
public class Main
{
public Table table = new Table();
public Row row;
}
//у этого класс свой личный serializerpublic class Table
{
public Row row;
}
void Test()
{
Main main = new Main();
main.row = main.table.row = new Row();
Save(main);
Main main2 = Load();
}
В этом примере, если у класс B мы написали свой хитрый serializer, а сохраняли весь класс Main.
То получится, что main2 будет с другой структурой (будет две копии Row), чем main
Здравствуйте, mihailik, Вы писали:
M>Да, разные там MethodReturnMessage. Видимо, поэтому XML Serialization быстрее SoapFormatter получается.
XmlSerializer быстрее еще потому что во первых не использует DOM парсер, во вторых сериализующий класс генериться в конструкторе, а затем при сериализации рефлекшен почти не используется.
Здравствуйте, VladD2, Вы писали:
VD>Кто бы спорил. Но сделать ручную сериализацию для того же Датасета можно. И относительно не сложно.
Чтобы это работало в ремоутинге нужно писать кастомную сериализацию на основе ISerializable, а это, как показывает практика, сильного выигрыша не дает. Есдинственный выход — явно сериализовать, не используя инфраструктуру нета и передавать в качестве параметров массив байтов.