Сериализация в дотнете 2 - самопальный датасет
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.04.03 02:39
Оценка: 40 (4)
Итак, убил 5 часа на переписывания этого дела
Автор: Trantor
Дата: 15.04.03
под бинарный вид... отладку и тесты...

Результаты показались настолько интересными, что решил создать новую тему.

На всякий пожарный сделал проверку качества сериализации путем загрузки данных в другой DataSet и сравнения с исходным. Вариант Trantor-а не прошел проверку. Оказалось, что дата записанная в стринг по средствам ToString() и считанная обратно теряет точность. Так что вариант со строковой сериализацией нужно еще дописывать. Если вообще нужна строковая сериализация. Ведь разница в скорости почти 4 раза, и в размерах данных примерно в 1.7 раза.

За одно измерил время загрузки. Результаты оказались дико интересным! Гляньте на результаты BinaryFormatter-а! Загрузка почти в два раза медленнее чем у любого другого. А по сравнению сDetaSetCustomSerializer2 разница в скорости в 55 раз! И это МС продает как прорыв в работе с данными! По убывал бы. Причем вообще не ясно, что делает этот BinaryFormatter, так как известно, что DataSet всегда сериализуется в XML.

Скептики вроде DG должны заметить, что датасет не нуждается в универсальном сохранении графа объектов. Максимум что ему нужно сохранить свои таблицы... И это прекрасно делается с помощью ручной сериализации.

Попутно обнаружил серьезный недостаток. Дело в том, что Trantor не рассчитывал на то, что ячейки могут содержать DbNull. Мой вариант пока, что тоже не учитывать DbNull, но завтра допишу его шоб поддерживал. Запас по скорости таки есть.

Так же добавил в тесте сборку мусора (двойную, с ожиданием). Чтобы вообще не было намека на наложенку.

Традиционно вначале результаты (мой вариант называется DetaSetCustomSerializer2):
  DataSetHandsSerializate
Serialization time elapsed:        381
Length is:                   1 600 338
Loading time elapsed:            1 171

  DetaSetCustomSerializer2
Serialization time elapsed:        100  !!!
Length is:                     948 955  !!!
Loading time elapsed:              290  !!!
DataTable одинаковые!

  BinaryFormatter
Serialization time elapsed:      3 715
Length is:                   7 819 323 !!!! Это все потому, что реально в Датасете хранится XML!
Loading time elapsed:           16 103 !!!!
DataTable одинаковые!

  XmlSerializer
Serialization time elapsed:      1 833
Length is:                  10 059 102
Loading time elapsed:            7 371
DataTable одинаковые!

  SoapFormatter
Serialization time elapsed:      2 854
Length is:                  11 980 266
Loading time elapsed:            9 063
DataTable одинаковые!


Собственно, коментарии излишни.

Код улучшенного ручного варианта сериализации DetaSet-а (основан на коде Trantor-а
Автор: Trantor
Дата: 15.04.03
):
using System;
using System.IO;
using System.Data;

namespace SerializeTest
{
    class DetaSetCustomSerializer2
    {
        // Массив для перемапливания TypeCode на Type
        // TypeCode это перечисление включающее константы для базовых типов.
        static Type[] _TypeMap;

        // Статический констрктор. Нужен для инициализации _TypeMap.
        static DetaSetCustomSerializer2()
        {
            // TypeCode.DBNull и TypeCode.Empty пропущены, так как они 
            // по сути не являются типами.
            _TypeMap = new Type[(int)TypeCode.String + 1];
            _TypeMap[(int)TypeCode.Object] = typeof(Object);
            _TypeMap[(int)TypeCode.Boolean] = typeof(Boolean);
            _TypeMap[(int)TypeCode.Char] = typeof(Char);
            _TypeMap[(int)TypeCode.SByte] = typeof(SByte);
            _TypeMap[(int)TypeCode.Byte] = typeof(Byte);
            _TypeMap[(int)TypeCode.Int16] = typeof(Int16);
            _TypeMap[(int)TypeCode.UInt16] = typeof(UInt16);
            _TypeMap[(int)TypeCode.Int32] = typeof(Int32);
            _TypeMap[(int)TypeCode.UInt32] = typeof(UInt32);
            _TypeMap[(int)TypeCode.Int64] = typeof(Int64);
            _TypeMap[(int)TypeCode.UInt64] = typeof(UInt64);
            _TypeMap[(int)TypeCode.Single] = typeof(Single);
            _TypeMap[(int)TypeCode.Double] = typeof(Double);
            _TypeMap[(int)TypeCode.Decimal] = typeof(Decimal);
            _TypeMap[(int)TypeCode.DateTime] = typeof(DateTime);
            _TypeMap[(int)TypeCode.String] = typeof(String);
        }

