О проблеме
Сериализация массива структур
Результаты смелого эксперимента советских ученых:
Сериализация DataSet-а
Комментарии и выводы Кто виноват? И что делать? Исходные коды теста P.S. |
Исходный код сериализатора и тестов
Библиотека ZLib (MC++), 200kB
Библиотека SharpZipLib (C#), 1,5MB
Я не раз на форумах RSDN заявлял, что сериализация в дотнете сделана очень неоптимально. Данная статья является научным доказательством правоты моих слов. А так же описывает пути, позволяющие уменьшить негативное влияние этого факта на прикладные приложения.
Итак, я сделал тест, в котором создается массив структур размером в сто тысяч элементов (ну, чтобы было, что измерять) который сериализуется с помощью:
При этом выводится время сериализации, загрузки данных, а также размер полученных данных. Я также попытался сжать полученный бинарный поток, так что тест ко всему прочему выводит время, требующееся на сжатие данных, и размер получаемых сжатых данных. В качестве архиватора были опробованы:
Обе библиотеки можно найти на прилагаемом к журналу CD.
Скажу честно, что zlib мне понравилась намного больше. Во-первых, она банально быстрее работает. Во-вторых, она поддерживает типы компрессии (быстрая, обычная, сильная), что позволяет радикально сократить время на архивацию при не очень значительной потере в степени сжатия. В-третьих, zlib оказалась очень проста в использовании. Ее managed-обертка проста, но функциональна и даже изящна. В общем, в окончательный вариант теста я включил только результат zlib.
Все тесты повторялись по три раза, чтобы исключить влияние инициализации на результаты теста.
Вот
Время сериализации | Размер | Сжатие (быстрое) | Сжатие (нормальное) сохранение | загрузка | stream-а | время | размер | время | размер (сек.) | (сек.) | (байт) | (сек.) | (байт) | (сек.) | (байт) ------------+----------+------------+--------+-----------+--------+---------+ CustomSerialization 0.0621 | 0.1243 | 3 641 406 | 0.2307 | 1 353 346 | 0.8073 | 1 209 622 0.0616 | 0.1198 | 3 641 406 | 0.2321 | 1 353 346 | 0.8259 | 1 209 622 0.0660 | 0.1295 | 3 641 406 | 0.2444 | 1 353 346 | 0.8225 | 1 209 622 BinaryFormatter 1.0207 | 2.1445 | 5 041 593 | 0.3118 | 1 777 823 | 1.2298 | 1 690 670 1.0066 | 2.1887 | 5 041 593 | 0.3079 | 1 777 823 | 1.2285 | 1 690 670 1.0007 | 2.1943 | 5 041 593 | 0.3079 | 1 777 823 | 1.2293 | 1 690 670 SoapFormatter 3.1607 | 8.7860 | 12 285 042 | 0.3809 | 2 204 652 | 0.8129 | 1 875 580 3.1064 | 8.7989 | 12 285 042 | 0.3887 | 2 204 652 | 0.8293 | 1 875 580 3.1029 | 8.8187 | 12 285 042 | 0.3849 | 2 204 652 | 0.8196 | 1 875 580 XmlSerializer 1.9823 | 2.4203 | 13 595 633 | 0.3600 | 1 934 505 | 0.7528 | 1 609 710 1.8117 | 2.3614 | 13 595 633 | 0.3597 | 1 934 505 | 0.7528 | 1 609 710 1.8057 | 2.3591 | 13 595 633 | 0.3695 | 1 934 505 | 0.7601 | 1 609 710 |
Скажу честно, если говорить о размерах данных, то я думал о бинарном форматере несколько хуже. Бинарный форматер умудряется создать всего на четверть более пухлый, чем следовало бы. Но по скорости форматеры дотнета не выдерживают никакой критики. Разница в десятки раз (около 16/18 между бинарными и 50/73 (!) между SOAP и бинарным).
И это притом, что сериализацией по сути дела заведует сам компилятор!
Сжатие несколько скрашивает грустную ситуацию с размером сериализованных данных, но даже сжатый файл на 30-60 процентов больше идеального. Плюс потери времени на сжатие. А ведь сериализовались совершенно простые структуры.
Вы только представьте! За время сериализации BinaryFormatter-ом можно успеть не только сериализовать, но и сжать данные, уменьшив их в 3 раза!
SOAP-форматер по времени вообще убийственен.
Сначала немного разбавим темные краски. Все же в этом тесте сериализовались относительно большие объемы данных. Обычно приходится иметь дело с данными в десять, а то и в сто раз меньшими по объему. Учитывая, что процессоры нынче более-менее шустрые, на маленьких объемах можно стерпеть и 60-кратные тормоза. Особенно, если сериализация делается не часто. Однако на сервере, учитывая, что каждый пользователь умножает объем обрабатываемой информации, это может стать серьезной проблемой. Причем, не зная приведенных мной данных, скорее всего тормоза будут списаны на JIT-компиляцию и другие аспекты.
Ремоутинг и COM+... Именно их данная проблема задевает в первую очередь. Дело в том, что при маршалинге данные сериализуются. А значит время, потраченное на этот процесс, замедляет работу ремоутинга. Раздутый результат сериализации только усугубляет ситуацию.
Однако ручная сериализация – это дополнительный код, а значит, и дополнительное время, ну и разумеется, дополнительные ошибки, а стало быть, снова время и нервы. Если учесть, что стандартная сериализация в .NET обеспечивает автоматическую сериализацию графов объектов (обеспечивая его восстановление при десериализации, с восстановлением всех связей), то становится понятным, что к ручной сериализации стоит прибегать, только имея серьезные основания. Если же сериализуемый объект содержит ссылки на MarshalByRefObject (передаваемые по ссылке объекты), то сериализация существенно усложняется, так как придется залезть в довольно низкоуровневые вещи, чтобы обеспечить передачу ссылки на объект в другой процесс (домен приложения, контекст).
Однако при передаче данных по сети часто бывает, что их структура проста, сами они однородны, а вот объем передаваемых данных велик. В таких случаях подумать о ручной сериализации будет совсем не грешно. Но опять таки, убедитесь, что это даст реальную выгоду.
Лучше всего начинать заниматься оптимизацией (а ручная сериализация и есть оптимизация) только если есть проблемы с производительностью. При этом стоит в первую очередь постараться реализовать ручную сериализацию для классов, которые используются наиболее часто, и/или используются для передачи значительных объемов данных.
Ну, виноват известно кто... Мокрософт. На такую важную вещь, как сериализация, можно было бы потратить больше усилий и средств.
А вот что делать? Дотнет 1.1 не решает проблему. Ждать же более новой версии дотнета еще очень долго, да и скорее всего, проблема в нем снова не будет решена. Единственное, что может заставить Microsoft заняться сериализацией - это какой-нибудь конкурент. Например, Sun может заявить, что Ява сериализует объекты в ХХХ раз быстрее... И тогда буквально через полгода сериализация в .NET станет круче паровоза. Однако верится в это с трудом. Так что нужно брать все в свои руки.
Пути... В первую очередь можно написать ручную сериализацию для наиболее часто используемых объектов, хранящих/передающих большие объемы данных. DataSet просто напрашивается на то, чтобы первым надругались над ним. Вернее не над ним, а на одной из его составляющих – DataTable. Ведь именно этот класс хранит реальные данные.
Ну и последний вопрос. Стоит ли ломать копья? Шестидесятикратная разница в скорости может убедить кого угодно. Особенно если пользователи уже начинают подвывать. Например, похожая проблема назревает в rsdn@home. Несколько хоумщиков, одновременно выбирающих сообщения, могут серьезно и довольно надолго затормозить сервер. А ведь страдают от этого в основном онлайн-пользователи. Ведь хоумщики в это время пьют чай.
using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization.Formatters.Soap; using System.Xml.Serialization; using Zlib; using System.Runtime.Serialization; using System.Xml; namespace SerializeTest { /// <summary>/// Подопытная структура. Из нее создается массив, который/// нужно сериализовать./// </summary> [Serializable] publicstruct TestData { // Данные...publicint _i1; publicint _i2; publicdouble _f1; publicstring _s1; /// <summary>/// Ручная сериализация./// Сериализует содержимое массива структур TestData в стрим./// </summary>/// <param name="ms">Стрим, куда сериализуются данные</param>/// <param name="ary">Массив</param>publicstaticvoid 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>publicstatic 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>publicstatic 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; } } publicclass Test1 { conststring _serHeaderFormatString = " Время сериализации | Размер | Сжатие (быстрое) | Сжатие (нормальное)\n" + " сохранение | загрузка | stream-а | время | размер | время | размер\n" + " (сек.) | (сек.) | (байт) | (сек.) | (байт) | (сек.) | (байт)\n" + "------------+----------+------------+--------+-----------+--------+---------+"; // Определяет, была ли отпечатана шапка. Нужно для подавления// повторной печати шапки.staticbool _headerNotPrinted = true; // Тип сериализации. Нужна для того, чтобы не выводить тип при // повторных тестах. Используется в функции PrintHeader.staticstring _serializationType; staticvoid PrintHeader(string testName) { if(_headerNotPrinted) { _headerNotPrinted = false; Console.WriteLine(_serHeaderFormatString); } if(_serializationType != testName) { _serializationType = testName; Console.WriteLine("\t\t\t" + _serializationType); } } // Строка формата для вывода данных о времени и размере сжатия.// 0 - время, 1 - размер.conststring _zipFormatString = "{0,5:##0.0000} | {1,9:### ### ###}"; staticvoid ZipData(MemoryStream ms) { byte[] aryZip; // Используется для вычисления времени выполнения. PerfCounter timer = new PerfCounter(); // Сжимаем данные "быстрым" методом (BestSpeed) timer.Start(); aryZip = ZipBase.Compress(ms.ToArray(), ZipBase.Level.BestSpeed); Console.Write(_zipFormatString + " | ", timer.Finish(), aryZip.Length); // Сжимаем данные стандартным методом (DefaultCompression) timer.Start(); aryZip = ZipBase.Compress(ms.ToArray(), ZipBase.Level.DefaultCompression); Console.Write(_zipFormatString + "\n", timer.Finish(), aryZip.Length); } // Строка формата для вывода данных о времени и размере сериализации.// 0 - время сериализации, 1 - время восстановления, 2 - размер.conststring _serFormatString = "{0,11:### ### ##0.0000} | {1,8:##0.0000} | {2,10:### ### ###} | "; publicstaticvoid TestCustomSerialization(TestData[] aryTestData) { PrintHeader("CustomSerialization"); Utils.ClearMem(); // Используется для вычисления времени выполнения. PerfCounter timer = new PerfCounter(); // Стрим для сериализации через CustomSerialization MemoryStream ms = new MemoryStream(10*1000*1000); // Сериализация CustomSerialization-ом timer.Start(); TestData.Write(ms, aryTestData); float timeSerialize = timer.Finish(); // Загрузка CustomSerialization ms.Seek(0, SeekOrigin.Begin); timer.Start(); TestData.Read(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); ZipData(ms); // Сжимаем данные } publicstaticvoid TestBinaryFormatter(TestData[] aryTestData) { PrintHeader("BinaryFormatter"); Utils.ClearMem(); // Используется для вычисления времени выполнения. PerfCounter timer = new PerfCounter(); // Стрим для сериализации через BinaryFormatter MemoryStream ms = new MemoryStream(10*1000*1000); // Сериализация BinaryFormatter-ом timer.Start(); BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(ms, aryTestData); float timeSerialize = timer.Finish(); // Загрузка BinaryFormatter ms.Seek(0, SeekOrigin.Begin); timer.Start(); bf.Deserialize(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); ZipData(ms); // Сжимаем данные } // ...// Остальные тесты практически такие же, как TestBinaryFormatter.// В них меняется только форматер.// Основной метод теста. В нем создается массив структур и последовательно// вызываются тесты (по три раза каждый).publicstaticvoid Run() { Console.WriteLine( "\nТест 1: Сериализация массива простых струкртур\n"); // Массив структур, который будет сериализоваться TestData[] aryTestData = TestData.InitErray(100*1000); // Сериализация врукопашную TestCustomSerialization(aryTestData); TestCustomSerialization(aryTestData); TestCustomSerialization(aryTestData); // Сериализация BinaryFormatter-ом TestBinaryFormatter(aryTestData); TestBinaryFormatter(aryTestData); TestBinaryFormatter(aryTestData); // Сериализация SoapFormatter-ом TestSoapFormatter(aryTestData); TestSoapFormatter(aryTestData); TestSoapFormatter(aryTestData); // Сериализация XmlSerializer-ом TestXmlSerializer(aryTestData); TestXmlSerializer(aryTestData); TestXmlSerializer(aryTestData); Console.WriteLine("\nГотово!"); } } } |
Реализовать ручную сериализацию для классов прикладного приложения – занятие неблагодарное. Если придерживаться стратегии получения максимальной отдачи при минимальном вкладе, то нужно создать ручную сериализацию для объектов, которые чаще всего используются при передаче данных по сети. DataSet как раз и является таким объектом. Он универсален, а значит, с его помощью можно передавать совершенно разные данные. Причем код самого DataSet-а при этом изменять не нужно (да и как, собственно, это сделать?!). Ваше желание, думаю, усилится, если я скажу, что DataSet принципиально не умеет сериализоваться в бинарную форму. То есть он прекрасно может быть сохранен средствами BinaryFormatter, но при этом в поток будет сохранено XML-представление (попросту размеченный текст) DataSet-а. Сделано это так. Для DataSet-а реализована ручная сериализация. При этом в методе ISerializable.GetObjectData попросту добавляются два значения, KEY_XMLSCHEMA и KEY_XMLDIFFGRAM, содержащие (как не трудно догадаться по их именам) XML-описание и содержимое DataSet-а. Вот код этого метода объекта DataSet:
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { string s = GetXmlSchemaForRemoting(null); string ss = null; info.AddValue(KEY_XMLSCHEMA, s); StringBuilder b = new StringBuilder(EstimatedXmlStringSize() * 2); StringWriter w = new StringWriter(b); XmlTextWriter x = new XmlTextWriter(w); WriteXml(x, XmlWriteMode.DiffGram); ss = w.ToString(); info.AddValue(KEY_XMLDIFFGRAM, ss); } |
Примерно такой же код содержит метод GetObjectData объекта DataTable.
Ух ты! - подумают многие. А зачем же это сделано? По всей видимости, господа из Microsoft были вынуждены делать ручную сериализацию для DataSet-а, поскольку столь сложный объект, как DataSet очень трудно корректно сохранить автоматически. А может быть, они просто пытались улучшить скорость сериализации. Как бы то ни было, но получилось все это у них (как говорилось в одном анекдоте) препохабно. Мало того, что объем сериализации даже при сериализации BinaryFormatter-ом не лезет ни в какие ворота. Так еще к тому же и скорость сериализации (а особенно загрузки) просто катастрофически низка. Вы хотите видеть цифры? Их есть у меня. Но об этом позже. Ну а создать ручную бинарную сериализацию у парней из Microsoft, видимо, просто не хватило терпения. Хотя возможно, что сериализация в XML была приоритетом, поставленным менеджерами компании, а схема сериализации с Iserializable не позволяет получить информацию о том, в какого типа стриме происходит сериализация. Дело в том, что изначально предполагалось, что программист не будет сам производить сериализацию. Он должен был просто последовательно вызвать метод SerializationInfo.AddValue для всех полей класса. Но если попытаться как-то оптимизировать этот процесс, придется передавать в AddValue данные в определенном формате. Это и случилось с DataSet-ом. И так как приоритет был отдан XML и SOAP, мы получили то, что получили.
В общем, если верить мне, ручная сериализация DataSet-а может дать значительное повышение производительности.
Однако сериализация DataSet-а – не такая простая задача, как это может показаться поначалу. Дело в том, что DataSet – это комплексный объект, состоящий из кучи маленьких объектов. DataSet содержит коллекции:
Таблица в свою очередь содержит коллекции:
Почти каждый из этих классов содержит вложенные объекты и коллекции.
Самый хитрый из этих объектов – DataRow.
Дело в том, что строка может хранить две версии данных – Current и Original. Кроме того, хранимые в строке данные могут содержать значение DbNull, то есть NULL баз данных (это совсем не то же самое, что null).
Версии нужны, чтобы DataSet мог хранить информацию о модификации строки (удалении, добавлении, изменении), и чтобы после модификаций строка сохраняла информацию о предыдущем состоянии. Все это нужно, чтобы на базе информации, хранимой в DataTable, можно было сгенерировать SQL-скрипт, модифицирующий БД (например, чтобы можно было удалить строку из БД). DataSet должен не удалять ее из себя явно, а только помечать ее как удаленную. Реализуется это так. В DataRow хранится две переменных: oldRecord и newRecord. Удаленные строки содержат в oldRecord номер записи, а в newRecord – -1. Вставленные строки, наоборот, содержат в oldRecord -1, а в newRecord – номер записи. Модифицированные записи хранят в oldRecord номер старой записи, а в newRecord – новой. В неизмененных строках oldRecord и newRecord содержат один и тот же номер записи. Версия, хранимая в oldRecord, называется Original, а в newRecord – Current. oldRecord и newRecord – это скрытые члены класса, и легально получить доступ к ним невозможно. Но получить данные для заданной версии все же можно. Для этого используется перегруженный индексатор объекта DataRow. Его второй параметр может принимать значения DataRowVersion.Original или DataRowVersion.Current.
Узнать, какие версии доступны, можно с помощью метода:
bool HasVersion(DataRowVersion version);
|
Понимание внутренней структуры позволяет также стопроцентно угадать, какие версии есть в строке, при условии, что известно состояние строки (удалена, добавлена, изменена, неизменна). Получить состояние строки можно с помощью свойства RowState. Список доступных значений определяется перечислением DataRowState:
Added – строка добавлена. При этом присутствует только Current-версия строки (oldRecord = -1, а newRecord равна номеру записи, хранящей информацию о строке).
Deleted – строка удалена. При этом присутствует только Original-версия строки (oldRecord равна номеру записи, хранящей информацию об удаленной строке, а newRecord = -1).
Modified – строка изменена. При этом присутствует и Original-, и Current-версия.
Unchanged – строка не была изменена. Такое состояние может быть у строки после загрузки данных из БД или после применения к строке метода AcceptChanges.
Detached – строка не добавлена к DataTable. Это состояние нас не интересует, так как мы не будем сериализовать отдельные строки, а строки, подключенные к DataTable, не могут иметь этого состояния.
При записи строки в поток нужно записывать информацию о состоянии строки и версии строки. Напомню, несмотря на большое количество состояний строки, версий может быть только две. Причем только в случае измененной строки (DataRowState.Modified) действительно появляется необходимость сериализации обоих состояний.
Я создал упрощенный вариант сериализации DataSet-а. Он умеет сериализовать в бинарный формат таблицы DataSet-а. При сериализации учитываются ячейки, содержащие DbNull и версии строк, но не поддерживается сериализация ограничений (constrains), связей между таблицами (relations) и некоторых других аспектов. Этот сериализатор можно с успехом применить во многих приложениях, так как неподдерживаемые возможности используются на практике не так часто. К тому же несложно доделать сериализацию необходимых возможностей DataSet-а самостоятельно. Размер сериализованных данных при этом не должен серьезно увеличиться, так как все возможности, для которых я не реализовал сериализацию, являются чисто декларативными. Единственное, почему я не реализовал их сериализацию – это то, что на это нужно время, и не малое.
Время сериализации | Размер | Сжатие (быстрое) | Сжатие (нормальное) сохранение | загрузка | stream-а | время | размер | время | размер (сек.) | (сек.) | (байт) | (сек.) | (байт) | (сек.) | (байт) ------------+----------+------------+--------+-----------+--------+---------+ RsdnDataSerializer 0.2175 | 0.4357 | 1 320 126 | 0.0561 | 306 853 | 0.2343 | 255 201 0.2081 | 0.4338 | 1 320 126 | 0.0566 | 306 853 | 0.2320 | 255 201 0.1965 | 0.4359 | 1 320 126 | 0.0564 | 306 853 | 0.2343 | 255 201 BinaryFormatter 2.8173 | 9.1600 | 9 057 810 | 0.2146 | 900 317 | 0.4050 | 791 163 2.1798 | 9.0301 | 9 057 810 | 0.2159 | 900 317 | 0.4044 | 791 163 2.2008 | 9.0180 | 9 057 810 | 0.2154 | 900 317 | 0.4044 | 791 163 XmlSerializer 2.0541 | 9.3235 | 11 962 601 | 0.2615 | 919 852 | 0.4787 | 800 479 2.0009 | 9.3226 | 11 962 601 | 0.2598 | 919 852 | 0.4785 | 800 479 1.9373 | 9.3173 | 11 962 601 | 0.2599 | 919 852 | 0.4772 | 800 479 SoapFormatter 3.1591 | 11.2147 | 14 078 329 | 0.3069 | 1 081 327 | 0.5629 | 825 619 3.1357 | 11.2414 | 14 078 329 | 0.3071 | 1 081 327 | 0.5612 | 825 619 3.1420 | 11.2351 | 14 078 329 | 0.3086 | 1 081 327 | 0.5637 | 825 619 |
using System; using System.Data; using System.Xml; using System.Xml.Serialization; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization.Formatters.Soap; using RSDN; using Zlib; namespace SerializeTest { publicclass Test2 { publicstaticvoid Run(int InitRowCount) { Console.WriteLine("\nТест 2: Сериализация DataSet-а\n"); DataSet ds = new DataSet("MyDataSet"); DataTable dt = new DataTable("MyTable1"); // Добавляем 4 колонки две из которых (0 и 3) не допускают DbNull,// а две (1 и 2) допускают. dt.Columns.Add(new DataColumn("Col0", typeof(Int32))); dt.Columns[0].AllowDBNull = false; dt.Columns.Add(new DataColumn("Col1", typeof(string))); dt.Columns.Add(new DataColumn("Col2", typeof(DateTime))); dt.Columns.Add(new DataColumn("Col3", typeof(byte))); dt.Columns[3].AllowDBNull = false; DataRow dr; dt.BeginLoadData(); // Заполняем DataTable строками.for(int i = 0; i < InitRowCount; i++) { dr = dt.NewRow(); dr.BeginEdit(); dr[0] = i; // В каждой 100-й строке колонка 1 заполняется DBNull-ом. dr[1] = i % 100 == 0 ? DBNull.Value : (object)("2test" + i.ToString()); // В каждой 40-й строке колонка 2 заполняется DBNull-ом. dr[2] = i % 40 == 0 ? DBNull.Value : (object)DateTime.Now; dr[3] = (byte)i % 255; dt.Rows.Add(dr); if(i % 20 == 0) { // Каждая 20 строка помечается как отредактированная dr.AcceptChanges(); dr[0] = i; } elseif(i % 21 == 0) { // Каждая 21 строка помечается как удаленная dr.AcceptChanges(); dr.Delete(); } elseif(i % 22 != 0) // Все остальные строки кроме каждой 22 помечаются как // оригинальные (не модифицированные). Таким обрезом каждая 22 // строка остается помеченной как добавленная. dr.AcceptChanges(); dr.EndEdit(); } dt.EndLoadData(); ds.Tables.Add(dt); RsdnDataSerializer(ds); RsdnDataSerializer(ds); RsdnDataSerializer(ds); TestBinaryFormatter(ds); TestBinaryFormatter(ds); TestBinaryFormatter(ds); TestXmlSerializer(ds); TestXmlSerializer(ds); TestXmlSerializer(ds); TestSoapFormatter(ds); TestSoapFormatter(ds); TestSoapFormatter(ds); //// Это позволяет посмотреть получившийся DataSet.//frmDataset frm = new frmDataset();//frm.dataGrid1.DataSource = ds.Tables[0];//frm.ShowDialog(); Console.WriteLine("\nГотово!"); } // Шапка таблицы.conststring _serHeaderFormatString = " Время сериализации | Размер | Сжатие (быстрое) | Сжатие (нормальное)\n" + " сохранение | загрузка | stream-а | время | размер | время | размер\n" + " (сек.) | (сек.) | (байт) | (сек.) | (байт) | (сек.) | (байт)\n" + "------------+----------+------------+--------+-----------+--------+---------+"; // Определяет была ли отпечатана шапка. Нужно для подавления// повторной печати шапки.staticbool _headerNotPrinted = true; // Тип сериализации. Нужна для того, чтобы не выводить тип при // повторных тестах. Используется в функции PrintHeader.staticstring _serializationType; staticvoid PrintHeader(string testName) { if(_headerNotPrinted) { _headerNotPrinted = false; Console.WriteLine(_serHeaderFormatString); } if(_serializationType != testName) { _serializationType = testName; Console.WriteLine("\t\t\t" + _serializationType); } } // Строка формата для вывода данных о времени и размере сжатия.// 0 - время, 1 - размер.conststring _zipFormatString = "{0,5:##0.0000} | {1,9:### ### ###}"; staticvoid ZipData(MemoryStream ms) { byte[] aryZip; // Используется для вычисления времени выполнения. PerfCounter timer = new PerfCounter(); // Зипуем данные "выстрым" методом (BestSpeed) timer.Start(); aryZip = ZipBase.Compress(ms.ToArray(), ZipBase.Level.BestSpeed); Console.Write(_zipFormatString + " | ", timer.Finish(), aryZip.Length); // Зипуем данные стандартным методом (DefaultCompression) timer.Start(); aryZip = ZipBase.Compress(ms.ToArray(), ZipBase.Level.DefaultCompression); Console.Write(_zipFormatString + "\n", timer.Finish(), aryZip.Length); } // Строка формата для вывода данных о времени и размере сериализации.// 0 - время сериализации, 1 - время востановления, 2 - размер.conststring _serFormatString = "{0,11:### ### ##0.0000} | {1,8:##0.0000} | {2,10:### ### ###} | "; staticvoid RsdnDataSerializer(DataSet ds) { PrintHeader("RsdnDataSerializer"); // Используется для вычисления времени выполнения. PerfCounter timer = new PerfCounter(); Utils.ClearMem(); // Стрим для ручной сериализации через DataSetHandsSerializate. MemoryStream ms = new MemoryStream(1024); // Сериализация DataSetCustomSerializer2 timer.Start(); DataSerializer.SerializeDataSet(ms, ds); float timeSerialize = timer.Finish(); Utils.ClearMem(); // Загрузка DataSetCustomSerializer2 ms.Seek(0, SeekOrigin.Begin); timer.Start(); DataSet dsLoaded = DataSerializer.DeserializeDataSet(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); // dsLoaded.Tables[0].Rows[2][0] = 123; // проверка CmpDataSets// Проверяем правильность загруженного DataSet-а. CmpDataSets(dsLoaded, ds); ZipData(ms); // Зипуем данные } staticvoid TestBinaryFormatter(DataSet ds) { PrintHeader("BinaryFormatter"); PerfCounter timer = new PerfCounter(); Utils.ClearMem(); // Стрим для сериализации через BinaryFormatter MemoryStream ms = new MemoryStream(1024); BinaryFormatter bf = new BinaryFormatter(); // Сериализация BinaryFormatter timer.Start(); bf.Serialize(ms, ds); float timeSerialize = timer.Finish(); Utils.ClearMem(); // Загрузка BinaryFormatter ms.Seek(0, SeekOrigin.Begin); timer.Start(); DataSet dsLoaded = (DataSet)bf.Deserialize(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); CmpDataSets(dsLoaded, ds); ZipData(ms); // Зипуем данные } staticvoid TestXmlSerializer(DataSet ds) { PrintHeader("XmlSerializer"); PerfCounter timer = new PerfCounter(); Utils.ClearMem(); // Стрим для сериализации через XmlSerializer MemoryStream ms = new MemoryStream(1024); // Сериализация XmlSerializer timer.Start(); XmlSerializer serializer = new XmlSerializer(typeof(DataSet)); serializer.Serialize(ms, ds); float timeSerialize = timer.Finish(); Utils.ClearMem(); // Загрузка XmlSerializer ms.Seek(0, SeekOrigin.Begin); timer.Start(); DataSet dsLoaded = (DataSet)serializer.Deserialize(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); CmpDataSets(dsLoaded, ds); ZipData(ms); // Зипуем данные } staticvoid TestSoapFormatter(DataSet ds) { PrintHeader("SoapFormatter"); PerfCounter timer = new PerfCounter(); Utils.ClearMem(); // Стрим для сериализации через SoapFormatter MemoryStream ms = new MemoryStream(1024); SoapFormatter sf = new SoapFormatter(); // Сериализация SoapFormatter timer.Start(); sf.Serialize(ms, ds); float timeSerialize = timer.Finish(); Utils.ClearMem(); // Загрузка SoapFormatter ms.Seek(0, SeekOrigin.Begin); timer.Start(); DataSet dsLoaded = (DataSet)sf.Deserialize(ms); Console.Write(_serFormatString, timeSerialize, timer.Finish(), ms.Length); CmpDataSets(dsLoaded, ds); ZipData(ms); // Зипуем данные } ///////////////////////////////////////////////////////////////////////// Сервисные функции.// Сравнивает два DataTable. Нужно для проверки качества сериализации.// Это способ убедиться, что мой код делает то что нужно.staticbool CmpDataSets(DataSet ds1, DataSet ds2) { if(!CmpDataTables(ds1.Tables[0], ds2.Tables[0])) { Console.WriteLine("DataTable НЕ одинаковые!!!"); returnfalse; } returntrue; } // Тоже, что и CmpDataSets, но сравнивает две таблицы (DataTable).// Функция CmpDataSets использует эту функцию.staticbool CmpDataTables(DataTable dt1, DataTable dt2) { if(dt1.Rows.Count != dt1.Rows.Count) returnfalse; if(dt1.Columns.Count != dt1.Columns.Count) returnfalse; int iCols = dt1.Columns.Count; DataRowCollection rows1 = dt1.Rows; DataRowCollection rows2 = dt2.Rows; int iRows = rows1.Count; for(int r = 0; r < iRows; r++) { DataRow dr1 = rows1[r]; DataRow dr2 = rows2[r]; if(IsCurrentVer(dr1)) { if(!IsCurrentVer(dr2)) returnfalse; for(int i = 0; i < iCols; i++) { if(!dr1[i, DataRowVersion.Current].Equals( dr2[i, DataRowVersion.Current])) returnfalse; } } elseif(IsCurrentVer(dr2)) returnfalse; if(IsOriginalVer(dr1)) { if(!IsOriginalVer(dr2)) returnfalse; for(int i = 0; i < iCols; i++) { if(!dr1[i, DataRowVersion.Original].Equals( dr2[i, DataRowVersion.Original])) returnfalse; } } elseif(IsOriginalVer(dr2)) returnfalse; } returntrue; } // Возвращает true если в строке содержится версия "Original".// Используется в CmpDataTables.staticbool IsOriginalVer(DataRow dr) { DataRowState state = dr.RowState; switch(state) { // Original + Current и они равны!case DataRowState.Unchanged: returntrue; case DataRowState.Deleted: returntrue; case DataRowState.Added: returnfalse; case DataRowState.Modified: returntrue; default: thrownew ApplicationException( "Недопустимое состояние строки: " + state.ToString()); } } // Возвращает true если в строке содержится версия "Current".// Используется в CmpDataTables.staticbool IsCurrentVer(DataRow dr) { DataRowState state = dr.RowState; switch(state) { // Original + Current и они равны!case DataRowState.Unchanged: returntrue; case DataRowState.Deleted: returnfalse; case DataRowState.Added: returntrue; case DataRowState.Modified: returntrue; default: thrownew ApplicationException( "Недопустимое состояние строки: " + state.ToString()); } } } } |
Пользуясь случаем, передаю привет AVK и всем, кто считает что сериализация в .NET сделана нормально.