シリアル化について


38

私はソフトウェアエンジニアであり、同僚と議論した後、シリアル化の概念を十分に把握していないことに気付きました。私が理解しているように、シリアル化は、OOPのオブジェクトなどのエンティティをバイトシーケンスに変換するプロセスであり、そのエンティティを後のアクセスのために保存または送信できます(「逆シリアル化」のプロセス)。

私が抱えている問題は、すべての変数(プリミティブのようなものでもint、複合オブジェクトでも)がすでにバイトシーケンスで表されているのではないかということです。(もちろん、レジスタ、メモリ、ディスクなどに格納されているためです)

それでは、シリアル化をこれほど深いトピックにしているのはなぜですか?変数をシリアル化するために、これらのバイトをメモリに取り込んでファイルに書き込むことはできませんか?私が見逃した複雑さは何ですか?


21
連続したオブジェクトのシリアル化は簡単です。オブジェクト値がポインターグラフとして表される場合、特に上記のグラフにループがある場合は、事態がよりトリッキーになります。
チー

1
@chi:連続性が無関係であることを考えると、最初の文は少し誤解を招くかもしれません。グラフがメモリ内で連続している場合、(a)実際に連続していることを検出し、(b)内部のポインターを修正する必要があるため、それでもシリアル化に役立ちません。私はあなたが言ったことの第二の部分を言うだけです。
-Mehrdad

@Mehrdadあなたが言及した理由により、私のコメントが完全に正確ではないことに同意します。おそらく、ポインタフリー/ポインタ-使用して、より良い区別(さえ完全に正確ではない場合は、どちらか)である
カイ

7
また、ハードウェア上の表現についても心配する必要があります。4 bytesPDP-11でintをシリアル化してから、同じ4バイトをMacbookのメモリに読み取ろうとすると、それらは同じ数ではありません(エンディアンのため)。したがって、デコード可能な表現にデータを正規化する必要があります(これはシリアル化です)。データをシリアル化する方法には、人間と機械が読み取り可能な速度/柔軟性のトレードオフもあります。
マーティンヨーク

深く接続された多くのナビゲーションプロパティを持つEntity Frameworkを使用している場合はどうなりますか?ある場合には、ナビゲーションプロパティをシリアル化したいが、別の場合はnullのままにします(シリアル化された親オブジェクトにあるIDに基づいてデータベースからその実際のオブジェクトを再ロードするため)。これはほんの一例です。沢山あります。
エリック

回答:


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では、プロセスは最大4GiBのメモリスペースを持ち、OSまたはMMUは実際にどのメモリであるかの一般的な考えしか持っていません(プロセスに割り当てられたメモリページに基づいて)意味があるかどうかに関係なくNotepad.exe、テキストファイルを保存するたびに4GBのrawバイトをディスクにダンプするのは少し無駄です。

別の問題はバージョン管理にあります:Node1日目にグラフをシリアル化し、2日目に別のフィールドNode(別のポインター値、プリミティブ値など)を追加し、3日目にファイルを逆シリアル化するとどうなりますか1日目?

エンディアンのような他のことも考慮する必要があります。表向きは同じプログラム(Word、Photoshopなど)で作成されているにもかかわらず、1980年代と1990年代にMacOSとIBM / Windows / PCファイルが互いに互換性がない主な理由の1つは、x86 / PCのマルチバイト整数値Macではリトルエンディアンの順序で保存されましたが、ビッグエンディアンの順序で保存されました。また、ソフトウェアはクロスプラットフォームの移植性を考慮して構築されていませんでした。開発者教育の改善と、ますます多様化するコンピューティングの世界のおかげで、現在は状況が改善されています。


2
プロセスのメモリ空間のすべてをダンプすることは、セキュリティ上の理由から恐ろしいことです。プログラムの夜には、1)いくつかの公開データと2)パスワード、秘密のナンス、または秘密鍵の両方がメモリにあります。前者をシリアル化する場合、後者に関する情報を公開したくないでしょう。
チー

8
このトピックに関する非常に興味深いメモ:Microsoft Officeファイル形式はなぜそれほど複雑なのですか?
打つ

15

トリッキーは、実際には「シリアル化」という言葉自体ですでに説明されています。

問題は基本的に、任意の複雑なオブジェクトの任意に複雑な相互接続された循環有向グラフをバイトの線形シーケンスとしてどのように表現できますか?

