PONOS Advent Calendar 2023の8日目の記事です。
ルックアップテーブルとは
ゲームにおいて ルックアップテーブル はグラフィクス関連で度々使用されていると思います。例えばRGB値を変換したりするケースです。
もちろん ルックアップテーブル は色を変換するためのテーブルではありません。本質的には 複雑な計算処理を単純なテーブル参照に置き換える という、すご〜くシンプルな構造です。
この解釈に従えば、ゲームの様々な局面で ルックアップテーブル 的なものが見つかります。
例えば、事前計算したテクスチャから値を参照することも、ある種の ルックアップテーブル と言えそうですし、もっといえば何もグラフィクスに限らず有用な場面はありそうです。
そこで ルックアップテーブル は実際に効果的なのか?をUnityで計測してみようと思いました。
検証
環境
- Mac(なんでも)
- 手元のMacを使用します。ここでは同じ環境で比較するのでスペックはどうでもいいです。
- Unity(なんでも)
- 今回Unityを使って検証しますが、本当は別にUnityである必要もありません。
準備
- プレーンな
Scene
を用意します -
GameObject
をSceneに一つ用意します - テストコードを書き込む
TestScript.cs
を作成します。 - 作成した
GameObject
にTestScript.cs
をはりつけます。
Case 1
やりたいこと
- テストデータを用意します
- 100万件のintの配列データ
- この配列にはランダムで0~9の数値を入れる
- 毎フレームこのデータをチェックし、内容に応じてスコアを加算します
- 数値が1の時 > スコア+1
- 数値が5の時 > スコア+2
- 数値が9の時 > スコア+3
- それ以外 > 何もしない
※ 毎フレーム同じテストデータなら、フレーム単位で処理する意味は全くありません。あくまで平均時間を測定したいだけですので、ご了承ください。
IF文で実装したコード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestScript : MonoBehaviour
{
// 100万件のテストデータの配列です。
private int[] data = new int[1000000];
// 計算したスコアを入れるところです。テストなので特に参照しません。無意味な変数です。
private int score = 0;
void Start()
{
this.InitArray(); // 開始時に100万件のテストデータを初期化しておきます
}
void Update()
{
this.Calc(); // 毎フレーム一定の計算を行います。
}
// データ配列を0~9の数字で初期化します。
private void InitArray() {
for (var i = 0; i < this.data.Length; i ++) {
this.data[i] = Random.Range(0, 10);
}
}
// データを走査して条件に一致したスコアを加算します。わかりやすいよう愚直なIF文です。真似しないでね。
private void Calc() {
for (var i = 0; i < this.data.Length; i ++) {
var value = this.data[i];
if (value == 1) {
this.score += 1;
} else if (value == 5) {
this.score += 2;
} else if (value == 9) {
this.score += 3;
}
}
}
}
IF文での結果
Unityを実行し、Profilerで TestScript.cs
のUpdateにかかっている時間を確認します。
5~6ms でした。
ルックアップテーブルで実装したコード
// 〜(省略)〜
public class TestScript : MonoBehaviour
{
// 〜(省略)〜
// ルックアップテーブルです。
private int[] lut = new int[10]{0,1,0,0,2,0,0,0,0,3};
// 〜(省略)〜
// この内容が参照にかわっています。
private void Calc() {
for (var i = 0; i < this.data.Length; i ++) {
var value = this.data[i];
this.result += this.lut[value];
}
}
}
ルックアップテーブルでの結果
2.2~2.7ms でした。
処理時間はおおよそ半分以下になっています!
Case 2
では、次に処理をもう少し複雑にします。
コードを書くのが面倒なので、基本的に処理は先ほどの流用で、条件のみを増やします。
やりたいこと
- テストデータを用意します
- 100万件のintの配列データ
- この配列にはランダムで0~9の数値を入れる
- 毎フレームこのデータをチェックし、内容に応じてスコアを加算します
- 数値が0の時 > スコア+1
- 数値が1の時 > スコア+0
- 数値が2の時 > スコア+3
- 数値が3の時 > スコア+2
- 数値が4の時 > スコア+9
- 数値が5の時 > スコア+1
- 数値が6の時 > スコア+8
- 数値が7の時 > スコア+4
- 数値が8の時 > スコア+5
- 数値が9の時 > スコア+6
IF文で実装したコード
見るに耐えない感じですが、我慢してください。。。
// 〜(省略)〜
public class TestScript : MonoBehaviour
{
// 〜(省略)〜
private void Calc() {
for (var i = 0; i < this.data.Length; i ++) {
var value = this.data[i];
if (value == 0) {
this.score += 1;
} else if (value == 1) {
this.score += 0;
} else if (value == 2) {
this.score += 3;
} else if (value == 3) {
this.score += 2;
} else if (value == 4) {
this.score += 9;
} else if (value == 5) {
this.score += 1;
} else if (value == 6) {
this.score += 8;
} else if (value == 7) {
this.score += 4;
} else if (value == 8) {
this.score += 5;
} else if (value == 9) {
this.score += 6;
}
}
}
}
IF文での結果
11~12ms かかっていました。
SWITCH文での結果
ちなみにせっかくなので、SWITCHでも計測してみました。コードは省略しますが結果は、、、
13~14ms でした。やや伸びている気がしますが、少なくとも速くはないですね。
ルックアップテーブルで実装したコード
// 〜(省略)〜
public class TestScript : MonoBehaviour
{
// 〜(省略)〜
// ルックアップテーブルです。
private int[] lut = new int[10]{1,0,3,2,9,1,8,4,5,6};
// 〜(省略)〜
private void Calc() {
for (var i = 0; i < this.data.Length; i ++) {
var value = this.data[i];
this.result += this.lut[value];
}
}
}
ルックアップテーブルでの結果
2.2~2.7ms でした。
当然ですが、ルックアップデーブルの場合はテーブルの中身が変わるだけで、処理は何も変わりません。そのため速度の遅延はありません。
結果比較
LUT | IF文 3パターン | IF文 10パターン |
---|---|---|
2.2~2.7ms | 5~6ms | 11~12ms |
※ LUTはルックアップテーブルをさしています。
まとめ
冒頭の繰り返すですが、ルックアップテーブル は 複雑な計算処理をテーブル(配列)の参照に置き換える ものです。
一方で今回行ったことは、複雑な計算式は行なっておらず、単純にIF文でデータをチェックして固定のスコアを加算しているだけです。
しかし、たったそれだけのことでもパフォーマンスには大きな差が出ました。こんな単純なことでも確かに効果があると言えそうです。
皆様のまわりには事前計算しておけるものはないでしょうか?「計算」という言葉に惑わされ、「これくらいの分岐なら」となっていないでしょうか?
それでは次回は@blockさんです。