Розуміння серіалізації


38

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

Проблема у мене полягає в тому, чи не всі змінні (будь то примітиви, як intскладові об'єкти) вже представлені послідовністю байтів? (Звичайно, вони є, тому що вони зберігаються в регістрах, пам'яті, диску тощо)

То що робить серіалізацію такою глибокою темою? Щоб серіалізувати змінну, чи не можемо ми просто взяти ці байти в пам'ять і записати їх у файл? Які тонкощі я пропустив?


21
Серіалізація може бути тривіальною для суміжних об'єктів. Коли значення об'єкта представлено у вигляді вказівного графіка , речі стають набагато складнішими, особливо якщо зазначений графік має петлі.
чі

1
@chi: Ваше перше речення трохи вводить в оману, оскільки суміжність не має значення. У вас може бути графік, який має постійну пам’ять і який все-таки не допоможе вам його серіалізувати, оскільки вам все-таки доведеться (а) виявити, що воно дійсно є суміжним, і (б) встановити покажчики всередині. Я просто сказав би другу частину сказаного вами.
Мехрдад

@Mehrdad Я погоджуюся, що мій коментар не зовсім точний, з тих причин, які ви згадуєте. Можливо, без вказівника / використання вказівника є кращою відмінністю (навіть якщо це не зовсім точно)
chi

7
Ви також повинні турбуватися про представлення на апаратному забезпеченні. Якщо я серіалізую int 4 bytesна моєму PDP-11, а потім спробую прочитати ці самі чотири байти в моїй книзі macbook, це не однакове число (через Endianes) Отже, вам доведеться нормалізувати дані до представлення, яке ви можете декодувати (це серіалізація). Під час серіалізації даних також можна легко читати швидкість / гнучкість, що читаються людиною та машиною.
Мартін Йорк

Що робити, якщо ви використовуєте Entity Framework з багатьма глибоко пов'язаними навігаційними властивостями? В одному випадку ви, можливо, захочете серіалізувати властивість навігації, але в іншому залиште його недійсним (оскільки ви повторно завантажите цей фактичний об’єкт із бази даних на основі ідентифікатора, який є у вашому серіалізованому батьківському об'єкті). Це лише один приклад. Тут багато.
ЕрікЕ

Відповіді:


40

Якщо у вас складна структура даних, її представлення в пам'яті, як правило, може бути розпорошене по пам'яті. (Наприклад, подумайте про двійкове дерево).

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


27

Проблема у мене полягає в тому, чи не всі змінні (будь то примітиви на зразок int чи складених об'єктів) вже представлені послідовністю байтів? (Звичайно, вони є, тому що вони зберігаються в регістрах, пам'яті, диску тощо)

То що робить серіалізацію такою глибокою темою? Щоб серіалізувати змінну, чи не можемо ми просто взяти ці байти в пам'ять і записати їх у файл? Які тонкощі я пропустив?

Розглянемо об’єктний графік на C з вузлами, визначеними так:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

Під час виконання весь Nodeграфік об'єкта буде розсіяний по простору пам’яті, і той самий вузол міг би вказуватися на багато різних Вузлів.

Ви не можете просто скинути пам'ять на файл / потік / диск і назвати його серіалізованим, оскільки значення вказівника (які є адресами пам'яті) не можна було десериалізувати (оскільки ці місця пам'яті можуть бути вже зайняті під час завантаження дампа назад в пам'ять). Ще одна проблема простого демпінгу пам’яті полягає в тому, що ви в кінцевому підсумку зберігаєте всілякі невідповідні дані та невикористаний простір - на x86 процес має до 4 Гбіт простору пам’яті, а ОС або MMU має лише загальне уявлення про те, що саме є пам'ять змістовно чи ні (на основі сторінок пам'яті, призначених для процесу), тому завантаження Notepad.exe4 Гб необроблених байтів на мій диск, коли я хочу зберегти текстовий файл, здається трохи марним.

Інша проблема полягає у версії версії: що трапиться, якщо ви серіалізуєте свій Nodeграфік у перший день, потім на 2 день ви додасте ще одне поле Node(наприклад, інше значення вказівника чи примітивне значення), а потім на 3 день ви будете де-серіалізувати файл із день 1?