考えてみてください:線形シーケンスは、すべての頂点がちょうど1つの入力エッジと出力エッジを持つ退化した有向グラフのようなものです(入力エッジのない「最初の頂点」と出力エッジのない「最後の頂点」を除く) 。そして、バイトは明らかにオブジェクトほど複雑ではありません。

したがって、任意の複雑なグラフからより制限された「グラフ」(実際には単なるリスト)に、そして任意の複雑なオブジェクトから単純なバイトに進むにつれて、情報失うことになります。何らかの方法で「外来」情報をエンコードします。そして、それがまさにシリアライゼーションです。複雑な情報を単純な線形形式にエンコードします。

YAMLに精通している場合は、「同じオブジェクトが異なる場所に表示される可能性がある」という考え方をシリアル化で表すことができるアンカー機能とエイリアス機能を見ることができます。

たとえば、次のグラフがある場合:

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

次のように、YAMLの線形パスのリストとしてそれを表すことができます。

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

また、隣接リスト、隣接マトリックス、または最初の要素がノードのセットであり、2番目の要素がノードのペアのセットであるペアとして表すこともできますが、これらのすべての表現では、既存のノード、つまり、通常はファイルまたはネットワークストリームにはないポインターを前後に参照する方法。最終的には、バイトだけです。

(BTWは、上記のYAMLテキストファイル自体も「シリアル化」する必要があることを意味します。これは、さまざまな文字エンコーディングとUnicode転送フォーマットの目的です。テキストファイルは既にシリアルであるため、 /コードポイントの線形リスト。ただし、いくつかの類似点があります。)


13

他の答えはすでに複雑なオブジェクトグラフに対処していますが、プリミティブをシリアル化することも簡単ではないことを指摘する価値があります。

具体性のためにCプリミティブ型名を使用する場合は、次のことを考慮してください。

  1. をシリアル化しlongます。いくつかの時間後、私にそれをデシリアライズが...別のプラットフォーム上で、今longあるint64_tのではなく、int32_t保存されたI。そのため、保存するすべてのタイプの正確なサイズに注意するか、すべてのフィールドのタイプとサイズを記述するメタデータを保存する必要があります。

    この異なるプラットフォームは、将来の再コンパイル後に同じプラットフォームになる可能性があることに注意してください。

  2. をシリアル化しint32_tます。しばらくして、私はそれを逆シリアル化しますが、...別のプラットフォームで、今では値が壊れています。悲しいことに、ビッグエンディアンのプラットフォームに値を保存し、リトルエンディアンのプラットフォームにロードしました。今、私は私のフォーマットのための規則を確立し、または追加する必要がある以上、各ファイル/ストリーム/何のendiannnessを記述したメタデータを。そして、もちろん、実際に適切な変換を実行します。

  3. 文字列をシリアル化します。今回はchar、1つのプラットフォームがUTF-8を使用し、1つがwchar_tUTF-16を使用します。

したがって、連続したメモリ内のプリミティブに対しても、合理的な品質のシリアル化は簡単ではないと主張します。インラインメタデータを使用して文書化または説明する必要があるエンコードの決定は多数あります。

オブジェクトグラフは、さらに複雑なレイヤーを追加します。


6

複数の側面があります。

同じプログラムによる可読性

プログラムは、データを何らかの形でメモリにバイトとして保存しました。しかし、ポインタが小さな部分の間を行き来することで、異なるレジスタにarbitrarily意的に散らばっている場合があります[編集:コメントのように、物理的にデータはデータレジスタよりもメインメモリにある可能性が高いですが、ポインタの問題は取り除かれません] 。リンクされた整数リストを考えてください。各リスト要素は完全に異なる場所に保存される場合があり、リストをまとめて保持するのは、ある要素から次の要素へのポインタだけです。そのデータをそのまま使用して、同じプログラムを実行している別のマシンにコピーしようとすると、問題が発生します。

  1. 何よりもまず、1台のマシンにデータが保存されているレジスタは、別のマシンでまったく異なるものに既に使用されている可能性があります(誰かがスタック交換を参照しており、ブラウザーがそのメモリをすべて食べました)。したがって、これらのレジスタを単純にオーバーライドする場合、さようならブラウザ。したがって、2番目のマシンで空いているアドレスに合うように、構造体のポインターを再配置する必要があります。後で同じマシンにデータを再ロードしようとすると、同じ問題が発生します。
  2. 一部の外部コンポーネントが構造を指している場合、または構造に外部データへのポインターがある場合、送信しませんでしたか?どこでもセグフォールト!これはデバッグの悪夢になります。

