LoginSignup
49
46

More than 5 years have passed since last update.

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

Last updated at Posted at 2012-12-26

26日目、@adachi_c です。こんにちは〜。

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

アプリ

シリアライズとはなにか

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

システムごとに、いろんな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に指定

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バイトをもうけて、送るのが大事。

クライアント動作時(クライアントボタン押す):

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++;
}

サーバ動作時(サーバボタン押す):

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メソッドがシリアライズ、デシリアライズを担当する。

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];
}

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

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

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;
}

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

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

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日目、おつきあいくださいましてありがとうございました!

49
46
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
46