Ви також повинні враховувати інші речі, наприклад, витривалість. Однією з головних причин, через які файли MacOS та IBM / Windows / PC були несумісні між собою у 1980-х та 1990-х роках, не дивлячись на те, що вони створювалися тими ж програмами (Word, Photoshop тощо), полягали в тому, що для багатобайтових цілочисельних значень x86 / PC були збережені в малопомітному порядку, але замовлення з великим ендіанством на Mac - і програмне забезпечення не було побудовано з урахуванням перехресності платформ. На сьогоднішній день все покращується завдяки покращеній освіті розробників та у нашому все більш неоднорідному світі обчислень.


2
Демпінг всього в просторі пам’яті процесу також буде жахливим з міркувань безпеки. Програмна ніч має в пам'яті як 1) деякі загальнодоступні дані, так і 2) пароль, секретне повідомлення або приватний ключ. Під час серіалізації першого не хочеться розкривати жодної інформації про останню.
чі

8
Дуже цікава примітка до цієї теми: Чому формати файлів Microsoft Office настільки складні?
б'ючи

15

Хитрість насправді вже описана в самому слові: " серійна ізація".

Питання в основному: як я можу представити довільно складний взаємопов'язаний циклічний спрямований графік довільно складних об'єктів як лінійну послідовність байтів?

Подумайте про це: лінійна послідовність є подібною до виродженого спрямованого графа, де кожна вершина має рівно один вхідний та вихідний край (за винятком "першої вершини", яка не має вхідного краю, та "останньої вершини", яка не має вихідного краю) . А байт , очевидно, менш складний, ніж об’єкт .

Отже, здається розумним, що, коли ми переходимо від довільно складного графа до значно більш обмеженого "графа" (насправді просто списку) і від довільно складних об'єктів до простих байтів, інформація буде втрачена, якщо ми зробимо це наївно і не зробимо " t певним чином кодувати "сторонні" відомості. І саме це робить серіалізація: кодування складної інформації у простий лінійний формат.

Якщо ви знайомі з YAML , ви можете ознайомитися з функціями якоря та псевдоніма, які дозволяють представити думку про те, що "один і той же об'єкт може з'являтися в різних місцях" при серіалізації.

Наприклад, якщо у вас є такий графік:

A → B → D
↓       ↑
C ––––––+

Ви можете представити це як список лінійних контурів у YAML так:

- [&A A, B, &D D]
- [*A, C, *D]

Ви можете також представити його як список суміжності, або матрицю примикання, або як пару, першим елементом якого є набір вузлів і чий другий елемент є набором пар вузлів, але у всіх цих поданнях вам потрібно мати спосіб посилання назад і вперед до існуючих вузлів, тобто покажчиків , яких у вас зазвичай немає у файлі чи мережевому потоці. Зрештою, у вас є байти.

(Що означає BTW означає, що сам вищезазначений текстовий файл YAML також потребує "серіалізації", саме для цього використовуються різні кодування символів та формати передачі Unicode ... це не суворо "серіалізація", а просто кодування, оскільки текстовий файл вже є послідовним / лінійний список кодових точок, але ви можете побачити деякі подібності.)


13

Інші відповіді вже стосуються складних графіків об'єктів, але варто зазначити, що серіалізація примітивів також нетривіальна.

Використовуючи назви примітивних типів C для конкретності, врахуйте:

  1. Я серіалізую a long. Деякий час пізніше я де-серіалізую його, але ... на іншій платформі, і зараз longце int64_tшвидше, ніж int32_tя зберігав. Отже, мені потрібно бути дуже обережним щодо точного розміру кожного типу, який я зберігаю, або зберігати деякі метадані, що описують тип і розмір кожного поля.

    Зауважте, що ця інша платформа може бути просто тією ж платформою після майбутньої перекомпіляції.

  2. Я серіалізую int32_t. Деякий час пізніше я де-серіалізую його, але ... на іншій платформі, і тепер значення пошкоджене. На жаль, я врятував цінність на платформі big-endian і завантажив її на маленьку-ендіанську. Тепер мені потрібно встановити конвенцію для свого формату або додати більше метаданих, що описують небезпеку кожного файлу / потоку / будь-якого іншого. І, звичайно, фактично виконувати відповідні перетворення.

  3. Я серіалізую рядок. Цього разу одна платформа використовує charі UTF-8, а одна wchar_tі UTF-16.

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

Об'єктні графіки додають ще один складний рівень.


6

Є кілька аспектів:

Читання за тією ж програмою

