Liberent-Dev’s blog

株式会社リベル・エンタテインメントのテックブログです。

ASP.NET Coreを初めて触りつつMemory Packを試してみた2

こんにちは!
システム開発部のK.Mです。

前置き

前々回前回のMemoryPackの記事にて、MemoryPackを色々触ってみましたが、まだ少しやり残したことがありましたので、やり残した分の内容となっております。

速度確認

公式では数倍違うという記載になっておりましたが、実際のところどれ位違うのかをMessagePackとMemoryPackで速度計測してみました。

下記の3ケースで後述しているデータ構造体40万件をサーバから送り、クライアント(Unity)にてデシリアライズ処理をする箇所にて計測しています。

  • MessagePack(未圧縮:約20MBのデータサイズ)
  • MemoryPack(未圧縮:約14MBのデータサイズ)
  • MessagePack(LZ4圧縮:約4MBのデータサイズ)
    • 念のため参考用

MemoryPackの圧縮がLZ4がデフォルトで記事作成している現在未対応になっているので、対応されるのを期待したいところです!

  • https://neue.cc/2022/11/04_memorypack.html

    MemoryPackの実装と統合された効率的な圧縮については、現在BrotliEncode/Decodeのための補助クラスを標準で用意しています。しかし、性能を考えるとLZ4やZStandardを使えたほうが良いため、将来的にはそれらの実装も提供する予定です。

データ構造

public partial class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}

Summaryのstring型は必ず半角英数字スペースで12文字が設定される形にしています。

計測結果

1回目 2回目 3回目 4回目 5回目 平均
MessagePack (未圧縮) 337ms 346ms 337ms 341ms 338ms 339.8ms
MemoryPack (未圧縮) 251ms 262ms 313ms 260ms 256ms 268.4ms
MessagePack (LZ4圧縮) 416ms 413ms 409ms 426ms 451ms 423ms

データ構成がよろしくなかったのか数倍という感じまでは行きませんでしたが、MemoryPackがやはり速いという結果になりました。
MemoryPackに標準でLZ4などの圧縮対応が入れば、送受信時のデータ容量が減り、MessagePackより速いという形となり、C#だけの環境であればMemoryPack一択になりそうです。

バージョン耐性

基本的にサーバとクライアント間で同じリクエストとレスポンスを定義したファイルを使っていれば、データ構成が異なっていて正常に処理が出来ないということは発生しないのですが、MemoryPackのReadMeにバージョン耐性の説明が存在するため、興味本位で色々と確認してみました。

POST対応

バージョン耐性を確認する前に前回、前々回のものですと、GETの対応しか入れていなかったのでPOSTの対応を入れます。

サーバ側

この辺りを参考にしてPOSTで貰った値をオウム返しで同じものを返すものを作成しました。

    [HttpPost(Name = "PostWeatherForecast2")]
    public ActionResult<TodoItemOut> Post(TodoItem todoItem)
    {
        TodoItemOut res = new TodoItemOut
        {
            Id = todoItem.Id,
            IsComplete = todoItem.IsComplete.ToString(),
            Name = todoItem.Name
        };

        return res;
    }

TodoItemとTodoItemOutの中身は下記のようになっております。

using System.Runtime.InteropServices;

using MemoryPack;

namespace TestMemoryPack;

[MemoryPackable]
public partial class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

クライアント側

POST用の処理を下記の様に用意しました。
SendWebRequest以降はGET時と同じような形となりますので省いております。

        TodoItem data = new TodoItem();
        data.Id = 1;
        data.IsComplete = true;
        data.Name = "test";

        // memorypackでシリアライズ
        byte[] reqData = MemoryPackSerializer.Serialize(data);

        var request = new UnityWebRequest("http://IPアドレス:ポート/WeatherForecast2", "POST");
        request.uploadHandler = (UploadHandler)new UploadHandlerRaw(reqData);
        request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/x-memorypack");
        request.SetRequestHeader("Accept", "application/x-memorypack");
        yield return request.SendWebRequest();

確認内容

いきなり結論からですが、ほとんどケースでシリアライズかデシリアライズ時にエラーが発生するか、エラーが出ない場合は無理やり型にはめ込んでいるので、データ壊れが発生しておりました。

