ものがたり(旧)

atsushieno.hatenablog.com に続く

XmlDictionaryReader.CreateBinaryReader()

ちょいとさる方面で圧縮XML表現の話を振られたので、最近実装し直していたWCFのbinary dictioanry xml reader / writerの話を書いてみようと思う。便宜上、ここで使われるバイナリデータ形式WCF XmlBinary、そのXmlReader/XmlWriterの実装をそれぞれXmlBinaryDictionaryReader/XmlBinaryDictionaryWriterと呼ぶことにする(これはMono上の実装に対応している)。眠いのでとりあえず資料のリンクとかは無しで。

この(仮称)WCF XmlBinaryの細かい仕様は、MS-OSP (Open Specification Promise) によって公開されている[MC-NBFX]である。*1
http://msdn.microsoft.com/en-us/library/cc219210(PROT.10).aspx

たとえば、<root>text</root> は次のような16進バイトシーケンスになる*2:

  • 40 04 72 6F 6F 74 99 04 74 65 78 74

…これではほとんど短くなっていない。

これらは、WCFのSystem.Xml.XmlDictionaryWriter.CreateBinaryWriter()で作成したXmlWriterで出力し、System.Xml.XmlDictionaryReader.CreateBinaryReader()で作成したXmlReaderで読むことが出来る。

XMLの圧縮についてはいろいろやり方があるだろうけど、基本的には要素・属性(等)の名前にはだいたい同じものが何度も使用される可能性が高い。XML入力を文字列処理する際には、文字列をinternにして同じStringインスタンスを使い回したり、その上でobjectのリファレンス参照で比較をすませたり(XmlNameTableの目的はこれ)する。一方で、文書インスタンスの表現としては、頻繁に出現する名前にあらかじめインデックスを振っておき、文書インスタンス中では名前の代わりにインデックスが出現すれば、大変短くなる。そのかわり辞書データは事前に交換されている必要がある。

かくして(?)、WCF XmlBinaryでは、XmlDictionaryというオブジェクトが入力のソースのひとつとして、XmlBinaryDictionaryReaderに渡されることがある(たとえばCreateDictionaryWriter(Stream,IXmlDictionary,XmlBinaryWriterSession)など)。ここには頻出する名前がプリセットされていて、インスタンス中から参照されるという仕組みだ。

WCF XmlBinaryでは、名前を、入力辞書におけるインデックス、または文字列で指定することができる。*3

辞書ベースで先のXMLを書くとこんな感じになる(辞書のインデックスはXmlDictionaryによる):

  • 42 00 99 04 74 65 78 74

名前についてはユーザがプリセットすることで最適な圧縮を実現出来るが、テキストノードや属性値には、何が来るか分からないし、文書の内容によって適した圧縮符号化形式は変わるだろう。これはむしろXMLのアプリケーションレベルのプロトコルで解決すべき問題なんじゃないかと僕は思う。

WCF XmlBinaryの場合、単なるテキストノードに加えて、typed contentに特化した命令コードを持っている。xs:booleanのfalseなら命令0x84、xs:booleanのtrueなら命令0x86、みたいな感じだ。これだと特にXML Schema Datatypesを多用するデータに特化した文書の記述はいささか効率が良くなる。xs:intやxs:dateTimeばかり使っている文書なら、そのまま読み書き出来た方が効率が良い場面もあるかもしれない。

バイナリデータの読み書きには、実際にはBinaryReader/BinaryWriterの仕様に依存している部分があって、たとえば可変長数値にはWrite7BitEncodedInt()が使われるし、上記typed contentもBinaryReader/BinaryWriterの形式になっているのがほとんどだ。

ちなみに、EndElementは 0x01 という命令が割り当てられているが、テキストノードや、これに該当するtyped contentに続く場合は、通常はそれらのノードの命令が+1される。xs:booleanのtrueは 0x84 の1バイトだが、これにEndElementが続く場合は 0x85 となる。全てのテキストノード命令は、これが可能になるように1ずつ開いている。せこい最適化だw

具体例などはnunitテストコードに入っていたりするので、そこでも見てもらえればと思う。

*1:僕が一番始めに実装したのはもちろんその前のことだったから、バイナリデータの仕様をあれこれ推測しながら実装していた。

*2:40=element [len=4] "root", 99=text+endElement [len=4] "text"

*3:ちなみに、入力辞書におけるインデックスは、実はそのままXmlDictionaryStringにおけるindexになるのではなく、2nならXmlDictionaryにおけるインデックスnの、2n+1ならXmlBinaryReaderSessionにおけるインデックスnの、XmlDictionaryStringに対応する。これは実装依存であるため、[MC-NBFX]にも記載されていない。