JSONとMessagePack、シリアライズ性能比較 under the iOS

26日目、[@adachi_c](http://twitter.com/adachi_c "adachi_c") です。こんにちは〜。

今日は2台のiPhoneで通信して、JSONとMessagePackのシリアライズ、デシリアライズの速度を計る計測アプリ作ったんで、見てってください。

![アプリ](http://s1.gazo.cc/up/s1_34460.png "アプリ")

## シリアライズとはなにか

シリアライズというのは何かというと、他の環境で復元（デシリアライズ）できるように、環境依存なデータ構造を、システム間で相互理解できる形式に変換することです。

システムごとに、いろんなOS、言語、新旧の違いなどがあるかと思いますが、それぞれが相互運用性を維持するために、共通のメッセージ表現手法が必要です。それがJSONだとか、XMLのDOMやSAXに該当します。シリアライズ形式によっては、型情報を持っていることもあったりします。

OSや言語が異なるシステム間でAPIを設計するとき、このシリアライズを使うことが有効になるわけです。


## どのシリアライズ方式をとるか

では、様々な種類のメッセージ表現手法のうち、どういうシリアライズ方式を選べばよいのでしょうか？

この話をするまえに、まず、次の論文はすごい目から鱗というか、シリアライズ関係する人はとりあえず今日中に1~3章だけでも必読だと思いました。日本語で書いてあるし。
http://syuki.skr.jp/files/201204041/furuhashi-master-last-iso-pdfa.pdf

これによると、シリアライズ選定では、次の3点がポイントになるそうな。

**  サービスはいろんな言語がくみ合わさってできてる＝＞多言語に対応する
**  スケールアウト型の分散システムは通信量がめちゃめちゃ多い=>圧縮効率が大事
**  古いシステムとも通信したい＝＞実装が簡単じゃないとダメ

という点で、考えると、今あるほとんどのシリアライズ方式には問題があるみたいで、
例えばJSONだと実装は簡単だけど、遅いし、型情報をもたない、という問題があります。Thriftとかだと効率はいいけど、IDLを別途実装しないとダメ。IDLというのは、型情報を相手に通知するための言語です。早さというのと、実装の難しさというのはトレードオフ関係にあるみたいですね。

で、MessagePackというのがこの論文の方が考えたフォーマットで、DQ10のバックエンドなどでも使われているという話も聞きました。型情報もってるし、早いという。これだけ聞いたら素晴らしいですね。

というわけで、今回はiPhone対iPhoneで簡単な通信サーバ、クライアントを実装して、このMessagePackと、JSONで送受信をやって、ふーんやっぱりね。って結果が出るのを確認し、そのあとでDQ10を起動してMessagePackのありがたみを体感したような気分になりたいと思います。

## ソースの場所

というわけで、1Mバイトの文字列が入ったNSDictionaryをシリアライズし、サーバに送信して、デシリアライズしてNSDictionaryを取り出すという簡単なアプリを作りました。

ソース：
https://github.com/adachic/Seriarize_compere

## サーバ、クライアント実装

GCDAsyncSocketで簡単に実装。
下記のように、アプリは一つのViewControllerにGCDAsyncSocketを持って、socketのデリゲートをViewControllerに指定

````Objective-C:ViewController.m
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.resultLog = [NSMutableString string];
    
    [self.cl_button addTarget:self
            action:@selector(exec_cl:) forControlEvents:UIControlEventTouchUpInside];
    [self.sv_button addTarget:self
            action:@selector(exec_sv:) forControlEvents:UIControlEventTouchUpInside];

    self.sum = 0;
    /*デリゲートを設定*/
    self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
```

送受信時、データが大きいと一度のチャンクで受信しきれないことがあるので、パケットの先頭にレングス4バイトをもうけて、送るのが大事。

クライアント動作時（クライアントボタン押す）：

````Objective-C:ViewController.m
- (void)exec_cl:(UIButton*)button
{
    [self.sv_button setEnabled:NO];
    self.label.text = @"running in Client mode";
    NSError *err;
    /*相手のiPhoneにつなぐ*/
    if(![self.socket connectToHost:DEV_SV_HOST onPort:DEV_SV_PORT error:&err] && !times){
        NSLog(@"connection error : %@",err);
    }

    /*テストデータの生成*/
    :
    :
   NSInteger datalen = [packed length];
    NSMutableData *d = [NSMutableData dataWithLength:0];
    /*データの長さを先頭4バイトに書く*/
    [d appendBytes:&datalen length:4];
    [d appendData:packed];


    /*サーバに送信*/
    [self.socket writeData:d withTimeout:-1 tag:TAG_WRITE];
    times++;
}
```

サーバ動作時（サーバボタン押す）：

````Objective-C:ViewController.m
- (void)exec_sv:(UIButton*)button
{
    [self.cl_button setEnabled:NO];
    self.label.text = @"running in Server mode: ready";

    NSError *error;
    /*listen,ポート番号で待ち受ける*/
    if (![self.socket acceptOnPort:DEV_SV_PORT error:&error])
    {
        NSLog(@"I goofed: %@", error);
    }
}

/*accept時のハンドラ*/
- (void)socket:(GCDAsyncSocket *)sender didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
    /*acceptしてnewSocketが得られるので、newSocketそこから先頭4バイト読む*/
    [newSocket readDataToLength:4 withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_LENGTH];

}

/*読み出し時のハンドラ*/
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    int size = 0;

    /*先頭4バイト読み取りに成功*/
    if (tag == TAG_READ_LENGTH){
        [data getBytes:&size length: sizeof(size)];
        NSLog(@"recvd size=%d",size);
        /*本体の長さがわかったので、読み取る*/
        [sender readDataToLength:size withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_DONE];
        return;
    }
    
    /*本体の読み取りに成功*/
    if (tag == TAG_READ_DONE){
        NSDate *startDate = [NSDate date];
　　　:
       /*以下、受信処理*/
```

## シリアライズ、デシリアライズ実装

以下の4メソッドがシリアライズ、デシリアライズを担当する。

````Objective-C:ViewController.m
- (NSMutableArray *) decodeDataJSON:(NSData *)data
{
    return  [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
}

- (NSMutableArray *) decodeDataMessagePack:(NSData *)data
{
    return [data messagePackParse];
}

- (NSData *) encodeDataJSON:(NSMutableDictionary *)dict
{
    return [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
}

- (NSData *) encodeDataMessagePack:(NSMutableArray *)dict
{
    return [dict messagePack];
}
```

あとはテストデータを作って、これらのメソッドにぶち込み、その前後の時間をはかる。これは送信側。シリアライズ時間をとり、ログ表示し、平均表示する。通信にかかる時間は環境によって変わると思うので、今回はやっていない。

##シリアライズ時間計測実装

````Objective-C:ViewController.m
- (void)exec_cl:(UIButton*)button
{
    [self.sv_button setEnabled:NO];
    self.label.text = @"running in Client mode";
    NSError *err;
    
    if(![self.socket connectToHost:DEV_SV_HOST onPort:DEV_SV_PORT error:&err] && !times){
        NSLog(@"connection error : %@",err);
    }

    /*テストデータの生成:1Mバイトの文字列をディクショナリにぶちこむ*/
    NSMutableDictionary *mdic = [NSMutableDictionary dictionary];
    [mdic setObject:[self makeStringAZ:300000] forKey:@"key1"];
    [mdic setObject:[self makeStringAZ:300000] forKey:@"key2"];
    [mdic setObject:[self makeStringAZ:400000] forKey:@"key3"];
    
    NSDate *startDate = [NSDate date];
 
#ifdef TEST_MESSAGEPACK
    NSData *packed = [self encodeDataMessagePack:mdic];
#elif TEST_JSON
    NSData *packed = [self encodeDataJSON:mdic];
#endif
    NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:startDate];
    self.sum += interval;
    /*アベレージの表示*/
    self.label.text = [NSString stringWithFormat:@"ave:%lf",self.sum/(times+1)];
    NSLog(@"%02d:encode time is %lf ",times, interval);
    [self.resultLog appendString:[NSString stringWithFormat:@"%02d:encode time:%lf\n",times, interval]];
    self.textview.text = self.resultLog;
    
    NSInteger datalen = [packed length];
    NSMutableData *d = [NSMutableData dataWithLength:0];
    [d appendBytes:&datalen length:4];
    [d appendData:packed];
    [self.socket writeData:d withTimeout:-1 tag:TAG_WRITE];
    times++;
}

/* aからzまでを繰り返す文字列を作る */
- (NSString *)makeStringAZ:(NSInteger)length
{
    NSMutableString *str = [NSMutableString string];
    unichar a = 0;
    for (int i=0; i<length; i++) {
        a = 'a'+i%26;
        [str appendString:[NSString stringWithCharacters:&a length:1]];
    }
    return str;
}
```

##デシリアライズ時間計測実装

次にデシリアライズ時間。受信側で、ログ表示し、平均表示する。

````Objective-C:ViewController.m
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    int size = 0;

    if (tag == TAG_READ_LENGTH){
        [data getBytes:&size length: sizeof(size)];
        NSLog(@"recvd size=%d",size);
        [sender readDataToLength:size withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_DONE];
        return;
    }
    
    if (tag == TAG_READ_DONE){
        NSDate *startDate = [NSDate date];

#ifdef TEST_MESSAGEPACK
        NSLog(@"recv MessagePack");
        NSMutableDictionary *dict = [self decodeDataMessagePack:data];
#elif TEST_JSON
        NSLog(@"recv Json");
        NSMutableDictionary *dict = [self decodeDataJSON:data];
#endif
        
        NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:startDate];
        self.sum += interval;
        self.label.text = [NSString stringWithFormat:@"ave:%lf",self.sum/(times+1)];
        NSLog(@"%02d:decode time is %lf ",times, interval);
        [self.resultLog appendString:[NSString stringWithFormat:@"%02d:encode time:%lf\n",times, interval]];
        self.textview.text = self.resultLog;

        times++;
        [sender readDataToLength:4 withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_LENGTH];
 
        return;
    }
}
```

## 結果(iPhone4Sで計測)

このアプリをつかって、1Mバイトのシリアライズ、デシリアライズをそれぞれ5回やって平均をとった。以下のような結果になった。

*JSON 
    -encode
    0.032196s
    -decode
    0.022391s

*msgpack
    -encode
    0.009353s
    -decode
    0.021298s

## おわりに

今回はJSONとMessagePackだけでしたが、性能的にはMessagePackが優れてそうです。が、MessagePackはエンコード早いが、デコードが結構遅いこともわかりました。

Thriftまでやりたかったんですが、そっちはちょっと難航しまして..今後実装追加したいと思います。

ではでは、26日目、おつきあいくださいましてありがとうございました！