Серіалізація в

Автор: Владислав Чистяков The RSDN Group Джерело: RSDN Magazine #2-2003
P.S.
можна

Про проблему

Серіалізація масиву структур

Отже, я зробив тест, в якому створюється масив структур розміром сто тисяч елементів (ну, щоб було, що вимірювати) який серіалізується за допомогою:

  • Рукопашної бінарної серіалізації.
  • BinaryFormatter-а.
  • SoapFormatter-а.
  • XmlSerializer-а.

При цьому виводиться час серіалізації, завантаження даних та розмір отриманих даних. Я також спробував стиснути отриманий бінарний потік, так що тест до всього іншого виводить час, потрібний на стиснення даних, і розмір стиснених даних. Як архіватор були випробувані:

  • Реінкарнація стародавньої бібліотеки zlib, перекомпільованої на MC++ з використанням змішаного коду (сам код стиснення відкомпільовано в native-код, і створена managed-обгортка, що дозволяє використовувати цей код з .NET.
  • SharpZipLib – бібліотека, що підтримує відразу кілька форматів архівації, та повністю реалізована на C#.

Обидві бібліотеки можна знайти на журналі CD, що додається.

Скажу чесно, що zlib мені сподобалася набагато більше. По-перше, вона банально швидше працює. По-друге, вона підтримує типи компресії (швидка, звичайна, сильна), що дозволяє радикально скоротити час на архівацію при не дуже значній втраті ступеня стиснення. По-третє, zlib виявилася дуже простою у використанні. Її managed-обгортка проста, але функціональна і навіть витончена. Загалом, до остаточного варіанта тесту я включив лише результат zlib.

Усі тести повторювалися по три рази, щоб унеможливити впливініціалізації результати тесту.

Результати сміливого експерименту радянських вчених:

Скажу чесно, якщо говорити про розміри даних, то я думав про бінарний форматор дещо гірше. Бінарний форматер примудряється створити всього на чверть більш пухкий, ніж слід. Але за швидкістю форматери дотнета не витримують жодної критики. Різниця в десятки разів (близько 16/18 між бінарними та 50/73 (!) між SOAP та бінарними).

І це при тому, що серіалізацією по суті справи сам компілятор!

Стиснення дещо прикрашає сумну ситуацію з розміром серіалізованих даних, але навіть стислий файл на 30-60 відсотків більший за ідеальний. Плюс втрати часу на стиск. Адже серіалізувалися зовсім прості структури.

Ви тільки уявіть! За час серіалізації BinaryFormatter-ом можна встигнути не лише серіалізувати, а й стиснути дані, зменшивши їх у 3 рази!

SOAP-форматер за часом взагалі вбивчий.

Коментарі та висновки

Спочатку трохи розбавимо темні фарби. Все ж таки в цьому тесті серіалізувалися відносно великі обсяги даних. Зазвичай доводиться мати справу з даними в десять, а то й у сто разів меншими за обсягом. Враховуючи, що процесори нині більш-менш спритні, на невеликих обсягах можна стерпіти і 60-кратні гальма. Особливо якщо серіалізація робиться не часто. Однак на сервері, враховуючи, що кожен користувач множить обсяг інформації, що обробляється, це може стати серйозною проблемою. Причому, не знаючи наведених мною даних, швидше за все, гальма будуть списані на JIT-компіляцію та інші аспекти.

Ремоутинг та COM+. Саме їх дана проблема зачіпає насамперед. Справа в тому, що за маршалінгу дані серіалізуються. Отже час, витрачений цей процес, уповільнює роботуремоутингу. Роздутий результат серіалізації лише посилює ситуацію.

Однак ручна серіалізація - це додатковий код, а значить, і додатковий час, та й зрозуміло, додаткові помилки, а отже, знову час і нерви. Якщо врахувати, що стандартна серіалізація в. Якщо ж об'єкт, що серіалізується, містить посилання на MarshalByRefObject (передані за посиланням об'єкти), то серіалізація істотно ускладнюється, тому що доведеться залізти в досить низькорівневі речі, щоб забезпечити передачу посилання на об'єкт в інший процес (домен додатку, контекст).

Однак при передачі даних по мережі часто буває, що їх структура проста, самі вони однорідні, а ось обсяг даних, що передаються, великий. У таких випадках подумати про ручну серіалізацію буде зовсім не грішно. Але знову ж таки переконайтеся, що це дасть реальну вигоду.

Найкраще починати оптимізацію (а ручна серіалізація і є оптимізація) тільки якщо є проблеми з продуктивністю. При цьому варто в першу чергу постаратися реалізувати ручну серіалізацію для класів, які найчастіше використовуються, та/або використовуються для передачі значних обсягів даних.

Хто винен? І що робити?

Ну, винний відомо хто. Мокрософт. На таку важливу річ, як серіалізація, можна було б витратити більше зусиль та коштів.

А що робити? Дотне 1.1 не вирішує проблему. Чекати ж на нову версію дотнета ще дуже довго, та й швидше за все, проблема в ньому знову не буде вирішена. Єдине, що можеЗмусити Microsoft зайнятися серіалізацією - це якийсь конкурент. Наприклад, Sun може заявити, що Ява серіалізує об'єкти у ХХХ разів швидше. І тоді буквально через півроку серіалізація в .NET стане крутішою за паровоз. Однак віриться в це важко. Тож треба брати все у свої руки.

