この記事は Node.js Advent Calendar 2016 25日目の記事です。
遅くなり申し訳ありません。
目的
ゲームのサーバとしてNode.jsを使っていたのですが、応答のバイナリ化についてnative addonでどれだけ処理が改善できるのかを調査してみました。
Structure
ゲーム開発界隈はC,C++,C#と脈々とC系の言語が続いていて、それこそ大昔はサーバもC++で書いていたりしていました。サーバ・クライアントという異なるターゲット間で通信で受け渡すデータ構造を合わせるために構造体を上手いこと設計し、ヘッダをサーバクライアント共用にするといったこともやっていました。気を付けることは主にpaddingだけの話なんですが、昔はendianの問題とかもあったりとか…
これ自体まぁ面白くはあるのですが要点一つ掴めば誰でもできるし、あまりこういうこと自体特殊技能にするのも良くない。もうjsonでいいじゃないか、バイナリにこだわるならmsgpackでいいじゃないか、と思うようになったのが5年前位?Node.jsの存在を知ったのもそれからちょっとしてからだったかと思います。
さてそれから幾年月。Node.jsでサーバを書きながらここ1年悩んでいたのは応答データのシリアライズの重さ。この主因に応答データの大きさがありました。
しかしゲームならでわかもしれませんが、本当にゲームの通信って1API辺り応答がデカいデータになりがちです。
ここら辺ぶっちゃけ個人的意見はあるのですが、サーバの方で今あるものをどうしようと分析結果を眺めながら、1周回ってnative addonで構造体設計してバイナリ化するのが一番いいのかなと、思うようになりました。
そこで、native addonでバイナリを作ることで実際変換パフォーマンスがどれくらい改善するのか調査してみました。
対象
題材として以下のようなデータを作ってみました。
var logindata = {
player: {
id: "550e8400-e29b-41d4-a716-446655440000",
name: "test",
exp: 1
},
items: [{id:"550e8400-e29b-41d4-a716-446655440000", itemid:1, num: 1}]
};
itemsの長さは可変です。これを構造体にするならこんな感じ?
class LOGINDATA
{
public:
PLAYERDATA player;
int num_item;
ITEMDATA items[1];
};
class ITEMDATA
{
public:
char id[64];
int itemid;
int num;
};
class PLAYERDATA
{
public:
char id[64];
char name[32];
int exp;
};
以下のように変換のロジックを書いてみました。
NAN_METHOD(Serialize)
{
Isolate* isolate = Isolate::GetCurrent();
Handle<Object> login = Handle<Object>::Cast(info[0]);
Handle<Value> player_ = login->Get(String::NewFromUtf8(isolate, "player"));
Handle<Value> items_ = login->Get(String::NewFromUtf8(isolate, "items"));
Handle<Object> player = Handle<Object>::Cast(player_);
Handle<Array> items = Handle<Array>::Cast(items_);
size_t sizeof_buf = sizeof(LOGINDATA) + sizeof(ITEMDATA)*items->Length();
char* buf = new char[sizeof_buf];
LOGINDATA* logindata = (LOGINDATA*)buf;
player->Get(String::NewFromUtf8(isolate, "id"))->ToString()->WriteUtf8(logindata->player.id);
player->Get(String::NewFromUtf8(isolate, "name"))->ToString()->WriteUtf8(logindata->player.name);
logindata->player.exp = player->Get(String::NewFromUtf8(isolate, "exp"))->Uint32Value();
logindata->num_item = items->Length();
for (unsigned int ii=0; ii<items->Length(); ii++) {
Handle<Object> item = Handle<Object>::Cast(items->Get(ii));
item->Get(String::NewFromUtf8(isolate, "id"))->ToString()->WriteUtf8(logindata->items[ii].id);
logindata->items[ii].itemid = item->Get(String::NewFromUtf8(isolate, "itemid"))->Uint32Value();
logindata->items[ii].num = item->Get(String::NewFromUtf8(isolate, "num"))->Uint32Value();
}
info.GetReturnValue().Set(Nan::CopyBuffer((char*)buf, sizeof_buf).ToLocalChecked());
}
測定
以下のような測定コードを作成しました。
var addon = require('./build/Release/addon');
var logindata = {
player: {
id: "550e8400-e29b-41d4-a716-446655440000",
name: "test",
exp: 1
},
items: []
};
var test = function(cnt, use_addon) {
for(var ii = 0; ii < cnt; ii++) {
logindata.items.push({id:"550e8400-e29b-41d4-a716-446655440000", itemid:ii, num: ii})
}
s = addon.Serialize(logindata);
console.log(s.length);
console.time('loop');
for(var ii = 0; ii < 1000; ii++) {
if (use_addon) {
s = addon.Serialize(logindata);
} else {
s = JSON.stringify(logindata);
}
}
console.timeEnd('loop');
}
// 各item数によるパフォーマンスの差を比較する
var sample = [100,200,300,400,500,600,700,800,900,1000];
for (var ii=0; ii< sample.length; ii++) {
// test(sample[ii], true);
test(sample[ii], false);
}
結果。
num items | JSON.stringify | native addon |
---|---|---|
100 | 34.292ms | 81.262ms |
200 | 90.983ms | 233.420ms |
300 | 179.845ms | 470.435ms |
400 | 303.717ms | 782.321ms |
500 | 453.410ms | 1166.975ms |
600 | 646.167ms | 1675.138ms |
700 | 854.502ms | Killed... |
800 | 1111.940ms | |
900 | 1397.911ms | |
1000 | 1699.615ms |
native addonが途中で失敗しましたが、失敗を改善するまでもなくnative addonの方が重い!?
多分開放とか色々エラーが出ているのとnative addonの使い方が良くないのでしょうが…いやしかしちょっと夢がない…
結論
メリットとしてはクライアント側のデシリアライズ負荷は無くなるかと思うのですが、サーバ側のパフォーマンス改善は厳しそう、応答サイズはできるだけ小さくしてほしい…ということになってしまいました。
トリを頂いたのに夢のない結論で申し訳ないです。メリークリスマス。
もう少しコード改善して実験コード公開いたします。
※続きを書きました
http://qiita.com/takeswim/items/693a10428f6a8ad9980b