別のプログラムによる可読性

データが収まるように、別のマシンに適切なアドレスを割り当てることができたとします。そのマシン上の別のプログラム(異なる言語)でデータが処理される場合、そのプログラムはデータの基本的な理解がまったく異なる可能性があります。ポインターを持つC ++オブジェクトがあるが、ターゲット言語ではそのレベルのポインターさえサポートされていないとします。繰り返しますが、2番目のプログラムでそのデータに対処するための明確な方法はありません。最終的にメモリ内にバイナリデータがいくつかありますが、データをラップし、何らかの方法でターゲット言語で使用できるものに変換する追加のコードを記述する必要があります。デシリアライゼーションのように聞こえますが、出発点はメインメモリに散らばった奇妙なオブジェクトであり、それはソース言語によって異なるため、明確に定義された構造を持つファイルの代わりに。もちろん、ポインターを含むバイナリファイルを直接解釈しようとする場合も同じことです。別の言語がメモリ内のデータを表す可能性のあるあらゆる方法でパーサーを記述する必要があります。

人間による可読性

Webベースのシリアル化(xml、json)で最も有名な2つの最新のシリアル化言語は、人間が簡単に理解できます。グーのバイナリの山の代わりに、データを読み取るプログラムがなくても、データの実際の構造と内容は明確です。これには複数の利点があります。

  • 簡単なデバッグ->サービスパイプラインに問題がある場合は、1つのサービスから出力されるデータを見て、それが理にかなっているかどうかを確認します(最初のステップとして)。また、エクスポートインターフェイスを最初に作成するときに、データが想定どおりに見えるかどうかを直接確認します。
  • アーカイブ性:データが純粋なバイナリグーパイルとしてあり、それを解釈するためのプログラムを失うと、データが失われます(または、実際にそこに何かを見つけるのにかなりの時間を費やす必要があります)。シリアル化されたデータが人間が読める場合は、アーカイブとして簡単に使用したり、新しいプログラムのインポーターをプログラムしたりできます
  • このようにシリアル化されたデータの宣言的な性質は、コンピューターシステムとそのハードウェアから完全に独立していることも意味します。それをまったく異なる構成の量子コンピューターにロードしたり、別の事実をエイリアンAIに感染させて、誤って次の太陽に飛ぶことができます(これを読んだ場合、エメリッヒは次の7月4日にそのアイデアを使用するといいでしょう)映画)

私のデータはおそらくレジスタではなく、主に主記憶にあります。データがレジスタに収まる場合、シリアル化はほとんど問題になりません。レジスタとは何かを誤解していると思います。
デヴィッドリチャービー

実際、ここではレジスタという用語をあまりにも緩やかに使用しました。ただし、主な点は、データにアドレススペースへのポインターを含めて、独自のコンポーネントを識別したり、他のデータを参照したりできることです。物理レジスタであるか、メインメモリの仮想アドレスであるかは関係ありません。
フランクホプキンス

いいえ、「登録」という用語を完全に間違って使用しました。レジスタを呼び出しているものは、メモリ階層の実際のレジスタとはまったく異なる部分にあります。
デビッドリチャービー

6

他の答えが言ったことに加えて:

場合によっては、純粋なデータではないものをシリアル化したいことがあります。

たとえば、ファイルハンドルやサーバーへの接続を考えます。ファイルハンドルまたはソケットはであるにもかかわらずint、この数値はプログラムが次に実行されるときに意味がありません。そのようなものへのハンドルを含むオブジェクトを適切に再作成するには、ファイルを再度開いて接続を再作成し、これが失敗した場合の対処方法を決定する必要があります。

最近の多くの言語onBlah()は、Javascriptのハンドラーなど、オブジェクト内に匿名関数を格納することをサポートしています。このようなコードには、シリアル化する必要がある追加のデータへの参照を含めることができるため、これは困難です。(そして、クロスプラットフォームの方法でコードをシリアル化する問題があります。これは、インタプリタ言語にとって明らかに簡単です。)それでも、言語のサブセットのみをサポートできる場合でも、それは非常に有用です。多くのシリアル化メカニズムはコードのシリアル化を試みませんが、serialize-javascriptを参照してください。

