Edited at

C#でFlatBuffers

More than 3 years have passed since last update.

WEB+DB Vol.86でFlatBuffersが紹介されていたのでC#で試したくなりました。

記事を書いた時点でのFlatBuffersのバージョンは 1.1.0 です。


FlatBuffersとは

FlatBuffersとはGoogleが開発したシリアライザであり、ハイパフォーマンスが要求されるようなゲームに適しているとされています。


特徴


  • パースやunpackingが不要でデータにアクセスできる

  • メモリ効率が良く、高速

  • フレキシブル - 後方互換性を備える

  • 厳密な型指定

  • クロスプラットフォーム(C++11/Java/C#/Go)


利用の要点


  1. スキーマファイルを作成する

  2. C#クラスを生成するためにflatc(スキーマコンパイラ)を使う

  3. フラットなバイナリバッファーを構成するためにFlatBufferBuilderを使う

  4. シリアライズされたバイナリバッファーを何処かへ送信または保存する

  5. 逆に読み込むときにはバイナリバッファーからルートオブジェクトのポインタを取得し、object.Filed()のように簡単に取り出せる


1. IDLを定義

まず、利用するにはシリアライズ対象のデータを定義する必要があります。


sample.fbs

// example IDL file

namespace MyGame;

enum Color:byte { Red = 0, Green, Blue = 2 }

union Any { Monster } // add more elements..

struct Vec3 {
x:float;
y:float;
z:float;
}

table Monster {
pos:Vec3;
mana:short = 150;
hp:short = 100;
name:string;
friendly:bool = false (deprecated);
inventory:[ubyte];
color:Color = Blue;
}

root_type Monster;



2.スキーマをコンパイル

スキーマコンパイラ(flatc)でコンパイルする。コンパイルすることでクラスファイルが生成される。

flatc.exeはここからZIP取得するか自分でビルドする。



$ flatc.exe -n sample.fbs



※「-n」オプションは(.NETから)C#のクラスを生成する意味。「-j」であればJavaのクラスが生成される。「-c」はC++。


参考までに生成されたクラスの例


MyGame/Monster.cs

// automatically generated, do not modify

namespace MyGame
{

using FlatBuffers;

public sealed class Monster : Table {
public static Monster GetRootAsMonster(ByteBuffer _bb) { return GetRootAsMonster(_bb, new Monster()); }
public static Monster GetRootAsMonster(ByteBuffer _bb, Monster obj) { return (obj.__init(_bb.GetInt(_bb.position()) + _bb.position(), _bb)); }
public Monster __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; return this; }

public Vec3 Pos() { return Pos(new Vec3()); }
public Vec3 Pos(Vec3 obj) { int o = __offset(4); return o != 0 ? obj.__init(o + bb_pos, bb) : null; }
public short Mana() { int o = __offset(6); return o != 0 ? bb.GetShort(o + bb_pos) : (short)150; }
public short Hp() { int o = __offset(8); return o != 0 ? bb.GetShort(o + bb_pos) : (short)100; }
public string Name() { int o = __offset(10); return o != 0 ? __string(o + bb_pos) : null; }
public byte Inventory(int j) { int o = __offset(14); return o != 0 ? bb.Get(__vector(o) + j * 1) : (byte)0; }
public int InventoryLength() { int o = __offset(14); return o != 0 ? __vector_len(o) : 0; }
public Color Color() { int o = __offset(16); return o != 0 ? (Color)bb.GetSbyte(o + bb_pos) : (Color)2; }

public static void StartMonster(FlatBufferBuilder builder) { builder.StartObject(7); }
public static void AddPos(FlatBufferBuilder builder, int posOffset) { builder.AddStruct(0, posOffset, 0); }
public static void AddMana(FlatBufferBuilder builder, short mana) { builder.AddShort(1, mana, 150); }
public static void AddHp(FlatBufferBuilder builder, short hp) { builder.AddShort(2, hp, 100); }
public static void AddName(FlatBufferBuilder builder, int nameOffset) { builder.AddOffset(3, nameOffset, 0); }
public static void AddInventory(FlatBufferBuilder builder, int inventoryOffset) { builder.AddOffset(5, inventoryOffset, 0); }
public static int CreateInventoryVector(FlatBufferBuilder builder, byte[] data) { builder.StartVector(1, data.Length, 1); for (int i = data.Length - 1; i >= 0; i--) builder.AddByte(data[i]); return builder.EndVector(); }
public static void StartInventoryVector(FlatBufferBuilder builder, int numElems) { builder.StartVector(1, numElems, 1); }
public static void AddColor(FlatBufferBuilder builder, Color color) { builder.AddSbyte(6, (sbyte)(color), 2); }
public static int EndMonster(FlatBufferBuilder builder) {
int o = builder.EndObject();
return o;
}
public static void FinishMonsterBuffer(FlatBufferBuilder builder, int offset) { builder.Finish(offset); }
};

}