サーバのリクエスト/レスポンス定義は変わらず、クライアントのリクエスト/レスポンスの定義を変更

運用時ほぼ発生しないはず。

  1. リクエストの順番変更
    • 結果:サーバ側でリクエスト受付時のシリアライズでエラー発生
    • Sequence reached end, reader can not provide more buffer.
  2. リクエストの項目追加
    • 最初に追加
      • 結果:サーバ側でリクエスト受付時のシリアライズでエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
    • 最後に追加
      • 結果:サーバ側でリクエスト受付時のシリアライズでエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
    • 途中に追加
      • 結果:サーバ側でリクエスト受付時のシリアライズでエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
  3. リクエストの型変更(項目数そのまま)
    • long -> string
      • 結果:サーバ側でリクエスト受付時にシリアライズでエラー発生
      • Sequence reached end, reader can not provide more buffer.
    • strnig -> long
      • 結果:longの値が0~2ならエラー出ずデータ壊れ、3以上ならエラー発生
      • 恐らく、構造体に合わせて単純にコピーしているだけなので、コピーできる場合はデータ壊れた状態になって、コピーできない場合はエラーになっているように感じる。データ構造によっては動きが異なってくる可能性がある箇所。
  4. レスポンスの順番変更
    • 結果:クライアント側でデシリアライズでエラー発生
    • Length header size is larger than buffer size, length: 数字.
    • 順番の変え方次第でエラーにならずにデータ壊れのケースもあった
  5. レスポンスの項目追加
    • 最初に追加
      • 結果:クライアント側でデシリアライズでエラー発生
      • Length header size is larger than buffer size, length: 数字.
      • Sequence reached end, reader can not provide more buffer.
      • 追加する型次第でエラー内容が変わっていた
    • 最後に追加
      • 結果:エラー発生せず。追加した項目はnull扱い。(数字だと0)
    • 途中に追加
      • 結果:クライアント側でデシリアライズでエラー発生
      • Sequence reached end, reader can not provide more buffer.
  6. レスポンスの型変更(項目数そのまま)
    • 結果:クライアント側でデシリアライズでエラー発生 か エラー発生せずにデータ壊れ
    • Sequence reached end, reader can not provide more buffer.

サーバのリクエスト/レスポンス定義を変更、クライアントのリクエスト/レスポンス定義は変わらず

こちらは運用時にミスをした場合にはありえそう。

  1. リクエストの順番変更
    • 結果:サーバ側のシリアライズ時にエラー発生 か エラー発生せずにデータ壊れ
    • Length header size is larger than buffer size, length: 数字.
  2. リクエストの項目追加
    • 最初に追加
      • 結果:サーバ側のシリアライズ時にエラー発生
      • Sequence reached end, reader can not provide more buffer.
      • Length header size is larger than buffer size, length: 数字.
    • 最後に追加
      • 結果:エラー発生せず。追加した項目はnull扱い。(数字だと0)
    • 途中に追加
      • 結果:サーバ側のシリアライズ時にエラー発生
      • Sequence reached end, reader can not provide more buffer.
  3. リクエストの型変更(項目数そのまま)
    • 結果:サーバ側のシリアライズ時にエラー発生 か エラー発生せずにデータ壊れ
    • Sequence reached end, reader can not provide more buffer.
  4. レスポンスの順番変更
    • 結果:クライアント側のシリアライズ時にエラー発生
    • Length header size is larger than buffer size, length: 数字.
    • Sequence reached end, reader can not provide more buffer.
  5. レスポンスの項目追加
    • 最初に追加
      • 結果:クライアント側のシリアライズ時にエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
    • 最後に追加
      • 結果:クライアント側のシリアライズ時にエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
    • 途中に追加
      • 結果:クライアント側のシリアライズ時にエラー発生
      • Current object's property count is 3 but binary's header maked as 4, can't deserialize about versioning.
  6. レスポンスの型変更(項目数そのまま)
    • 結果:クライアント側のシリアライズ時にエラー発生
    • Sequence reached end, reader can not provide more buffer.
    • Length header size is larger than buffer size, length: 数字.

最後に

リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。
もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/