オブジェクトをシリアル化したいが、シリアル化メカニズムでサポートされていないものが含まれている場合は、この問題を回避する方法でコードを書き直す必要があります。たとえば、可能な関数の数が限られている場合、匿名関数の代わりに列挙を使用できます。

多くの場合、シリアル化されたデータを簡潔にする必要があります。

ネットワーク経由でデータを送信する場合、またはディスクに保存する場合でも、サイズを小さく保つことが重要です。これを実現する最も簡単な方法の1つは、再構築可能な情報を破棄することです(たとえば、キャッシュ、ハッシュテーブル、および同じデータの代替表現を破棄する)。

もちろん、プログラマは保存するものと破棄するものを手動で選択し、オブジェクトが再作成されたときに物事が再構築されることを確認する必要があります。

ゲームを保存する行為について考えてください。オブジェクトには、グラフィックスデータ、サウンドデータ、およびその他のオブジェクトへの多くのポインターが含まれる場合があります。ただし、これらのほとんどはゲームデータファイルから読み込むことができ、保存ファイルに保存する必要はありません。時間をかけていくつかの保存ファイルを16進編集し、テキスト項目の説明のように明らかに冗長なデータを発見しました。

スペースは重要ではないが、読みやすさが重要な場合があります。その場合は、代わりにASCII形式(おそらくJSONまたはXML)を使用できます。


3

バイトシーケンスが実際に何であるかを定義しましょう。バイトのシーケンスは、非負整数から成る呼ば長さと任意の整数マッピングいくつかの任意の機能/対応I少なくともゼロ未満であり、長さバイトの値(0〜255の整数)にします。

典型的なプログラムで扱うオブジェクトの多くはその形式ではありません。オブジェクトは実際にはRAMのさまざまな場所にある多くの異なるメモリ割り当てで構成されており、何百万バイトものものによって互いに分離できるためです。気にしません。基本的なリンクリストを考えてみてください。リスト内の各ノードは一連のバイトです。ただし、ノードはコンピューターのメモリ内のさまざまな場所にあり、ポインターで接続されています。または、可変長文字列へのポインタを持つ単純な構造体を考えてください。

データ構造をバイトシーケンスにシリアル化する理由は、通常、ディスクに保存したり、ネットワーク経由で別のシステムに送信したりするためです。ポインタをディスクに保存したり、別のシステムに送信しようとすると、そのポインタを読み取るプログラムは使用可能なメモリ領域のセットが異なるため、ほとんど役に立ちません。


1
それがシーケンスの優れた定義であるかどうかはわかりません。ほとんどの人は、シーケンスを次のように定義します。あなたの定義でint seq(int i) { if (0 <= i < length) return i+1; else return -1;}は、シーケンスです。それをどのようにディスクに保存するのですか?
デビッドリチャービー

1
長さが4の場合、内容が1、2、3、4の4バイトのファイルを保存します。
David Grayson

1
@DavidRicherby彼の定義は「次々に物事の列」に相当し、直感的な定義よりも数学的で正確な定義にすぎません。シーケンスを作成するには、その関数、長さと呼ばれる別の整数が必要なため、関数はシーケンスではないことに注意してください。
user253751

1
@FreshAir私のポイントは、シーケンスが1、2、3、4、5 であることです。書き留めたのは関数です。関数はシーケンスではありません。
デビッドリチャービー

1
関数をディスクに書き込む簡単な方法は、すでに提案した方法です。可能なすべての入力に対して、出力を保存します。まだわからないかもしれませんが、何と言ったらいいかわかりません。組み込みシステムではsin、数値のシーケンスであるルックアップテーブルに高価な関数を変換するのが一般的であることをご存知ですか?あなたの機能は、私たちが気にする入力についてはこれと同じであることを知っていましたか? 4バイトのファイルが不適切な表現であると正確に言うのはint seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } なぜですか?
デビッドグレイソン

2

複雑さは、データとオブジェクト自体の複雑さを反映しています。これらのオブジェクトは、実世界のオブジェクトでも、コンピューターのみのオブジェクトでもかまいません。答えは名前にあります。シリアル化は、多次元オブジェクトの線形表現です。RAMの断片化以外にも多くの問題があります。

12の5次元配列と一部のプログラムコードをフラット化できる場合、シリアル化により、コンピュータープログラム(およびデータ)全体をマシン間で転送することもできます。RMI / CORBAなどの分散コンピューティングプロトコルは、シリアル化を広く使用してデータとプログラムを転送します。