Ваша програма якось зберігає ваші дані як байти в пам'яті. Але це може бути довільно розсіяно по різних регістрів, вказівники будуть повертатися вперед і назад між його меншими фрагментами [редагувати: Як коментується, фізично дані є більш імовірними в основній пам'яті, ніж регістри даних, але це не знімає проблеми вказівника] . Просто придумайте пов'язаний цілий список. Кожен елемент списку може зберігатися в абсолютно іншому місці, і все, що містить цей список, є покажчиками від одного елемента до іншого. Якби ви взяли ці дані такими, які є, і спробували скопіювати їх на іншій машині, що працює з тією ж програмою, у вас виникнуть проблеми:

  1. Перш за все, реєстр повідомляє, що ваші дані зберігаються на одній машині, можливо, вже використовуються для чогось зовсім іншого на іншій машині (хтось переглядає обмін стеками, і браузер уже з’їв всю цю пам'ять). Тож якщо ви просто перекриєте ці регістри, до побачення браузер. Таким чином, вам потрібно буде переставити вказівники в структурі, щоб вони вміщували вільні адреси на другій машині. Ця ж проблема виникає, коли ви намагаєтеся пізніше завантажити дані на той же апарат.
  2. Що робити, якщо якісь зовнішні компоненти компонентів у вашій структурі або ваша структура має вказівники на зовнішні дані, ви не передавали? Segfaults скрізь! Це стане кошмаром налагодження.

Читання іншою програмою

Скажімо, вам вдалося виділити лише правильні адреси на іншій машині, щоб ваші дані вміщувалися. Якщо ваші дані обробляються окремою програмою на цій машині (різною мовою), ця програма може мати зовсім інше базове розуміння даних. Скажімо, у вас є об'єкти C ++ з покажчиками, але ваша цільова мова навіть не підтримує покажчики на цьому рівні. Знову ж таки, у вас немає чистого способу вирішення цих даних у другій програмі. У кінцевому підсумку у пам'яті є деякі двійкові дані, але потім вам потрібно написати додатковий код, який обертається навколо даних і якось переводить їх у те, з чим може працювати ваша цільова мова. Здається, що дезаріалізація, просто те, що зараз ваша відправна точка - це дивний об'єкт, розкиданий навколо вашої основної пам'яті, який відрізняється від різних мов джерел, замість файлу з чітко визначеною структурою. Те саме, звичайно, якщо ви намагаєтеся безпосередньо інтерпретувати двійковий файл, що включає вказівники, - вам потрібно написати парсери для всіх можливих способів, якими інша мова може представляти дані в пам'яті.

Зрозумілість людиною

Дві найвизначніші сучасні мови серіалізації для веб-серіалізації (xml, json) людиною легко зрозуміти. Замість двійкової купи goo фактична структура та зміст даних зрозумілі навіть без програми для зчитування даних. Це має ряд переваг:

  • спрощення налагодження -> якщо у вашому сервісному трубопроводі є проблеми, ви просто перегляньте дані, які виходять з однієї служби та перевіряють, чи є це сенс (як перший крок); Ви також безпосередньо бачите, чи дані виглядають так, як ви думаєте, що це має бути, коли ви пишете експортний інтерфейс в першу чергу.
  • архівабельність: якщо ви маєте свої дані як чисту бінарну купу, і ви втрачаєте програму, яка призначена для її інтерпретації, ви втрачаєте дані (або вам доведеться витратити досить багато часу, щоб насправді щось знайти там); якщо ваші серіалізовані дані читаються людиною, ви можете легко використовувати їх як архів або програмувати власного імпортера для нової програми
  • декларативний характер даних, серіалізованих таким чином, також означає, що вони абсолютно незалежні від комп'ютерної системи та її обладнання; ви можете завантажити його в абсолютно інший побудований квантовий комп'ютер або заразити чужорідного ШІ альтернативними фактами, щоб він випадково перелетів на сусіднє сонце (Еммеріх, якщо ви прочитаєте це, посилання буде добре, якщо ви будете використовувати цю ідею наступного 4 липня фільм)

Мої дані, мабуть, в основному в основному пам'яті, а не в регістрах. Якщо мої дані вписуються в регістри, серіалізація ледве навіть є проблемою. Я думаю, ви неправильно зрозуміли, що таке реєстр.
Девід Річербі

Дійсно, тут я занадто вільно використовував термін "регістр". Але головне в тому, що ваші дані можуть містити вказівники на адресний простір для ідентифікації його власних компонентів або для посилання на інші дані. Не має значення, чи це фізичний регістр чи віртуальна адреса в основній пам'яті.
Френк Хопкінс

Ні, ви ввели термін "реєструватись" абсолютно неправильно. Речі, за якими ви викликаєте регістри, знаходяться в зовсім іншій частині ієрархії пам'яті до реальних регістрів.
Девід Річербі

6

Окрім того, що сказали інші відповіді:

Іноді хочеться серіалізувати речі, які не є чистими даними.

Наприклад, подумайте про ручку файлу або про підключення до сервера. Навіть незважаючи на те, що ручка або сокет файлу є int, це число безглуздо при наступному запуску програми. Щоб правильно відтворити об’єкти, що містять ручки для таких речей, потрібно знову відкрити файли та відтворити з'єднання та вирішити, що робити, якщо це не вдасться.

Багато мов сьогодні підтримують збереження анонімних функцій у об’єктах, наприклад, onBlah()обробник у Javascript. Це складно, оскільки такий код може містити посилання на додаткові фрагменти даних, які, у свою чергу, потребують серіалізації. (І тоді виникає проблема серіалізації коду міжплатформовим способом, що, очевидно, простіше для інтерпретованих мов.) Але все-таки, навіть якщо підтримується лише підмножина мови, вона все ще може виявитися досить корисною. Не багато механізмів серіалізації намагаються серіалізувати код, але дивіться serialize-javascript .

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

Часто ви хочете, щоб серіалізовані дані були лаконічними.

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

Звичайно, програмісту потрібно вручну вибрати те, що потрібно зберегти, а що потрібно відкинути, і переконатися, що речі відбудуються під час відтворення об’єкта.

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

Іноді простір не важливий, але читабельність - у цьому випадку ви можете використовувати формат ASCII (можливо, JSON або XML).


3

Давайте визначимо, що насправді є послідовністю байтів. Послідовність байтів складається з невід'ємного цілого числа, що називається довжиною, і деякої довільної функції / відповідності, яка відображає будь-яке ціле число i, що є принаймні нулем і менше довжини, на значення байта (ціле число від 0 до 255).

Багато об'єктів, якими ви займаєтесь у типовій програмі, не в такій формі, оскільки об'єкти фактично складаються з безлічі різних розподілів пам'яті, які є в різних місцях оперативної пам’яті, і їх можна відокремити від іншого на мільйони байтів матеріалів, які ви не хвилюйся. Подумайте лише про базовий зв’язаний список: кожен вузол у списку - це послідовність байтів, так, але вузли знаходяться в багатьох різних місцях у пам’яті вашого комп’ютера, і вони пов'язані з покажчиками. Або просто подумайте про просту структуру, яка має вказівник на рядок змінної довжини.

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


1
Я не впевнений, що це чудове визначення послідовності. Більшість людей визначають послідовність, яка є, ну, послідовністю: рядок речей одна за одною. За вашим визначенням, int seq(int i) { if (0 <= i < length) return i+1; else return -1;}це послідовність. То як я збираюсь зберігати це на диску?
Девід Річербі

1
Якщо довжина 4, я зберігаю чотири байтний файл із вмістом: 1, 2, 3, 4.
Девід Грейсон,

1
@DavidRicherby Його визначення еквівалентно "ряду речей одна за одною", це просто більш математичне і точне визначення, ніж ваше інтуїтивне визначення. Зауважте, що ваша функція не є послідовністю, тому що, щоб мати послідовність, вам потрібна ця функція та інше ціле число, яке називається довжиною.
користувач253751

1
@FreshAir Моя думка полягає в тому, що послідовність дорівнює 1, 2, 3, 4, 5. Я записав це функція . Функція - це не послідовність.
Девід Річербі

1
Простий спосіб записати функцію на диск - це той, який я вже запропонував: для кожного можливого вводу зберігайте вихід. Я думаю, може, ти все ще не зрозумієш, але я не впевнений, що сказати. Чи знали ви, що у вбудованих системах звичайно перетворювати дорогі функції, наприклад, sinу таблицю пошуку, яка є послідовністю чисел? Чи знаєте ви, що ваша функція така ж, як і ця для входів, які нас цікавлять? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } Чому саме ви кажете, що мій чотирибайтний файл є неадекватним представленням?
Девід Грейсон

