Protocol Bufferと数値型のサイズ

ちと気になったので検証。

Protocol Buffer(の.NETライブラリのprotobuf-net)では、数値型変数が値によってサイズが切り詰められ、ファイルサイズを削減できることが確認できます。

次のように、ProtoBufでメモリーにシリアル化し、サイズを図るというようなプログラムを考えます。
        static void CheckSize(int[] array)
{
using(MemoryStream ms = new MemoryStream())
{
Serializer.Serialize(ms, array);
Console.WriteLine("{0} bytes", ms.Length);
}
}


●全て0の配列をシリアル化してみる
例えば、長さ1000、要素が全てint型の0の配列をProtoBufでシリアル化してみます。
        static void Main(string[] args)
{
//全て0の配列
Console.WriteLine("全て0");
int[] a1 = Enumerable.Repeat(0, 1000).ToArray();
CheckSize(a1);
}

結果:
全て0
2040 bytes
続行するには何かキーを押してください . . .

●2バイト→4バイトの切り替わりはどこ?
C#のintは符号付き32ビット変数なので、ビットの切り詰めをしていなければ32ビット=4バイト×1000=4000バイト近くになるはずです。値を変えてみましょう。今度はsbyteの上限の127とその1つ上の128で試してみます。ソースはさっきのEnumerable.Repeatの第1引数を変えただけなので省略。
結果:
全て127
2040 bytes
全て128
4080 bytes
続行するには何かキーを押してください . . .

というわけで、127が約2バイトでいられる上限のようです(普通2バイトっていうと0~65535まで、符号付きならその半分まで表現できるけど何に使ってるんだろう…)

●2^nをひたすら試す
ここからは以外と切り替わりません。どんどん数字を上げていって試してみます。

        static void Main(string[] args)
{
for(int i=15; i<=31; i++)
{
int x = (int)Math.Pow(2, i) -1;
Console.WriteLine("全て{0} (2^{1}-1)", x, i);
int[] a = Enumerable.Repeat(x, 1000).ToArray();
CheckSize(a);
}
}

結果:
全て32767 (2^15-1)
4080 bytes
全て65535 (2^16-1)
4080 bytes
全て131071 (2^17-1)
4080 bytes
全て262143 (2^18-1)
4080 bytes
全て524287 (2^19-1)
4080 bytes
全て1048575 (2^20-1)
4080 bytes
全て2097151 (2^21-1)
4080 bytes
全て4194303 (2^22-1)
8160 bytes
全て8388607 (2^23-1)
8160 bytes
全て16777215 (2^24-1)
8160 bytes
全て33554431 (2^25-1)
8160 bytes
全て67108863 (2^26-1)
8160 bytes
全て134217727 (2^27-1)
8160 bytes
全て268435455 (2^28-1)
8160 bytes
全て536870911 (2^29-1)
8160 bytes
全て1073741823 (2^30-1)
8160 bytes
全て2147483647 (2^31-1)
8160 bytes
続行するには何かキーを押してください . . .

2^21あたりから1個8バイトぐらいになるみたいですね。なんでこんな半端なところで切り替わるかはよくわかりません。

●マイナスの数字を試してみる
なんかいやな予感しますが-1を試してみます。
結果:
全て-1
16368 bytes
続行するには何かキーを押してください . . .

なんと倍以上になってしまいました(1個あたり約16バイト)。これはとても困ったことがおきます。変数に未初期化されていない値を含みたい場合、

 x = (正の値) 値が正常にされている場合、値が正の値限定
  = -1 初期化されていない場合

というようなのをやりたくなりますが、-1は最上位ビットに値を含んでいるため重くなってしまうようです。この対策を考えてみます。

●負の数で重くならない対策(1) ~int?型を使ってみる~
ぱっと思い浮かぶのが、null許容型のint?型を使う方法。こんなのシリアル化できるでしょうか? さっきのCheckSizeにint?[]型のオーバーロードを用意して試してみます。

            Console.WriteLine("全てNULL");
int?[] a = new int?[1000];
for (int i = 0; i < a.Length; i++) a[i] = null;
CheckSize(a);


結果:
全てNULL