電話代を考慮してください。すべての呼び出し(文字列のリスト)、支払い金額(整数)、および国で構成される単一のオブジェクトである場合があります。または、あなたの電話代は上記の裏返しであり、あなたの名前にリンクされた個別の項目別の電話で構成される可能性があります。平らにされたそれぞれは異なって見えます、あなたの電話会社がそのソフトウェアのそのバージョンを書いた方法とオブジェクト指向データベースが決して離陸しなかった理由を反映します。

構造体の一部は、まったくメモリ内にない場合もあります。遅延キャッシュを使用している場合、オブジェクトの一部はディスクファイルのみを参照し、特定のオブジェクトのその部分にアクセスしたときにのみロードされます。これは深刻な永続化フレームワークでは一般的です。BLOBは良い例です。ゲッティイメージズは、フィデルカストロの巨大なマルチメガバイト画像と、画像の名前、レンタルコスト、画像自体などのメタデータを保存する場合があります。実際に見ない限り、毎回200 MBの画像をメモリにロードしたくないかもしれません。シリアル化すると、ファイル全体に200MB以上のストレージが必要になります。

一部のオブジェクトはまったくシリアル化することさえできません。Javaプログラミングの国では、グラフィック画面または物理シリアルポートを表すプログラミングオブジェクトを使用できます。それらのいずれかを直列化するという本当の概念はありません。ネットワーク経由で他の人にどのようにポートを送信しますか?

パスワード/暗号化キーのようなものは保存または送信されるべきではありません。それらはそのようにタグ付けすることができ(volatile / transientなど)、シリアル化プロセスはそれらをスキップしますが、RAMに置くことができます。これらのタグを省略すると、暗号化キーが誤ってプレーンASCIIで送信/保存される方法になります。

これと他の答えは、それが複雑な理由です。


2

私が抱えている問題は、すべての変数(intや複合オブジェクトなどのプリミティブ)が既にバイトシーケンスで表されているわけではないということです。

はい、そうです。ここでの問題は、これらのバイトのレイアウトです。シンプルintは、2、4、または8ビット長です。ビッグエンディアンでもスモールエンディアンでもかまいません。符号なし、1の補数で符号化、またはネガバイナリのような超エキゾチックなビットコーディングでも符号化できます。

intメモリからバイナリを単にダンプし、それを「シリアル化」と呼ぶ場合、ほとんどのコンピュータ、オペレーティングシステム、およびプログラムを接続解除して、シリアル化解除する必要があります。または、少なくとも、それらの正確な説明。

それでは、シリアル化をこれほど深いトピックにしているのはなぜですか?変数をシリアル化するために、これらのバイトをメモリに取り込んでファイルに書き込むことはできませんか?私が見逃した複雑さは何ですか?

単純なオブジェクトのシリアル化は、いくつかの規則に従ってほとんど書き留めています。これらのルールは豊富であり、必ずしも明白ではありません。たとえば、xs:integerXMLの10進数で記述されています。16進数ではなく、9進数ではなく、10です。これは隠された仮定ではなく、実際のルールです。そして、そのようなルールは、シリアル化をシリアル化にします。なぜなら、ほとんどの場合、メモリ内のプログラムのビットレイアウトに関する規則はないからです

それは氷山の一角にすぎません。C:のは、それらの最も単純なプリミティブのシーケンスの例を見てみましょうstruct。あなたはそれを考えることができます

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

特定のコンピューター+ OSでメモリレイアウトが定義されていますか?まあ、そうではありません。現在の#pragma pack設定に応じて、コンパイラはフィールドを埋め込みます。32ビットコンパイルのデフォルト設定では、両方shortsが4バイトに埋め込まれるため、struct実際にはメモリ内に4バイトの3つのフィールドがあります。そのため、short16ビットの長さを指定する必要があるだけでなく、1の補数の負、ビッグまたはリトルエンディアンで記述された整数です。また、プログラムがコンパイルされたときの構造パッキング設定を書き留めておく必要があります。

これが、シリアル化の目的です。つまり、一連のルールを作成し、それらに固執します。

その後、これらのルールを拡張して、さらに高度な構造(可変長リストや非線形データなど)を受け入れたり、人間の可読性、バージョン管理、下位互換性、エラー修正などの機能を追加したりintできます。確実に読み返せるようにするだけです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.