        public static void Serialize(Stream stream, DataSet ds)
        {
            BinaryWriter bw = new BinaryWriter(stream);
            DataTableCollection tables = ds.Tables;

            bw.Write(ds.DataSetName);
            bw.Write(tables.Count);
            foreach(DataTable dt in tables)
            {    // Вообще-то foreach-и лучше на всякий пожанный избегать. Но мне было в лом.
                DataColumnCollection columns = dt.Columns;
                int iColCount = columns.Count;
                TypeCode[] colTypeCodes = new TypeCode[iColCount];
                //table name
                bw.Write(dt.TableName);
                //count columns
                bw.Write(iColCount);
                for(int i = 0; i < iColCount; i++)
                {
                    DataColumn dc = columns[i];
                    // Получаем TypeCode для типа обрабатываемой колонки.
                    TypeCode tc    = Type.GetTypeCode(dc.DataType);
                    // Запоминаем TypeCode колонки в соотвествующем массиве.
                    colTypeCodes[i] = tc;
                    bw.Write(dc.ColumnName);
                    // Записываем TypeCode как Int32. Можно было бы и сэкономить 3 байта. :)
                    bw.Write((Int32)tc);
                    // TODO: Нужно добавть проверку на DbNull !!!
                }

                ///////////////////////////////////////////////////////////////
                // add data

                // count rows
                bw.Write(dt.Rows.Count);
                foreach(DataRow dr in dt.Rows)
                {
                    for(int i = 0; i < iColCount; i++)
                    {
                        // Записываем данные ячейки.
                        // TODO: Нужно добавть проверку на DbNull !!!
                        switch(colTypeCodes[i]) 
                        {    // Каждому типу соотвествует переопределенная фунция...
                            case TypeCode.Boolean: bw.Write((Boolean)dr[i]); break;
                            case TypeCode.Char: bw.Write((Char)dr[i]); break;
                            case TypeCode.SByte: bw.Write((SByte)dr[i]); break;
                            case TypeCode.Byte: bw.Write((Byte)dr[i]); break;
                            case TypeCode.Int16: bw.Write((Int16)dr[i]); break;
                            case TypeCode.UInt16: bw.Write((UInt16)dr[i]); break;
                            case TypeCode.Int32: bw.Write((Int32)dr[i]); break;
                            case TypeCode.UInt32: bw.Write((UInt32)dr[i]); break;
                            case TypeCode.Int64: bw.Write((Int64)dr[i]); break;
                            case TypeCode.UInt64: bw.Write((UInt64)dr[i]); break;
                            case TypeCode.Single: bw.Write((Single)dr[i]); break;
                            case TypeCode.Double: bw.Write((Double)dr[i]); break;
                            case TypeCode.Decimal: bw.Write((Decimal)dr[i]); break;
                            case TypeCode.DateTime:
                                // Для DateTime приходится выпендриваться особым образом.
                                bw.Write(((DateTime)(dr[i])).ToFileTime());
                                break;
                            case TypeCode.String: bw.Write((String)dr[i]); break;
                            default:
                                // На всякий случай пробуем записать неопознанный тип
                                // виде строки.
                                bw.Write(dr[i].ToString());
                                break;
                        }
                    }
                }
            }
        }

