Liberent-Dev’s blog

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

アイ★チュウ最適化の話

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

先月の11月10日にNintendo Switch版「アイ★チュウ」が発売されました!

前回が好評だったため、今回もアイ★チュウの話をお届けします。
今回ですが、Android/iOS時代に発生した運用・開発時のアプリ制作の技術的な内容となりますので、少しとっつきにくいお話かもしれません。


こんにちは。
システム開発部のT.Iです。

祝:アイ★チュウswitch版発売!
2022年11月10日に発売されました。

本記事ではAndroid/iOS版のアイ★チュウ運営時に、マスターデータ1の一つ「リソースリスト」の内部処理で発生した問題と、その際にどのように改善したかを振り返ります。

基本的にアイ★チュウを運用していた当時(数年前)のお話となります。

リソースリスト内容

  • AssetBundle、サウンド、ADVスクリプト等ダウンロード対象のリソース情報が記述されたリスト
  • データ数は各OS(iOS,Android)21,000個程
    • 全体で約47,000件
  • データ構造

リソースリストデータ:改修前

  • csvjson形式シリアライズする2
  • アプリのjsonライブラリにはLitJsonを使用3
  • データサイズ
    • json生データ 1814 KB
    • 暗号化&圧縮 125 KB
  • アプリ内実装
    • 復号化の後、LitJsonでDictionary<string,DataClass>にデシリアライズして使用

リソースリスト運用 経過

  1. アイ★チュウ運営数年間はリソースリスト制御に問題は無く低スペック端末でも動いていた。
  2. 日々の更新で画像データなどが増えていきリソースリストは増大。
  3. 社内チェックの際に「タイトルから遷移が重くなってるかも・・・?」という声がちらほら。
  4. ある日の更新から「この前の更新からタイトルから遷移できず落ちる」との お問い合わせが発生。
  5. 調査したところリソースリストの展開とデシリアライズでメモリが足りず落ちている模様。
    (落ちなかったとしてもデシリアライズに数秒要しUXは良くない状況)

改修作業開始

改修内容

問題点

展開+デシリアライズで一時的にメモリを40MB程消費(当時はRAM1GB未満の端末はざらにあった)
全てがテキスト形式であるjsonに起因する事象であり、json継続は厳しいという判断に。

バイナリ形式に変更へ

テキストとバイナリの違い

  • テキスト形式

    • 人間に理解しやすい形式
    • CPU的にはコスト高
    • データサイズは大きくなる
  • バイナリ形式

    • CPUに理解しやすい形式
    • 人間が理解するには内容の把握が必要
    • データサイズはテキスト形式よりも軽量
例)128という数値の場合
 ・テキスト形式は「1」「2」「8」の3byte使用。
  パッと見、人間が「128」とすぐにわかる

 ・バイナリ形式 0x80   1byte使用。
  パッと見、わかりづらいがデータサイズは小さい。 

バイナリ形式変更のメリット

  • 前述の通りデータサイズが軽量化できる
    • データ取得までの待ち時間が減る
  • データが軽量化されたため展開時とデシリアライズ時のメモリ消費が減る
  • CPUが適応しやすいためデシリアライズ時間がテキスト形式と比較して短縮できる

バイナリ形式変更のデメリット

  • 人間に理解しづらい管理しづらい

だが待ってほしい

本作でjsonはあくまで中間データで可読性は重視していません。
そもそもデータ節約のために最適化したjsonは見やすいでしょうか?
前述の通りkeyは1文字にしており、改行も行っていません。

正直な所、可読性は低い・・・!

つまりバイナリ形式のデメリットはデメリットにはならないのでは?

と、(やや逆説的に)判断しバイナリ形式に迷わず進むことに決定しました。

MessagePack検証

  • バイナリ形式のデータフォーマット
  • jsonと比較してデシリアライズ時間短縮、データサイズ軽量化、消費メモリ軽減が期待できる
  • 古橋貞之さんが開発
  • MessagePack for Unity で検証
  • スマホゲーム業界的には台頭してきた時期ではあるもののまだ情報は多くなかった頃

MessagePack for Unity 検証結果

  • データサイズは劇的に改善
  • シリアライズでの使用ヒープ(メモリ)もかなり改善
  • シリアライズ時間がもうちょっと早くならないと改善とは言えない

概ね改善が見られましたがデシリアライズ時間に更に改善が必要という結果になりました。
別の汎用的なフォーマットも考慮しましたが改修期間の限られていたため、
いっそアイ★チュウに特化した独自バイナリ形式で改善しよう!という方針にしました。4

独自バイナリ形式着手

独自バイナリ形式の構造を設計するにあたり問題点を分析しました。