3.FlatBuffersライブラリを準備

VisualStudioで何かライブラリを利用する場合はまずnugetパッケージがないか検索するものですが、FlatBuffersにはnugetパッケージがありません。(不便・・・)

パッケージがないのでGitHubからZIPをダウンロード、またはgitでcloneしてライブラリをロードするしかありません。(nugetパッケージ化を希望)

取得したソースコード中にはJavaやGo用のライブラリも含まれていますが、今回はC#での利用なので、net/FlatBuffers以下のフォルダを利用したいサンプルプロジェクトの中に放り込んでプログラムから利用できるようにしておきます。


4.シリアライズとデシリアライズ


Program.cs

using FlatBuffers;

internal class Program
{
private static void Main()
{
//シリアライズ
var fbb = new FlatBufferBuilder(1);
int str = fbb.CreateString("MyMonster");
Monster.StartMonster(fbb);
Monster.AddName(fbb, str);
Monster.AddHp(fbb, 80);
Monster.AddPos(fbb, Vec3.CreateVec3(fbb, 1.0f, 2.0f, 3.0f));
int mon = Monster.EndMonster(fbb);
Monster.FinishMonsterBuffer(fbb, mon);
var buf = fbb.DataBuffer();

//デシリアライズ
Monster monster = Monster.GetRootAsMonster(buf);
Console.WriteLine(monster.Name()); //-->MyMonster
Console.WriteLine(monster.Hp()); //-->80
Console.WriteLine(monster.Pos().Y()); //-->2
}
}



4.ベンチマーク / パフォーマンス

以下の表はここから引用しています。

FlatBuffers (binary)
Protocol Buffers LITE
Rapid JSON
FlatBuffers (JSON)
pugixml
Raw structs

Decode + Traverse + Dealloc (1万回, 秒)
0.08
302
583
105
196
0.02

Decode / Traverse / Dealloc (内訳)
0 / 0.08 / 0
220 / 0.15 / 81
294 / 0.9 / 287
70 / 0.08 / 35
41 / 3.9 / 150
0 / 0.02 / 0

Encode (1万回, 秒)
3.2
185
650
169
273
0.15

割り当てフォーマットサイズ (通常 / zlib圧縮, bytes)
344 / 220
228 / 174
1475 / 322
1029 / 298
1137 / 341
312 / 187

デコードされたデータを保存するのに必要なメモリ (bytes / blocks)
0 / 0
760 / 20
65689 / 4
328 / 1
34194 / 3
0 / 0

デコード時に割り当てられる一時的なメモリ (KB)
0
1
131
4
34
0

生成されたソースコードのサイズ (KB)
4
61
0
4
0
0

手書きされたコードのフィールドアクセス
型指定されたアクセサ
型指定されたアクセサ
手動エラーチェック
型指定されたアクセサ
手動エラーチェック
型指定だが安全ではない

ライブラリソースコードサイズ (KB)
15
some subset of 3800
87
43
327
0


  • msgpackとの比較も欲しい

  • そもそもデコードやエンコードをしないので速度が桁違い


所感

IDL作成やシリアライズ手順の多さなど、開発に手間がかかるので、開発効率以上にパフォーマンスが要求されるような全体最適が求められる部分では利用価値があると思います。

今のところ既存のシリアライザに比べると開発コストが高いので、そこが改善されれば採用するプロジェクトが増える気がします。

正直扱いづらい。きっと誰かがGoogle純正の実装以外にC#らしいFlatBuffersを実装してくれるだろうと期待しています。