        public static DataSet Deserialize( Stream stream )
        {
            BinaryReader br = new BinaryReader(stream);
            // Считываем имя датасета и создаем его...
            DataSet ds = new DataSet(br.ReadString());
            
            int counTables = br.ReadInt32();

            DataTable dt;
            DataColumn dc;
            DataRow dr;
            for(int t = 0; t < counTables; t++)
            {
                // Имя дататэйбла тоже передается в качестве параметра конструктора.
                dt = new DataTable(br.ReadString());
                int iColCount = br.ReadInt32();
                // В этом массив будут записаны TypeCode-ы для колонок DataTable-а.
                TypeCode[] colTypeCodes = new TypeCode[iColCount];
                // А в этот массив будут записаны типы (Type) колонок DataTable-а
                // соотвествующие TypeCode-ам.
                Type[] colTypes = new Type[iColCount];
                for(int c = 0; c < iColCount; c++)
                {
                    string colName = br.ReadString();
                    // Считываем TypeCode.
                    TypeCode tc = (TypeCode)br.ReadInt32();
                    // Помещаем TypeCode в массив для дальнейшего использования.
                    colTypeCodes[c] = tc;
                    // Получаем (через мап) тип соответсвующий TypeCode-у.
                    Type type = _TypeMap[(int)tc];
                    // Создаем колонку с полученым именем и типом.
                    dc = new DataColumn(colName, type);
                    dt.Columns.Add(dc);
                }

                int counRows = br.ReadInt32();
                for(int r = 0; r < counRows; r++)
                {
                    // Считываем данные.
                    dr = dt.NewRow();
                    for(int i = 0; i < iColCount; i++)
                    {
                        // TODO: Здесь нужно бы обрабатывать DbNull !!!
                        switch(colTypeCodes[i])
                        {
                            case TypeCode.Boolean: dr[i] = br.ReadBoolean(); break;
                            case TypeCode.Char: dr[i] = br.ReadChar(); break;
                            case TypeCode.SByte: dr[i] = br.ReadSByte(); break;
                            case TypeCode.Byte: dr[i] = br.ReadByte(); break;
                            case TypeCode.Int16: dr[i] = br.ReadInt16(); break;
                            case TypeCode.UInt16: dr[i] = br.ReadUInt16(); break;
                            case TypeCode.Int32: dr[i] = br.ReadInt32(); break;
                            case TypeCode.UInt32: dr[i] = br.ReadUInt32(); break;
                            case TypeCode.Int64: dr[i] = br.ReadInt64(); break;
                            case TypeCode.UInt64: dr[i] = br.ReadUInt64(); break;
                            case TypeCode.Single: dr[i] = br.ReadSingle(); break;
                            case TypeCode.Double: dr[i] = br.ReadDouble(); break;
                            case TypeCode.Decimal: dr[i] = br.ReadDecimal(); break;
                            case TypeCode.DateTime:
                                dr[i] = DateTime.FromFileTime(br.ReadInt64());
                                break;
                            case TypeCode.String: dr[i] = br.ReadString(); break;
                            default:
                                dr[i] = Convert.ChangeType(br.ReadString(), 
                                    colTypeCodes[i]);
                                break;
                        }
                    }
                    dt.Rows.Add(dr);
                }
                ds.Tables.Add(dt);
            }
            return ds;
        }
    }
}


Тестовый код:
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;