2

Тонкощі відображають тонкощі самих даних та об’єктів. Ці об'єкти можуть бути об'єктами реального світу або лише комп'ютерними. Відповідь у назві. Серіалізація - це лінійне представлення багатовимірних об'єктів. Існує багато питань, окрім фрагментованої оперативної пам’яті.

Якщо ви можете згладити 12 п'ятивимірних масивів та деякий програмний код, серіалізація також дозволяє перенести всю комп'ютерну програму (та дані) між машинами. Розподілені обчислювальні протоколи, такі як RMI / CORBA, широко використовують серіалізацію для передачі даних та програм.

Розгляньте свій телефонний рахунок. Це може бути єдиний об'єкт, що складається з усіх ваших дзвінків (список рядків), суми до сплати (цілого числа) та країни. Або ваш телефонний рахунок може бути зсередини вищезгаданого і складається з дискретних деталізованих телефонних дзвінків, пов’язаних з вашим іменем. Кожна вирівняна буде виглядати по-різному, відображати те, як ваша телефонна компанія написала цю версію свого програмного забезпечення та причину того, що об'єктно-орієнтовані бази даних ніколи не знімалися.

Деякі частини структури можуть взагалі не бути в пам'яті. Якщо у вас є ліниве кешування, деякі частини об'єкта можуть бути посилаються лише на файл диска, і завантажуються лише тоді, коли доступ до цієї частини конкретного об'єкта доступний. Це є загальним для серйозних систем збереження. BLOB - хороший приклад. Getty Images може зберігати величезну багатогабайтну картину Фіделя Кастро та деякі метадані, такі як назва зображення, вартість прокату та саме зображення. Можливо, ви не хочете щоразу завантажувати зображення в 200 Мб у пам'ять, якщо ви справді не дивитесь на нього. Серіалізований, весь файл потребує понад 200 Мб пам’яті.