問題分析

  • 汎用性によるオーバーヘッド
  • 同内容の文字列が多々ある
  • シリアライズとメモリの関係性

これらの問題点に対してどのように改善したかを1つずつ見ていきます。

汎用性

世に提供されている多くのデータフォーマットは様々な環境や状況に 幅広く対応するための仕組みが組み込まれています。
この仕組みのおかげで使用者は受け皿を用意するだけで問題無くデシリアライズを行えます。
例えば、各データバイナリの先頭1byteにデータ判別用の数値を用意して型がわかるようにしていたり等です。
この判別用のデータによりデシリアライザがいい感じに復元してくれています。
(以下画像は例です。実際のものではありません)

しかしアイ★チュウではこの汎用性を捨てリソースリスト用に特化しました。
リソースリストのデータ仕様は以下の画像ように決まっているので、実装者がデータ仕様に即したパーサを実装しました。
これにより判別用の数値は不要になります。
判別用の数値が不要になったことで以下の改善が見られました。

  • データ軽減
  • データ復元のため処理負荷軽減


※データ判別用の数値は不要に。

文字列


端的にいうとデータベースの正規化になります。
リソースリストを見渡すと複数のデータに同じ文字列(上記だと「adv/love」という文字列の部分)が含まれていました。
同内容の文字列でも文字列分変換処理が行われるのでかなりの無駄が発生していました。
バイナリから文字列への変換はコスト高いので回数を減らすに越したことはありません。

同内容の文字列はリストにまとめ、実データには文字列では無く文字列リストのインデックス値を持つように改修しました。

この対応により以下の改善が見られました。

  • データ軽減 
  • 処理負荷改善
  • 消費メモリ軽減

シリアライズとメモリの関係性

シリアライズは大まかに以下の順序で行われます。

  1. データバイナリファイル読み込み(バイナリをメモリに展開)
  2. メモリに展開したバイナリをデシリアライズ
  3. シリアライズが完了したらメモリのバイナリを解放

一見すると何も問題は無いと感じますがメモリに展開したバイナリに欲しいデータは含まれているためデシリアライズして受け皿のにデータを流し込むよりもバイナリを直接参照した方が無駄が無いと考え以下の改修を行いました。

  • 改修前

  • 改修後

受け皿にデータを持つのでは無く、データはあくまでバイナリに持ち受け皿にはバイナリデータのアドレスのみ持つ形にし、必要に応じてデータを都度取得するように変更しました。

この対応により以下の改善が見られました。

理論上メモリ消費はバイナリのサイズ+アドレスのみDictionaryとなり、デシリアライズの負荷軽減にもなりました。

独自形式デメリット

ロマンがある独自形式ですが、デメリットはとても多いです。

  • 全てを設計、実装する必要がある
  • 特化すればするほど使いまわし出来ない
  • 変化/変更に弱い。変更に対する修正箇所が多い

2022年現在、高速で扱いやすい汎用的なデータフォーマットは数多くありますのでデータフォーマットを決めた上で最適化を進めた方が開発的にはベターかと思います。

結果発表

LitJsonと比較して独自形式がどの程度改善できたのか、本記事を記述にあたり、いい機会なので他の汎用データフォーマットも計測しました。

参考になれば幸いです。
検証端末はGalaxy A7です。

使用データ

以下のデータ構成、約47000件のリストです。

項目
ファイル名 文字列
ファイルパス 文字列
ファイルサイズ 数字
ファイルタイプ 数字(0~255)
ダウンロード種別 数字(0~255)
特定時に使用するフラグ(チュートリアルフラグ) 数字(0~255)
サブパラメータ 数字
更新バージョン 数字

留意事項

  • 独自形式はアイ★チュウ用に特化/最適化しているのに対し、各データフォーマットは標準的な使用のみで、各フォーマット用のデータの最適化や工夫は行っていません。
    そのため以下の結果はフェアなものでは無くやや恣意的な検証となります。
  • シリアライズの検証となります。
    シリアライズ後のデータ制御(抽出、ソート等)の検証は含みません。
  • 本検証のデータは暗号化や圧縮は行っていません。

計測項目

デシリアライズ完了時間  :デシリアライズ完了までの秒数
展開メモリ        :デシリアライズに要したメモリ(単位MB)
常駐メモリ        :デシリアライズ後のデータで使用するメモリ(単位MB)
ファイルサイズ      :シリアライズ後のデータサイズ(単位KB)

LitJson

リリース時のアイ★チュウで使用していたJsonライブラリです。
本検証は当時使用したバージョンで行っています。

■結果

デシリアライズ完了時間:2.26 秒
展開メモリ    :8.66 MB
常駐メモリ    :3.16 MB
ファイルサイズ  :1814 KB