namespace SerializeTest
{
    class Class1
    {
        // Сравнивает два DataTable
        static bool CmpDataTables(DataTable dt1, DataTable dt2)
        {
            if(dt1.Rows.Count != dt1.Rows.Count)
                return false;
            if(dt1.Columns.Count != dt1.Columns.Count)
                return false;

            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];
                for(int i = 0; i < iCols; i++)
                {
                    if(!dr1[i].Equals(dr2[i]))
                        return false;
                }
            }
            Console.WriteLine("DataTable одинаковые!");
            return true;
        }

        // Вызывает суровую уборку мусора. :)
        static void ClearMem()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        [STAThread]
        static void Main(string[] args)
        {
            int start; // Используется для вычисления времени выполнения

            DataSet ds = new DataSet("myDataSet");
            DataTable dt = new DataTable("Table1");
            dt.Columns.Add(new DataColumn("col0", typeof(Int32)));
            dt.Columns.Add(new DataColumn("col1", typeof(string)));
            dt.Columns.Add(new DataColumn("col2", typeof(DateTime)));
            dt.Columns.Add(new DataColumn("col3", typeof(byte)));

            DataRow dr;

            for (int i = 0; i < 40000; i++)
            {
                dr = dt.NewRow();
                dr[0] = i;
                dr[1] = "2test" + i.ToString();
                dr[2] = DateTime.Now;
                dr[3] = (byte)i % 255;
                dt.Rows.Add(dr);        
            }

            ds.Tables.Add(dt);


            // Стрим для сериализации через BinaryFormatter
            MemoryStream msBf = new MemoryStream(1024);
            // Стрим для ручной сериализации через DataSetHandsSerializate.
            MemoryStream msBm1 = new MemoryStream(1024);
            // Стрим для ручной сериализации через DetaSetCustomSerializer2.
            MemoryStream msBm2 = new MemoryStream(1024);
            // Стрим для сериализации через SoapFormatter
            MemoryStream msSf = new MemoryStream(1024);
            // Стрим для сериализации через XmlSerializer
            MemoryStream msXml = new MemoryStream(1024);

            BinaryFormatter bf = new BinaryFormatter();
            SoapFormatter sf = new SoapFormatter();

            DataSet dsLoaded;

            ClearMem(); // Вызов GC чтобы небыло налаженки...

            // Сериализация DataSetHandsSerializate
            start = Environment.TickCount;
            DataSetHandsSerializate.Serialize(msBm1, ds);
            Console.WriteLine(
                "\n  DataSetHandsSerializate\n"
                + "Serialization time elapsed: {0,10:### ### ###}\n"
                + "Length is:                  {1,10:### ### ###}",
                Environment.TickCount - start, msBm1.Length);

            // Загрузка DataSetHandsSerializate
            msBm1.Seek(0, SeekOrigin.Begin);
            start = Environment.TickCount;
            dsLoaded = DataSetHandsSerializate.Deserialize(msBm1);
            Console.WriteLine("Loading time elapsed:       {0,10:### ### ###}",
                Environment.TickCount - start);
            CmpDataTables(dsLoaded.Tables[0], ds.Tables[0]);

            ClearMem();

            // Сериализация DetaSetCustomSerializer2
            start = Environment.TickCount;
            DetaSetCustomSerializer2.Serialize(msBm2, ds);
            Console.WriteLine(
                "\n  DetaSetCustomSerializer2\n"
                + "Serialization time elapsed: {0,10:### ### ###}\n"
                + "Length is:                  {1,10:### ### ###}",
                Environment.TickCount - start,msBm2.Length);

            // Загрузка DetaSetCustomSerializer2
            msBm2.Seek(0, SeekOrigin.Begin);
            start = Environment.TickCount;
            dsLoaded = DetaSetCustomSerializer2.Deserialize(msBm2);
            Console.WriteLine("Loading time elapsed:       {0,10:### ### ###}",
                Environment.TickCount - start);
            CmpDataTables(dsLoaded.Tables[0], ds.Tables[0]);

            ClearMem();

            // Сериализация BinaryFormatter
            start = Environment.TickCount;
            bf.Serialize(msBf, ds);
            Console.WriteLine(
                "\n  BinaryFormatter\n"
                + "Serialization time elapsed: {0,10:### ### ###}\n"
                + "Length is:                  {1,10:### ### ###}",
                Environment.TickCount - start,msBf.Length);

            // Загрузка BinaryFormatter
            msBf.Seek(0, SeekOrigin.Begin);
            start = Environment.TickCount;
            dsLoaded = (DataSet)bf.Deserialize(msBf);
            Console.WriteLine("Loading time elapsed:       {0,10:### ### ###}",
                Environment.TickCount - start);
            CmpDataTables(dsLoaded.Tables[0], ds.Tables[0]);

            ClearMem();

            // Сериализация XmlSerializer
            start = Environment.TickCount;
            XmlSerializer serializer = 
                new XmlSerializer(typeof(DataSet));
            serializer.Serialize(msXml, ds);
            Console.WriteLine(
                "\n  XmlSerializer\n"
                + "Serialization time elapsed: {0,10:### ### ###}\n"
                + "Length is:                  {1,10:### ### ###}",
                Environment.TickCount - start, msXml.Length);

            // Загрузка XmlSerializer
            msXml.Seek(0, SeekOrigin.Begin);
            start = Environment.TickCount;
            dsLoaded = (DataSet)serializer.Deserialize(msXml);
            Console.WriteLine("Loading time elapsed:       {0,10:### ### ###}",
                Environment.TickCount - start);
            CmpDataTables(dsLoaded.Tables[0], ds.Tables[0]);

            ClearMem();

            // Сериализация SoapFormatter
            start = Environment.TickCount;
            sf.Serialize(msSf, ds);
            Console.WriteLine(
                "\n  SoapFormatter\n"
                + "Serialization time elapsed: {0,10:### ### ###}\n"
                + "Length is:                  {1,10:### ### ###}",
                Environment.TickCount - start, msSf.Length);

            // Загрузка SoapFormatter
            msSf.Seek(0, SeekOrigin.Begin);
            start = Environment.TickCount;
            dsLoaded = (DataSet)sf.Deserialize(msSf);
            Console.WriteLine("Loading time elapsed:       {0,10:### ### ###}",
                Environment.TickCount - start);
            CmpDataTables(dsLoaded.Tables[0], ds.Tables[0]);

             Console.ReadLine();
        }
    }
}
... << RSDN@Home 1.0 beta 6a >>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.