Деякі об'єкти взагалі не можуть бути серіалізовані. У країні програмування Java ви можете мати об'єкт програмування, що представляє графічний екран або фізичний послідовний порт. Немає реальної концепції серіалізації жодної з них. Як би ви відправили свій порт комусь іншому по мережі?

Деякі речі, такі як паролі / ключі шифрування, не повинні зберігатися та передаватися. Вони можуть бути помічені як такі (мінливі / перехідні тощо), і процес серіалізації пропустить їх, але вони можуть жити в оперативній пам'яті. Опускання цих тегів - це те, як ключі шифрування ненароком надсилаються / зберігаються у звичайному ASCII.

Це та інші відповіді, чому це складно.


2

Проблема у мене полягає в тому, чи не всі змінні (будь то примітиви на зразок int чи складених об'єктів) вже представлені послідовністю байтів?

Так, вони є. Проблема тут - компонування цих байтів. Простий intможе бути довжиною 2, 4 або 8 біт. Це може бути великий або малий ендіан. Він може бути непідписаним, підписаним доповненням 1 або навіть у якомусь екзотичному бітовому кодуванні, як негабінарне.

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

То що робить серіалізацію такою глибокою темою? Щоб серіалізувати змінну, чи не можемо ми просто взяти ці байти в пам'ять і записати їх у файл? Які тонкощі я пропустив?

Серіалізація простого об'єкта, як правило, записує його за деякими правилами. Цих правил багато і не завжди очевидно. Наприклад, xs:integerв XML написано в базі-10. Не base-16, не base-9, але 10. Це не приховане припущення, це фактичне правило. І такі правила роблять серіалізацію серіалізацією. Тому що, в значній мірі, немає правил щодо бітового компонування вашої програми в пам'яті .

Це була лише верхівка айсберга. Давайте розглянемо приклад послідовності цих найпростіших примітивів: а C struct. Ви могли подумати про це

struct {
short width;
short height;
long count;
}

має визначений макет пам'яті на заданому комп'ютері + ОС? Ну, це не так. Залежно від поточного #pragma packналаштування, компілятор буде розміщувати поля. У налаштуваннях 32-розрядної компіляції обидва shortsбудуть зафіксовані на 4 байти, тому structфактично буде 3 поля в 4 байти. Тож тепер вам не тільки потрібно вказати, що shortце 16 біт, це ціле число, записане в доповнення 1 від'ємним, великим або маленьким ендіаном. Вам також потрібно записати структуру упаковки, з якої була складена ваша програма.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.