Сериализация в дотнете
От: VladD2 Российская Империя www.nemerle.org
Дата: 10.04.03 00:36
Оценка: 292 (23) +1
Всем привет!

Я уже на раз говорил, о том что сериализация в дотнете сделана очень не оптимально. Данная тема является научным доказательством правоты моих слов. Итак, я сделал тест в котором создается массив структур размеров в сто тысяч элементов (ну, шоб було чё мерить) который сиреализуется с помощью:
* 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):
' Class1
Option 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 >>
http://nemerle.org/Banners/?g=dark
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.