比較的複雑な(ゲーム)オブジェクト構造のファイルに状態情報を格納するための一時的なメカニズムとして、バイナリシリアル化(BinaryFormatter)を使用しています。ファイルは予想よりもはるかに大きく大きく出ており、データ構造には再帰参照が含まれています-したがって、BinaryFormatterが実際に複数のコピーを格納しているかどうか疑問に思っています同じオブジェクトの、または私の基本的な「必要なオブジェクトと値の数」の算術演算がベースから大きく外れているかどうか、または過剰なサイズがどこから来ているか。
スタックオーバーフローを検索すると、Microsoftのバイナリリモート形式の仕様を見つけることができました: http://msdn.Microsoft.com/en-us/library/cc236844(PROT.10).aspx
私が見つけられないのは、binaryformatter出力ファイルの内容を「覗き見」できる既存のビューアです。ファイル内のさまざまなオブジェクトタイプのオブジェクト数と合計バイト数などを取得します。
これは私の「google-fu」が私を失敗させているに違いないと思います(私が持っているものはほとんどありません)-誰か助けてもらえますか?これは必須以前に行われたことがありますか?
[〜#〜] update [〜#〜]:見つからず、回答も得られなかったので、比較的迅速にまとめました(ダウンロード可能なリンク以下のプロジェクト); BinaryFormatterが同じオブジェクトの複数のコピーを格納していないことは確認できますが、ストリームにかなりの量のメタデータが出力されます。効率的なストレージが必要な場合は、独自のカスタムシリアル化メソッドを構築してください。
誰かが興味を持っているかもしれないので、この投稿を行うことにしましたシリアル化された.NETオブジェクトのバイナリ形式はどのように見え、どのように正しく解釈できますか?
私はすべての調査を 。NET Remoting:Binary Format Data Structure 仕様に基づいています。
クラスの例:
実用的な例として、A
という単純なクラスを作成しました。このクラスには、1つの文字列と1つの整数値の2つのプロパティが含まれ、SomeString
およびSomeValue
と呼ばれます。
クラスA
は次のようになります:
[Serializable()]
public class A
{
public string SomeString
{
get;
set;
}
public int SomeValue
{
get;
set;
}
}
シリアル化には、もちろんBinaryFormatter
を使用しました。
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();
ご覧のとおり、A
と123
を値として含むクラスabc
の新しいインスタンスを渡しました。
結果データの例:
シリアル化された結果を16進エディターで見ると、次のようになります。
結果データの例を解釈しましょう:
上記の仕様(PDFへの直接リンクは次のとおりです: [MS-NRBF] .pdf )によると、ストリーム内のすべてのレコードはRecordTypeEnumeration
で識別されます。セクション2.1.2.1 RecordTypeNumeration
は次のように述べています:
この列挙は、レコードのタイプを識別します。各レコード(MemberPrimitiveUnTypedを除く)は、レコードタイプの列挙で始まります。列挙のサイズは1バイトです。
SerializationHeaderRecord:
したがって、取得したデータを振り返ると、最初のバイトの解釈を開始できます。
2.1.2.1 RecordTypeEnumeration
に記載されているように、0
の値は、2.6.1 SerializationHeaderRecord
で指定されているSerializationHeaderRecord
を識別します。
SerializationHeaderRecordレコードは、バイナリシリアル化の最初のレコードである必要があります。このレコードには、フォーマットのメジャーバージョンとマイナーバージョン、および最上位オブジェクトとヘッダーのIDが含まれています。
構成:
その知識があれば、17バイトを含むレコードを解釈できます。
00
はRecordTypeEnumeration
を表し、この場合はSerializationHeaderRecord
です。
01 00 00 00
はRootId
を表します
BinaryMethodCallレコードもBinaryMethodReturnレコードもシリアル化ストリームに存在しない場合、このフィールドの値には、シリアル化ストリームに含まれるClass、Array、またはBinaryObjectStringレコードのObjectIdが含まれている必要があります。
したがって、この場合、これは値1
のObjectId
である必要があります(データはリトルエンディアンを使用してシリアル化されているため)。
FF FF FF FF
はHeaderId
を表します
01 00 00 00
はMajorVersion
を表します
00 00 00 00
はMinorVersion
を表します
BinaryLibrary:
指定されているように、各レコードはRecordTypeEnumeration
で始まる必要があります。最後のレコードが完成したので、新しいレコードが始まると想定する必要があります。
次のバイトを解釈しましょう:
ご覧のとおり、この例ではSerializationHeaderRecord
の後にBinaryLibrary
レコードが続きます。
BinaryLibraryレコードは、INT32 ID([MS-DTYP]セクション2.2.22で指定されている)をライブラリ名に関連付けます。これにより、他のレコードがIDを使用してライブラリ名を参照できるようになります。このアプローチは、同じライブラリ名を参照する複数のレコードがある場合にワイヤサイズを削減します。
構成:
LengthPrefixedString
))2.1.1.6 LengthPrefixedString
..に記載されているとおり.
LengthPrefixedStringは、文字列値を表します。文字列の前には、UTF-8でエンコードされた文字列の長さがバイト単位で付けられます。長さは、最小1バイトから最大5バイトの可変長フィールドにエンコードされます。ワイヤサイズを最小化するために、長さは可変長フィールドとしてエンコードされます。
この簡単な例では、長さは常に1 byte
を使用してエンコードされます。その知識があれば、ストリーム内のバイトの解釈を続けることができます。
0C
は、RecordTypeEnumeration
レコードを識別するBinaryLibrary
を表します。
02 00 00 00
はLibraryId
を表し、この場合は2
です。
これで、LengthPrefixedString
は次のようになります。
42
は、LengthPrefixedString
を含むLibraryName
の長さ情報を表します。
この場合、42
(10進数の66)の長さ情報は、次の66バイトを読み取り、それらをLibraryName
として解釈する必要があることを示しています。
すでに述べたように、文字列はUTF-8
でエンコードされているため、上記のバイトの結果は次のようになります。_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClassWithMembersAndTypes:
ここでも、レコードが完成しているので、次のレコードのRecordTypeEnumeration
を解釈します。
05
は、ClassWithMembersAndTypes
レコードを識別します。セクション2.3.2.1 ClassWithMembersAndTypes
は次のように述べています:
ClassWithMembersAndTypesレコードは、Classレコードの中で最も冗長です。これには、メンバーの名前やリモートタイプなど、メンバーに関するメタデータが含まれています。また、クラスのライブラリ名を参照するライブラリIDも含まれています。
構成:
ClassInfo:
2.3.1.1 ClassInfo
に記載されているように、レコードは次のもので構成されます。
LengthPrefixedString
))LengthPrefixedString
のシーケンスであり、アイテムの数はMemberCount
フィールドで指定された値と等しくなければなりません。)
生データに戻って、ステップバイステップで:
01 00 00 00
はObjectId
を表します。これはすでに見てきましたが、RootId
でSerializationHeaderRecord
として指定されました。
0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41
は、Name
を使用して表されるクラスのLengthPrefixedString
を表します。前述のように、この例では、文字列の長さは1バイトで定義されているため、最初のバイト0F
は、UTF-8を使用して15バイトを読み取ってデコードする必要があることを指定します。結果は次のようになります。StackOverFlow.A
-明らかに、名前空間の名前としてStackOverFlow
を使用しました。
02 00 00 00
はMemberCount
を表し、両方ともLengthPrefixedString
で表される2つのメンバーが続くことを示しています。
最初のメンバーの名前:
1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64
は最初のMemberName
を表し、1B
は文字列の長さで27バイトの長さであり、結果は次のようになります:<SomeString>k__BackingField
。
2番目のメンバーの名前:
1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64
は2番目のMemberName
を表し、1A
は文字列の長さが26バイトであることを指定します。結果は次のようになります:<SomeValue>k__BackingField
。
MemberTypeInfo:
ClassInfo
の後に、MemberTypeInfo
が続きます。
セクション2.3.1.2 - MemberTypeInfo
は、構造に次のものが含まれていると述べています。
転送されるメンバータイプを表す一連のBinaryTypeEnumeration値。アレイは次のことを行う必要があります。
ClassInfo構造のMemberNamesフィールドと同じ数のアイテムがあります。
BinaryTypeEnumerationがClassInfo構造のMemberNamesフィールドのメンバー名に対応するように順序付けられます。
BinaryTpeEnum
の追加情報に応じて、AdditionalInfos(長さは可変)が存在する場合と存在しない場合があります。
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
それを考慮に入れると、ほぼそこにあります... 2つのBinaryTypeEnumeration
値が期待されます(MemberNames
に2つのメンバーがあったため)。
ここでも、完全なMemberTypeInfo
レコードの生データに戻ります。
01
は最初のメンバーのBinaryTypeEnumeration
を表し、2.1.2.2 BinaryTypeEnumeration
によれば、String
が期待でき、LengthPrefixedString
を使用して表されます。
00
は、2番目のメンバーのBinaryTypeEnumeration
を表します。また、仕様によれば、これはPrimitive
です。上記のように、Primitive
の後に追加情報(この場合はPrimitiveTypeEnumeration
)が続きます。そのため、次のバイトである08
を読み取り、それを2.1.2.3 PrimitiveTypeEnumeration
に記載されているテーブルと照合し、4で表されるInt32
を期待できることに驚いています。基本的なデータ型に関する他のドキュメントに記載されているバイト。
LibraryId:
MemerTypeInfo
LibraryId
が続くと、4バイトで表されます。
02 00 00 00
は、2であるLibraryId
を表します。
値:
2.3 Class Records
で指定されているとおり:
クラスのメンバーの値は、セクション2.7で指定されているように、このレコードに続くレコードとしてシリアル化する必要があります。レコードの順序は、ClassInfo(セクション2.3.1.1)構造で指定されているMemberNamesの順序と一致する必要があります。
だからこそ、メンバーの価値観が期待できるようになりました。
最後の数バイトを見てみましょう:
06
はBinaryObjectString
を識別します。これは、SomeString
プロパティ(正確には<SomeString>k__BackingField
)の値を表します。
2.5.7 BinaryObjectString
によると、次のものが含まれています。
LengthPrefixedString
として表される)
それを知っていると、それを明確に特定できます
03 00 00 00
はObjectId
を表します。
03 61 62 63
はValue
を表します。ここで、03
は文字列自体の長さであり、61 62 63
はabc
に変換されるコンテンツバイトです。
2番目のメンバーであるInt32
があったことを思い出していただければ幸いです。 Int32
が4バイトを使用して表されていることを知っていると、次のように結論付けることができます。
2番目のメンバーのValue
である必要があります。 7B
16進数は123
10進数に等しく、サンプルコードに適合しているようです。
したがって、ここに完全なClassWithMembersAndTypes
レコードがあります。
MessageEnd:
最後に、最後のバイト0B
はMessageEnd
レコードを表します。
Vasiliyは、バージョン管理をより適切に処理し、(圧縮前に)はるかにコンパクトなストリームを出力するために、最終的には独自のフォーマッター/シリアル化プロセスを実装する必要があるという点で正しいです。
しかし、ストリームで何が起こっているのかを理解したかったので、私が望んでいたことを実行する(比較的)クイッククラスを作成しました。
Codeprojectのように見える場所に置くのはあまり役に立たないので、プロジェクトを自分のWebサイトのZipファイルにダンプしました: http://www.architectshack.com/BinarySerializationAnalysis.ashx
私の特定のケースでは、問題は2つあることがわかりました。
これがいつか誰かに役立つことを願っています!
更新:Ian Wrightから、元のコードの問題について連絡がありました。ソースオブジェクトに「10進数」の値が含まれているとクラッシュしました。これは修正され、コードをGitHubに移動して、(パーミッシブ、BSD)ライセンスを付与する機会を利用しました。
私たちのアプリケーションは大量のデータを操作します。ゲームと同様に、最大1〜2GBのRAMが必要になる場合があります。同じ「同じオブジェクトの複数のコピーを保存する」という問題が発生しました。また、バイナリシリアル化では、保存するメタデータが多すぎます。最初に実装されたとき、シリアル化されたファイルは約1〜2GBかかりました。今日、私はなんとか値を減らすことができました-50-100MB。私たちは何をしましたか。
簡単な答え-.Netバイナリシリアル化を使用せず、独自のバイナリシリアル化メカニズムを作成します。独自のBinaryFormatterクラスとISerializableインターフェイス(Serialize、Deserializeの2つのメソッド)があります。
同じオブジェクトを複数回シリアル化しないでください。一意のIDを保存し、キャッシュからオブジェクトを復元します。
よろしければ、コードを共有できます。
編集:あなたは正しいようです。次のコードを参照してください-それは私が間違っていたことを証明しています。
[Serializable]
public class Item
{
public string Data { get; set; }
}
[Serializable]
public class ItemHolder
{
public Item Item1 { get; set; }
public Item Item2 { get; set; }
}
public class Program
{
public static void Main(params string[] args)
{
{
Item item0 = new Item() { Data = "0000000000" };
ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };
var fs0 = File.Create("temp-file0.txt");
var formatter0 = new BinaryFormatter();
formatter0.Serialize(fs0, holderOneInstance);
fs0.Close();
Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
//File.Delete(fs0.Name);
}
{
Item item1 = new Item() { Data = "1111111111" };
Item item2 = new Item() { Data = "2222222222" };
ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };
var fs1 = File.Create("temp-file1.txt");
var formatter1 = new BinaryFormatter();
formatter1.Serialize(fs1, holderTwoInstances);
fs1.Close();
Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
//File.Delete(fs1.Name);
}
}
}
BinaryFormatter
はobject.Equalsを使用して同じオブジェクトを検索しているようです。
生成されたファイルの内部を見たことがありますか?コード例から「temp-file0.txt」と「temp-file1.txt」を開くと、メタデータがたくさんあることがわかります。そのため、独自のシリアル化メカニズムを作成することをお勧めしました。
混乱してすみません。
プログラムをデバッグモードで実行して、コントロールポイントを追加してみてください。
ゲームのサイズやその他の依存関係のためにそれが不可能な場合は、デシリアライズコードを含むシンプルで小さなアプリをいつでもコーディングして、そこのデバッグモードから覗くことができます。