JsonUtility

Unity5頃にUnityが正式サポートしたJsonシステムです。

■結果

デシリアライズ完了時間:0.43 秒
展開メモリ    :9.90 MB
常駐メモリ    :3.26 MB
ファイルサイズ  :2813 KB

Utf8Json

C#最速のJsonリアライザと銘打たれたJsonシステムです。

■製作者

neue ccさん(河合 宜文さん)5

■結果

デシリアライズ完了時間:0.21 秒
展開メモリ    :7.61 MB
常駐メモリ    :3.47 MB
ファイルサイズ  :1814 KB

MessagePack for c#

C#用の非常に高速なMessagePackシリアライザーです。

■製作者

neue ccさん(河合 宜文さん)

■結果

デシリアライズ完了時間:0.19 秒
展開メモリ    :4.73 MB
常駐メモリ    :3.19 MB
ファイルサイズ  :976 KB

SQLite

オープンソースで軽量なデータベースシステムです。
SQLiteUnityKitで検証しました。

■結果

デシリアライズ完了時間:0.03 秒 ※1レコード取得に要した秒数
展開メモリ    :0.29 MB
常駐メモリ    :0.75 MB
ファイルサイズ  :1804 KB

■備考

  • 他のデータ方式とは性質が異なるので検証としては比較対象にしないほうが良かったかもしれない
  • ピンポイントの情報取得なら群を抜いて数値が良い
  • 一方でクエリ結果が多量にあると他よりもメモリを使う(全取得で14MBくらい)

Flatbuffers

Googleが開発したシリアライザです。
2015年当時「最速」との情報がありました。

■結果

デシリアライズ完了時間:0.13 秒
展開メモリ    :6.46 MB
常駐メモリ    :1.91 MB
ファイルサイズ  :2083 KB

ScriptableObject

Unity標準のデータコンテナです。テキスト形式で検証しました。

■結果

デシリアライズ完了時間:0.39 秒
展開メモリ    :8.04 MB
常駐メモリ    :6.96 MB
ファイルサイズ  :2204 KB

■備考

  • バイナリ形式は未検証です。

独自形式(当時)

本記事に記述した改修を行ったデータ形式です。

■結果

デシリアライズ完了時間:0.09 秒
展開メモリ    :3.23 MB
常駐メモリ    :2.53 MB
ファイルサイズ  :665 KB

独自形式(2022)

せっかくなので当時の独自形式をさらに最適化しました。
何を最適化したかは後述します。

■結果

デシリアライズ完了時間:0.04 秒
展開メモリ    :2.49 MB
常駐メモリ    :1.72 MB
ファイルサイズ  :665 KB

独自形式(2022) 改良点

  • DictionaryのKeyを見直しました。
    以下のneueさんのスライド記述内容にガッツリ該当していたため、Stringをやめました。(あわせてFarmHashも導入)

    引用:「CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する」

  • Before
    シリアライズ時、Keyのために文字列変換を行っていました。 データ取得時、リソースネームをkeyとして値取得をしています。

  • After
    シリアライズ時、ハッシュ値をkeyしました。 データ取得時、リソースネームをbyte配列に変換し、そこからハッシュ値を算出しkeyを取得しました。

結果一覧

個人的な見解

どのデータフォーマットを使うかデータ構造や運用方法で左右されますが

  • Messagepack for c#
  • Sqlite
  • ScriptableObject

を使用して最適化していくのが良いと感じました。(ただしライセンスには注意しましょう)
デメリットの項目で記述した通り、独自形式は速度面では期待できますが、作業量の多さと保守性の低さがネックになります。
上記のデータフォーマットを使用し効率化して、その分の時間をゲームの面白さやUXに注力した方が良いと思います。

今回の検証結果が参考になれば幸いです。


5~6年前の別案件の開発時にUnity側でどうやってJson扱えば良いのかを苦労したのを思い出しました。
当時はUnity標準でJsonが扱いづらかったものです。

今回の件で調べ物をしている時、11月4日にneueさんがまた新しいシリアライザー(MemoryPack)を作っており、非常に驚きました。
サーバの実装がC#ならMemoryPackが標準になる日が来るかもしれませんね。


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


  1. マスターデータ:アプリ内から参照する共通の数値やデータ
  2. 正確にはシリアライズしたデータを暗号化&圧縮したデータを使用
    • サイズ軽減のためkeyは1文字
  3. 当時はUnityがjsonを正式対応する前
  4. 数値は当時の結果です。現在はより最適化されて高速化していると思われます。
  5. 技術記事や動画等いつも参考にさせていただいております・・・!