ハンドルされていない例外: System.NullReferenceException: オブジェクト参照がオブ
ジェクト インスタンスに設定されていません。
場所 ProtoBuf.Meta.TypeModel.TrySerializeAuxiliaryType(ProtoWriter writer, Ty
pe type, DataFormat format, Int32 tag, Object value, Boolean isInsideList) 場所
c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:行 169
場所 ProtoBuf.Meta.TypeModel.SerializeCore(ProtoWriter writer, Object value)
場所 c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:行 188
場所 ProtoBuf.Meta.TypeModel.Serialize(Stream dest, Object value, Serializati
onContext context) 場所 c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:行 21
7
場所 ProtoBuf.Serializer.Serialize[T](Stream destination, T instance) 場所 c:
\Dev\protobuf-net\protobuf-net\Serializer.cs:行 86
: :

あ、はい。無理でしたね。

●対策(2) double.NaNを使ってみる
doubleの場合は、double.NaN(非数)という特別な値があるそうです。WPFの解説読んでて初めて知ったぞい

結果:
全てdouble.NaN
16288 bytes
全てdouble(0.0)
16288 bytes
全てdouble.MaxValue
16288 bytes
続行するには何かキーを押してください . . .

あ、はいintの場合と変わらないですね。ちなみに、double.NaNは==で比較すると全てFalseを返すという特別な仕様があるので、
double a = double.NaN;
Console.WriteLine(a == double.NaN);

とやると、なんとFalseが返ってきてしまいます。double.IsNaNを使うと正常に判定できるので、気をつけないとバグの温床になりそうですね。

●対策(3) 無効な値にも対応できる独自の構造体を作ってみる
無効な値が入力された場合も対応できる構造体を自分で作ってみます(MyNullableInt)。クラスでもいいけど、intみたく値参照できたほうがいいので構造体にしちゃいました。

    class Program
{
static void Main(string[] args)
{
Console.WriteLine("全てMyNullableInt.NullValue");
MyNullableInt[] a = Enumerable.Repeat(MyNullableInt.NullValue, 1000).ToArray();
CheckSize(a);

Console.WriteLine("全てMyNullableInt.MaxValue");
MyNullableInt[] b = Enumerable.Repeat(MyNullableInt.MaxValue, 1000).ToArray();
CheckSize(b);
}

static void CheckSize(MyNullableInt[] array)
{
using (MemoryStream ms = new MemoryStream())
{
Serializer.Serialize(ms, array);
Console.WriteLine("{0} bytes", ms.Length);
}
}
}

[ProtoContract]
public struct MyNullableInt
{
[ProtoMember(1)]
public int Value { get; set; }
[ProtoMember(2)]
public bool IsNull { get; set; }

private static MyNullableInt _nullValue;
private static MyNullableInt _maxValue;

public static MyNullableInt NullValue
{
get
{
return _nullValue;
}
}
public static MyNullableInt MaxValue
{
get
{
return _maxValue;
}
}

static MyNullableInt()
{
_nullValue = new MyNullableInt()
{
Value = 0,
IsNull = true,
};
_maxValue = new MyNullableInt()
{
Value = int.MaxValue,
IsNull = false,
};
}
}


結果:比較用にint.MaxValueをMyNullableIntに導入した場合と比較しました。
全てMyNullableInt.NullValue
4000 bytes
全てMyNullableInt.MaxValue
8000 bytes
続行するには何かキーを押してください . . .

あれ??intの場合よりも軽くなってる??? 原因はよくわからないですね…。

というわけで、ProtoBufで8バイトと16バイトの間でサイズを気にする場合は、無効な値として-1を導入のではなく、独自の(無効な値かどうかのフラグを含む)構造体を導入するのも一興だそうです。(ほとんどの場合こんな気にしないだろうけど)
スポンサーサイト
プロフィール

こしあん

Author:こしあん
(:3[____]
【TwitterID : koshian2】
【ほしい物リスト】http://goo.gl/bDtvG2

Twitter
カウンター
天気予報

-天気予報コム- -FC2-
カテゴリ
月別アーカイブ
最新記事
最新トラックバック
検索フォーム
リンク