Шляхи. В першу чергу можна написати ручну серіалізацію для об'єктів, що найбільш часто використовуються, що зберігають/передають великі обсяги даних. DataSet просто напрошується на те, щоб першим поглумилися з нього. Точніше не над ним, а на одній з його складових – DataTable. Адже саме цей клас зберігає реальні дані.

Та й останнє питання. Чи варто ламати списи? Шістдесятикратна різниця у швидкості може переконати будь-кого. Особливо якщо користувачі вже починають підвивати. Наприклад, схожа проблема назріває у rsdn@home. Декілька хоумщиків, які одночасно обирають повідомлення, можуть серйозно і досить надовго загальмувати сервер. Адже страждають від цього в основному онлайн-користувачі. Адже хоумщики тим часом п'ють чай.

Вихідні коди тесту

Серіалізація DataSet-а

Реалізувати ручну серіалізацію для класів прикладної програми – заняття невдячне. Якщо дотримуватись стратегії отримання максимальної віддачі при мінімальному вкладі, потрібно створити ручну серіалізацію для об'єктів, які найчастіше використовуються при передачі даних по мережі. DataSet і є таким об'єктом. Він універсальний, отже, з його допомогою можна передавати різні дані. Причому код самого DataSet при цьому змінювати не потрібно (та й як, власне, це зробити?!). Ваше бажання, гадаю, посилиться, якщо я скажу, що DataSet принципово не вміє серіалізуватися у бінарну форму. Тобто він чудово може бути збережений коштамиBinaryFormatter, але при цьому в потік буде збережено XML-подання (просто розмічений текст) DataSet-а. Зроблено це так. Для DataSet реалізована ручна серіалізація. При цьому в методі ISerializable.GetObjectData додаються два значення, KEY_XMLSCHEMA і KEY_XMLDIFFGRAM, що містять (як не важко здогадатися за їх іменами) XML-опис і вміст DataSet-а. Ось код цього методу об'єкта DataSet:

Приблизно такий код містить метод GetObjectData об'єкта DataTable.

Ух ти! - подумає багато хто. А навіщо це зроблено? Очевидно, панове з Microsoft змушені були робити ручну серіалізацію для DataSet-а, оскільки настільки складний об'єкт, як DataSet дуже важко коректно зберегти автоматично. А можливо, вони просто намагалися покращити швидкість серіалізації. Як би там не було, але вийшло все це у них (як говорилося в одному анекдоті) спокійно. Мало того, що обсяг серіалізації навіть при серіалізації BinaryFormatter не лізе ні в які ворота. До того ж і швидкість серіалізації (а особливо завантаження) просто катастрофічно низька. Ви бажаєте бачити цифри? Їх маю. Але про це згодом. Ну а створити ручну бінарну серіалізацію у хлопців із Microsoft, мабуть, просто не вистачило терпіння. Хоча можливо, що серіалізація в XML була пріоритетом, поставленим менеджерами компанії, а схема серіалізації з Iserializable не дозволяє отримати інформацію про те, в якому стрімі відбувається серіалізація. Справа в тому, що спочатку передбачалося, що програміст не сам проводитиме серіалізацію. Він повинен був просто послідовно викликати метод SerializationInfo.AddValue для всіх класових полів. Але якщо спробувати оптимізувати цей процес, доведеться передавати в AddValue дані в певному форматі. Це ітрапилося з DataSet-ом. І тому що пріоритет був відданий XML і SOAP, ми отримали те, що отримали.

Загалом, якщо мені вірити, ручна серіалізація DataSet-а може дати значне підвищення продуктивності.

Однак серіалізація DataSet-а - не таке просте завдання, як це може здатися спочатку. Справа в тому, що DataSet - це комплексний об'єкт, що складається з купи невеликих об'єктів. DataSet містить колекції:

  • Таблиць (DataTable)
  • Реляційні зв'язки (DataRelations).

Таблиця у свою чергу містить колекції:

  • Колонок (DataColumn).
  • Рядок (DataRow).
  • індексів.
  • Обмежень цілісності (DataRelations).
  • Дочірніх та батьківських зв'язків (DataRelation)
  • Обмежень цілісності (Constraint).

Майже кожен із цих класів містить вкладені об'єкти та колекції.

Найхитріший із цих об'єктів – 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.

Дізнатися, які версії доступні можна за допомогою методу:

Розуміння внутрішньої структури дозволяє також повністю вгадати, які версії є в рядку, за умови, що відомий стан рядка (віддалений, доданий, змінений, незмінний). Отримати стан рядка можна за допомогою 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 самостійно. Розмір серіалізованих даних при цьому не повинен серйозно збільшитись, оскільки всі можливості, для яких я не реалізував серіалізацію, є чисто декларативними. Єдине, чому я не реалізував їхню серіалізацію – це те, що на це потрібен час, і чималий.

Результати тесту

Вихідні коди

Користуючись нагодою, передаю привіт AVK і всім, хто вважає, що серіалізація в .NET зроблена нормально.