【Unity/C#】DictionaryでTryGetValueのメソッドを使って処理速度を比較
- 2024.11.14
- C#
- C#, Dictionary, Unity, 開発
仕事も自分の開発も忙しいぜウッヒョーとかやってたらもう10ヶ月くらいブログ書いてなかったんですね……。流石にこっちも放置するのはまずいよね、と一念発起してWordPressの編集画面を開いてみることに。
しばらくブランクがあるので、書きたいネタはそれはそれはたくさんあるのですが、それをまとめる力が衰えていて何から書いていいやら迷ってしまっています。が、そんな時こそ性能比較でリハビリしていくのが良いでしょう、と思い立ってのこのネタです。コード書いて、データ取って、比較して、結論出して終わり、という明快な流れがあるのはありがたいんです。
というわけで、ここではC#のDictionaryに値を追加する際の各種方法、値を取得する際の各種方法を比較してみたいと思います。もうn番煎じ(n = たくさん)ですが、自分の環境を使って性能確認の追試験をしてみるのも大切なので、2024年11月頃に性能比較してみるとこうなります、というのを残したいと思います。
環境
MacBook Pro 2023 Apple M2 Max
Unity6 (6000.0.26f1) Silicon
比較するもの
C#のDictionaryについて、
- 辞書へ値を追加
- 辞書から値を取得
の処理時間をそれぞれ確認します。
値を追加する際は、
- TryAddを使って値を追加
- ContainsKeyでキーの有無を確認してから追加
の2パターンで比較してみましょう。今回の比較においては、値の追加のみを主軸として、値の更新は対象外とします。というのも、TryAddは辞書内に指定したキーが存在していたら何もしないので、更新はしてくれないんですよね。Addという名前が入っているのに勝手に更新されても泣いてしまうけど。
辞書から値を取得する際は、
- TryGetValueを使って値を取得
- ContainsKeyでキーの有無を確認してから値を取得
の2パターンを比較します。
結果の要約
C#のDictionaryに関して、辞書に値を追加する処理においては、ContainsKeyでキーの有無を確認してから追加するより、TryAddのメソッドを使って追加した方が速くなりました。
辞書から値を取得する処理においては、ContainsKeyでキーの有無を確認してから取得するより、TryGetValueのメソッドを使って取得した方が速くなりました。
値の追加時、値の取得時にはTry系メソッドを使うのがいい感じです。ただし、値の更新を含む場合はContainsKeyを使うのが無難です。
コード
調子に乗って色々便利になるよう改造していたらとても長くなってしまったので、以下のリポジトリをご覧ください。コメントで解説を加えているので、Updateあたりから出現したメソッドを辿っていただけると良いかもしれません。
1フレーム目だと他のクラスの処理と重なってしまうので、ゲーム開始後10フレーム目に処理を行うようにしています。
値の追加に関しては、
- TryAddを使って追加するもの
- ContainsKeyを使ってキーの存在確認をしてから追加するもの
のメソッドを作って処理時間を計測しています。リポジトリに上げたコードだと、重複キーありの辞書に追加するメソッドも作ってありますが、それに関しては別ページで扱いたいと思います。
また、値の取得については、
- TryGetValueを使って取得するもの
- ContainsKeyを使ってキーの存在確認をしてから値を取得するもの
の4つのメソッドを作って処理時間を計測しています。こちらも、上記リポジトリでは存在しないキーを指定するメソッドを用意してあります。その比較についても別ページで扱いたいと思います。
結果
1,000回、10,000回、100,000回、1,000,000回、10,000,000回と、辞書内の要素を変えて処理を行いました。
値の追加
まずは値を追加する場合のTryAddメソッドと、ContainsKeyで確認後にAddの2通りについて、平均処理時間を比較します。試行回数は10回で、その平均値をとっています。
データ数 | TryAddのケースの処理時間(ms) | ContainsKeyで確認後にAddするケースの処理時間(ms) |
1000 | 0.0337 | 0.054 |
10000 | 0.2212 | 0.236 |
100000 | 1.6103 | 1.791 |
1000000 | 30.4214 | 164.278 |
10000000 | 162.9644 | 281.45 |
全体的にTryAddを使うケースの方が速くなりました。比率で考えると、データ数が100万の時に顕著な差が出ていますが、この辺りが大きな分岐点になっていますね。試行回数10回の平均をとっているので単純な異常値ではなさそうですが、深掘りすると長くなりそうなのでここでは追わないことにします(白目)
データ数100万、1000万の時には120から130msとフレームにして約7フレーム(60fpsのとき)の差がありました。ゲーム内でこの規模の辞書データを扱うかと考えると……そうそうないような気もするのですが、PCとスマホでは性能も違うことを考えると辞書への追加を行う場合はTryAddを使っておくのが無難かもしれません。
値の取得
続いて値を取得するケースを考えてみます。ここではデータ数分だけfor文を回して値の取得処理を行なっています。
データ数 | TryGetValueのケースの処理時間(ms) | ContainsKeyで確認後に取得するケースの処理時間(ms) |
1000 | 0.0078 | 0.0207 |
10000 | 0.0705 | 0.1262 |
100000 | 0.7047 | 1.2985 |
1000000 | 6.9656 | 12.821 |
10000000 | 68.4892 | 127.5894 |
こちらはTryGetValueを使うケースが全体的に速くなりました。データ数が1000万になると差が60msほど、フレームにして約4フレーム(60fpsのとき)程度の差になります。より短い時間で処理できるTryGetValueが有利でした。キーが存在する場合、ContainsKeyでキーを1回検索、キー指定で値を取得する際に1回検索、と考えるとざっくり2倍程度の差になっているのは分かりやすいですね。
感想
TryAddやTryGetValueを使って操作した方が全体的に速い傾向にありました。データ数が多いほど顕著に差が出てきましたが、実際にこの規模のデータをゲーム内で扱うかというとそうそうないかと思います。とは言いつつ、個別の処理速度が速いというのはスマホなどの環境において有利に働くので、基本的にTry系メソッドを使って追加、取得を行うのが良いかもしれません。実行中の処理落ちを防ぐ意味では、処理時間の0.1msでも削りたいですもんね。
値の更新が含まれる場合はTryAddでは対応できないので、ContainsKeyを使ってキーの有無を確認して、存在していれば値の更新、存在しなければ値の追加、なんて感じでif文で分岐させるのが鉄板です。
また、今回測定したデータは10回の平均をとっていますが、より多くの試行回数の平均をとることで、より安定した結果になるかもしれません。Unityのゲーム実行ボタンを押した後はしばらく放置していたのですが、いかんせん色々なアプリケーションが立ち上がっているので、バックグラウンドで別の処理が動いているのと競合していたら結果に影響しているかも……と不安な部分もあります。
まとめ
性能比較は楽しいですね(ご満悦)
このメソッドの方が速い、と言葉で知るだけではなく、数値で比較して性能差を確認できるのは大学時代に散々やった物理や化学の実験を思い出します。まぁレポートに追われて当時は大変な思いをしていたのですが、今になってみればこうしたものも活力になります。
今回はキー重複がない場合の値の追加、存在するキーでの値の取得を比較してみました。また別のページでは、キー重複のある場合の値の追加や、存在しないキーを指定した値の取得で比較をしてみたいと思います。一度性能比較用のコードを書くと、色々試してみたくなってしまいますね。
おまけ
初期化時にCapacityを指定したらどうなるかも計測してみました。
ゲーム開発の攻略チャートを作りました!
-
前の記事
【Unity】ビルドして分かるエラーもあるので定期的なビルドがおすすめ 2024.01.09
-
次の記事
【Unity/C#】DictionaryでCapacityを指定して初期化する場合の速度比較 2024.11.15
コメントを書く