Unityでコンパイル時間を可視化する

f:id:hirasho0:20190205124546p:plain

Unityでゲームを動かしてみるのに、ビルドを待つ必要はありません。 ソースコードのコンパイルさえ済めば、すぐに動かしてみることができます。

しかし、この時間でさえも、コード量が増えていけば耐え難いものになるのです。

この記事では、技術部平山が、 コインパル時間の可視化を行ったことについて書きます。 また、実際どれくらいコードがあるとどれくらい遅いのか、 ということも調べてみました。

まず測定

コンパイル中かどうかはEditorApplication.isCompiling で取れます。 あとはテキトーなエディタ拡張ウィンドウを作って、表示します。

public class CompileTimer : EditorWindow
{
    [MenuItem("Kayac/CompileTimer")]
    static void Init()
    {
        EditorWindow window = GetWindowWithRect(typeof(CompileTimer), new Rect(0, 0, 200f, 100f));
        window.Show();
    }

    // コンパイル前後で変数を保持できないのでEditorPrefsに入れる必要がある
    const string compileStartTimeKey = "kayac_compileStartTime";
    const string lastCompileTimeKey = "kayac_lastCompileTime";

    void OnGUI()
    {
        DateTime startTime = new DateTime();
        var compiling = EditorApplication.isCompiling;
        bool prevCompiling = false;
        if (EditorPrefs.HasKey(compileStartTimeKey))
        {
            string str = EditorPrefs.GetString(compileStartTimeKey);
            if (!String.IsNullOrEmpty(str))
            {
                startTime = DateTime.Parse(str);
                prevCompiling = true;
            }
        }
        float lastTime = 0f;
        lastTime = EditorPrefs.GetFloat(lastCompileTimeKey);
        var currentTime = 0f;
        if (prevCompiling)
        {
            if (compiling)
            {
                currentTime = (float)(DateTime.Now - startTime).TotalSeconds;
            }
            else
            {
                lastTime = (float)(DateTime.Now - startTime).TotalSeconds;
                EditorPrefs.SetFloat(lastCompileTimeKey, lastTime);
                EditorPrefs.DeleteKey(compileStartTimeKey);
            }
        }
        else if (compiling)
        {
            EditorPrefs.SetString(compileStartTimeKey, DateTime.Now.ToString());
        }
        else if (EditorPrefs.HasKey(compileStartTimeKey))
        {
            EditorPrefs.DeleteKey(compileStartTimeKey);
        }
        EditorGUILayout.LabelField("Compiling:", compiling ? currentTime.ToString("F2") : "No");
        EditorGUILayout.LabelField("LastCompileTime:", lastTime.ToString("F2"));
        this.Repaint();
    }
}

コードはプロジェクトごとgithubに置いてあります。 OnGUIが呼ばれる度にisCompilingを見て、「前コンパイル中で、今コンパイルしていなければ」 終了とみなします。開始時刻は保存しておき、今の時刻から引けば時間がわかります。

そんな簡単な処理の割には長いのは、コンパイルの前後で変数を保持できないからです。 _compileStartTimeみたいな変数を持っておいても、コンパイル終了時にクラスインスタンスが 作り直されて「西暦1年1月1日」になってしまいます。 なので、保存するものは全部EditorPrefsにセーブしているわけです。

OnGUIのためにToString()によるGCAllocが走るのが気に入りませんが、 それはまたそのうち考えます(変数が保持される間はキャッシュしておけば良いでしょう)。

かくして、最後のコンパイル所要時間を表示しておきつつ、 冒頭のようにコンパイル中はコンパイル経過時間を表示する、 というエディタ拡張ができました。

コンパイルは何をやったら遅いのか

結論から言えば、単純に量です。 クラス間関係の数とか、partialとか、まあいろいろ試しましたが、あんまり関係ないように見えます。

例えば、今回用意したサンプルプロジェクトは4-6秒でコンパイルが終わります。 コード量はゲームの製品に比べればほぼゼロですが、最低でもこれくらいかかってしまうようです。 測定は MacBook Pro Mid-2014にて行いました。

ここに、今回用意したスクリプト で、1000個のメソッドを持つクラスを1000個生成すると(GenerateHuge)、コード行数は100万行程度になり、 コンパイル時間は65秒前後になります。こうなると相当に辛い状況です。 Debug.Logをはさんで再実行する度に1分待たされます。

できるだけコードでなくAssetBundleに入れられる形のデータで物を足す、 というのが、理想的な解決策でしょう。量産前にその体制を作れるかどうかは、 相当に大事なことであると思います。 それを怠ると、コードはじわじわと増えていき、気づいた時にはもう手遅れになってしまうのです。 今回の可視化ツールは、それを防ぐ助けになるかもしれません。 できれば専用のウィンドウを出さずに表示するように変えたいものです。

もちろん、できてしまった物をどうにかする方法もあるにはあり、 dllを分ける(2017.3以降)Pluginsの下に入れる、 といった手法もあります。 100万行超えてしまってさすがに辛い、となれば、これらを駆使して改善する必要があります。

しかし、できればそうなる前に手を打ちたいものです。

おまけ

せっかくなので、プロジェクトのコード量を測定して、 どのフォルダ以下にどれくらいコードがあるのかわかる機能もつけておきました。 「コード量分析」ボタンがありまして、

f:id:hirasho0:20190205124546p:plain

押すとAssetsフォルダがある所にcodeAmount.txtが生成されます。 例として弊社東京プリズンの結果 をご覧ください。単位はバイトで、ほぼ文字数と考えて良いかと思います。 ご自分の製品と比べてみてください。

ちなみに、東京プリズンにおいては、自動で生成されたコードがそこそこあります。例えば、 AfterEffectsからの変換 で生成されたものも結構あり、コンパイル時間の面では良くありません。 単純に再生して終わりのアニメーションは、できるだけコードでない方法で実装したいものです。

端末性能を測るWebGLアプリ作ってみた

KonchBench

こんにちは。技術部平山です。

この記事では、雑にベンチマークプログラムを作ってみたことと、それに付随して、

  • ベンチマークプログラムを作りたくなるような事情
  • テストの設計と、その背後にあるハードウェア

といった点について書きます。

なお、実行はこちらからWeb上で可能です(上のスクショを押しても飛べます)。 UnityのWebGLにすることで、余計な手間なく多くの機械で測れるようにしています。 ただし、WebAssemblyを使っている関係上、iOS9以前では動きません。ご容赦ください。

測ってくださった方は、 twitterで結果(スクショ)を頂けると大変うれしいです。 ゲーム機、ハイエンドPC、古いスマホ、などは特に歓迎です!

なお、ソースコードもgithubにあります

測定方法

「ALL」を押し、元の画面に戻ってくるまで(キャラクターの絵が出てくるまで)放っておきます。 途中でスリープに入ったり、バックグラウンドに移行したりすると正確に測れません。 PCの場合フォーカスを外すと止まりますので、すみませんが 休憩中などの測定をおすすめします。

メジャーなベンチマークソフトのように豪華な画面は出ませんので、 「オレの端末はこんなに速い」的な感動は味わえません。 数値の意味と意図がわからないと何の意味もない、完全にゲーム開発玄人向けのものです。

動機

私は15年ほど家庭用やゲーセン向きにゲームを作っていました。 当然、機械の実物がそこにありましたから、 何をどれくらいできるのかを調べますし、 メーカーが書いた文書には 得意なことや苦手なことが詳細に書かれていました。

しかし、スマホの世界ではこれが全く違います。 まず、手元にない機械でも動かないといけません。 そして、機械の数が膨大なので、仮にハードウェアに関する文書があっても 読んでられません。

Unity関連の情報として「これをやったら重い」 という情報はあるのですが、多くはCPU側に偏っている上に 「どれくらい重い」という量的な情報は乏しく、 そもそも機械の性能はよくわからないのです。 ここ2年間ほどは、「テキトーに作ってみて、重いと思ってから高速化する」 という行きあたりばったりアプローチになっていました。

そこで、今更ながら測ってみることにしたわけです。 素人と玄人を分けるのは、量的な判断ができるかどうかであり、 まず量に関する情報を集めることは真っ先にやらないといけない気がします。

なぜ自作?

既存のベンチマークソフトは何をどう測っているのかがよくわかりません。 出てきた数字の意味もわかりません。 我々は開発者なので、ソースコードがあるテストで測らないとピンと来ませんし、 数字には「ギガバイト毎秒」とか「ピクセル毎秒」とかの単位が欲しいのです。

また、開発したゲームで測れば、そのゲームの負荷はわかりますが、 他のゲームに応用しにくい数字になります。 今後作るゲームにも活かしていこう、と思うならば、 もっと基本的な性能を測るものの方が適しているわけです。

測定結果

いきなりですが、測定結果です。

これはバージョン0.2.4での測定結果であり、 他のバージョンとは比較できないかもしれません。 さらに、測定誤差が大きく、OS、バッテリー量、熱状況、 ブラウザ、等々によっても結果が違う可能性があります。 あくまでも「雑に把握するためのもの」とお考えください。 各テストの詳細は後述します。

CPU側

4つのテストを用意しました。単位は秒で、「何秒で計算を終えたか」です。 小さいほど性能が良くなります。 WebAssemblyの実行性能はブラウザに大きく依存するので、 ブラウザ名も記しておきます。また、参考のために機械の発売時期も記します。

機械 ブラウザ 発売 FibonacciInt FibonacciFloat HeapSort QuaternionIntegration
i7-8700K,GeForceGTX 1080 chrome 2016/5 0.037 0.047 0.774 0.314
MacBookPro15'' chrome 2015/5 0.050 0.057 1.07 0.393
MacBookPro13'' UnityEditor 2014/7 0.473 0.537 4.18 1.37
MacBookPro13'' chrome 2017/X 0.049 0.059 0.998 0.313
MacBookPro13'' chrome 2014/7 0.091 0.133 1.67 0.462
iPhoneX chrome 2017/11 0.149 0.141 1.41 0.409
PH-1 chrome 2017/8 0.190 0.198 3.60 1.52
XperiaXZ premium chrome 2017/6 0.194 0.198 3.63 1.49
iPad mini4 safari 2015/9 0.243 0.239 3.58 0.881
XperiaZ5 Compact chrome 2015/10 0.321 0.403 5.58 2.60
Nexus5 chrome 2013/10 0.337 0.364 6.04 2.38
iPhone6Plus safari 2014/9 0.605 0.690 7.77 2.21
AndroidOne S2 chrome 2017/3 0.709 0.725 9.48 5.22
404KC chrome 2015/2 0.815 0.887 9.22 5.87

一番上がデスクトップPC、次の3つがノートPC、残りはスマホやタブレットです。

GPU側

GPU側のテストは6つあります。 単位はメガピクセル毎秒で、つまり「時間内に何個ピクセルを塗れるか」 を表します。大きいほど性能が良くなります。

機械 Color ColorZ AlphaTestColor HeavyCalc CoherentTexture IncoherentTexture DependentTexture OpaqueColor
i7-8700K,GeForceGTX 1080 4490 4010 4740 1430 4360 281 4330 4410
MacBookPro15'' 279 256 466 75.1 190 14.0 190 483
MacBookPro13''(Editor) 85.6 63.8 155 32.0 47.7 13.0 47.8 170
MacBookPro13'' 2017 (Chrome) 294 272 474 64.2 199 28.0 188 442
MacBookPro13'' 2014 (Chrome) 88.5 77.5 167 37.0 54.6 14.0 54.7 217
iPhoneX 993 946 508 37.9 450 5.00 339 4100
PH-1 428 420 365 32.0 211 2.97 206 428
XperiaXZ premium 429 422 366 32.0 187 2.08 181 430
iPad mini4 148 147 81.5 8.97 120 1.99 112 655
XperiaZ5 Compact 366 282 358 30.0 134 1.99 128 257
Nexus5 117 116 58.0 10.0 68.0 1.96 68.0 117
iPhone6Plus 144 144 78.9 8.30 95.3 1.85 85.5 641
AndroidOne S2 22.9 18.2 6.20 1.02 15.4 0.94 12.8 23.9
404KC 18.0 18.0 5.01 0.98 15.0 0.96 12.8 18.0

わかること

ざっと眺めてみてわかることをまず書きましょう。

  • デスクトップPCはさすがに別次元の速さ
  • 一口で「スマホ」と言っても、性能は全然違う。数十倍の差。
  • 新しいから速い、というわけではない。404KCは2015年、S2は2017年だが、2013年のNexus5より遅い。

これはゲーム(弊社「東京プリズン」) を作っている時からなんとなくはわかっていて、 「3年前の端末で動けばいいだろう」というような意見には違和感がありました。 GPUの世界では安物はいつまで経っても安物で、5年経っても古い高級品に勝てないことはザラです。 実際、古いiPhone5で40フレーム毎秒以上出るのに、買ったばかりのS2では25フレームも出ない、 ということが起こっていました。それが、こうしてテストを作ってみるとよくわかります。

なお、404KCやS2の低性能ぶりが目につきますが、 これを「例外的にショボい機械」と考えるのは危険です。 S2が搭載するmsm8917も、404KCが搭載するmsm8916も、採用例が多いチップです。 snapdragonのWikipediaをご覧くださいい。

「既存ゲームで機種の統計取ったけど、そんな機械ほとんどないじゃん」 ということもあるかもしれませんが、それは 「そういう機械を持っている人は重くて遊べていない」結果かもしれません。

チームメンバーがiPhoneのような高級機ばかり使っていると、 感覚がズレやすいように思います。 世の中の普通の人が何を基準にスマホを買っているか、というのは結構大事な視点 ではないでしょうか。

CPU側テスト詳細

では、テストの詳細を説明していきます。まずはCPU側のテストです。

FibonacciInt

フィボナッチ数を計算することで、 メモリアクセスがほとんどない整数演算及び分岐の性能を測定します。 フィボナッチ数である必要もないし、 コードの詳細はどうでも良いのですが、興味のある方はコードをご覧ください。

ここで見たいのは、素のCPU性能です。 メモリ性能の影響を極力除きたいので、 使う記憶領域の割に計算が多い処理が適します。 とはいえ、「1づつ1億回インクリメントする」などだと、 コンパイラの最適化次第で爆速になりそうで怖いです(+=100000000に変形するとか)。 ですので、本質的に必要な計算量が多い(=計算量オーダー が大きい)か、機械的な最適化が難しいものである必要があります。 巡回セールスマン問題 や、工夫をしないフィボナッチは計算量が指数オーダーで適します。 整数の加減算、比較、分岐、などが高密度で実行され、 計算に要するメモリ量はたかだか4バイト×40程度なので 全てCPU内のキャッシュに収まります。

では結果を見てみましょう。

機械 ブラウザ 発売 FibonacciInt
MacBookPro13'' UnityEditor 2014/7 0.473
MacBookPro13'' chrome 2014/7 0.091
AndroidOne S2 chrome 2017/3 0.709
iPhoneX chrome 2017/11 0.149
Nexus5 chrome 2013/10 0.337
XperiaXZ premium chrome 2017/6 0.194

まずPCですが、Editor実行するとWebGLより5倍くらい遅い、というのがわかります。 私の感覚として「エディタ実行すると安物のスマホと似た速度になる」 というのがあったのですが、裏付けられた感じです。 S2と大差ありません。軽くエディタでプロファイリングするのは助けになります。

そして、高級品は速い、ということも言えます。 古いnexus5ですら、S2を大きく凌ぎます。 iPhoneやXperia、nexus5などの高級機は「高性能CPUコア(Cortex-A9,A15,A57等)」 を少数(2個程度)積んでおり、 S2などの安物は「低性能CPUコア(Cortex-A5,A7,A53等)」を多めに(4-8個) 積んでいます。全コアの性能を引き出せれば安物でももっと戦えますが、 Unityでマルチコアを使いこなすのは大変なので、 今のところはシングルコア性能が重要です。 ECSで作るのが当たり前になれば違ってくるでしょうが、それにはまだ時間がかかるでしょう。 「A53は遅い」と覚えておくと良いかと思います。

FibonacciFloat

単にフィボナッチ数をfloatで求めただけです。 「分岐やインクリメント等の整数演算に浮動小数点演算が混ざった時の影響」が見たかったのですが、 さしたる差はないようです。

PCやAndroid系では整数よりもわずかに遅くなり、 iPhone系では同等か、逆にわずかに速くなる、という傾向は見て取れますが、微妙です。 浮動小数点演算性能については、別のテストでよりはっきりと差が出ます。

HeapSort

単純にヒープソートです。

ソートといえばクイックソートですね。何かと面倒なクイックソートですが、 「メモリ上近い位置にあるデータを操作する頻度が高い」という性質のために性能は最高です。 キャッシュに乗ってCPUの中で完結しやすくなるため、 メモリの操作待ち時間が少なくて済むのです

一方、ヒープソートは扱いやすいアルゴリズムなのですが、 実際の速度はクイックソートに大きく劣ります。 何故かと言えば、「メモリ上遠い位置にあるデータを操作する頻度が高い」からです。 キャッシュが有効に使えず、メモリ操作の待ち時間が長くなります。 逆に言えば、メモリアクセス込みのCPU性能が測定できるわけです。 フィボナッチとヒープソートの比が大きい機械は、メモリが遅い割にCPUが速い機械、 ということになります。

例えば、以下をごらんください。

機械 ブラウザ 発売 FibonacciInt HeapSort
iPhoneX Chrome 2017/11 0.149 1.41 9.5
XperiaXZ premium Chrome 2017/6 0.194 3.63 19

ほぼ同時期に発売した高級機種で比べてみます。 単純にiPhoneの方が速いんですが、比を見てください。 Xperiaは19、iPhoneの比は9.5なので、 iPhoneの方が相対的にメモリにコストをかけている臭いがします (こんな雑なテストで断定はできませんが)。 もしそうならば、Xperiaは「デカいメモリを使う計算」により弱いわけです。 C#の場合、classが参照でつながった複雑なデータ構造では メモリがあちこちに散るので、メモリが遅い機械では不利になります。 データを配列に固める高速化はどんな機械でも有効ですが、 メモリが弱い機械ほど改善の幅は大きくなります。

iPhoneはお高いですが、それはメモリが高級だからなのかもしれませんね。 CPUの周波数やコア数で差がなくても、 メモリの性能に差があれば、このように大きな差につながります。

かつて、ゲーム機がメモリをわずかしか積めなかった時代がありました。 ゲーム機はメモリの速度を非常に重視していましたが、 速いメモリは駅前の土地のように値段が高いので、 たくさんは積めなかったのです。 昔は機械に合わせてゲームを作っていたので、 メモリが少なくてもどうにかなったという事情があります。 一方、スマホは汎用コンピュータです。機械に合わせてソフトを作るわけではないので、 容量を削る選択は取れません。結果、どうしても「多いが遅い」を選択することになります。 ちなみに、2010年以降のゲーム機も、複数機種で出るゲームの比率が上がり、 OSやらブラウザやらを積むことを求められた結果、 速度よりも容量に傾けた設計になっています。

QuaternionIntegration

浮動小数点(float)演算の性能を測定します。 別件で書いたクォータニオンの積分をそのまま持ってきました。 ごく小さなデータ量で繰り返し計算するためメモリの影響を受けにくく、 また、分岐等が少ないため、 純粋な浮動小数点計算の性能を測定できます。

さて、これもヒープソートと同様、フィボナッチとの比を見ると 特徴が見えてきます。このチップがどれくらい 浮動小数点演算を大事だと思っているかがわかるのです。

機械 ブラウザ 発売 FibonacciInt QuaternionIntegration
iPhoneX Chrome 2017/11 0.149 0.409 2.7
XperiaXZ premium Chrome 2017/6 0.194 1.49 7.68

整数では似たような性能のiPhoneとXperiaが、比で見ると 2.7と7.68で大差です。 iPhoneは浮動小数が大事だと思っているが、 Xperiaはそうは思っていない、ということでしょう。

さて、ここは結構ゲームに影響しそうです。 Unityは現状スキニング計算(頂点ブレンディング)を CPUでやっているそうで、万単位の頂点を毎フレーム 動かすとなると、浮動小数点演算の性能がダイレクトに響きます。 実際にUnityのスキニングで性能を測定してみると良いかもしれません。

GPU側のテスト

今度はGPU側のテストを見ていきます。

全てのテストは、1024x1024のRenderTargetに、 40msの間にそれぞれのシェーダで全画面描画を何回行えるか、 を表しています。

実際のゲームで何画面塗れるかは場合によります。 例えば60FPSのゲームを作る場合、16.6msに収めねばなりませんから、 スコアの5/12が塗れる面積の限界となります。 スコアが120だったならば、120*5/12=50回 1024x1024を塗れるだろう、というわけです。

Color

アルファブレンド有効で、赤一色で塗ります。フラグメントシェーダは以下です。

fixed4 frag (v2f i) : SV_Target
{
    return fixed4(1.0, 0.0, 0.0, 0.5);
}

なぜアルファブレンド有効でテストするのか、 については後程OpaqueColorテストの項で説明します。

このテストの目的は、レンダーターゲットへの書き込み性能だけ を測定することです。頂点数は1回の描画につき4であり、 メモリからの頂点データ読み出しや、頂点シェーダの計算がボトルネック になることはありません。 また、頂点シェーダからフラグメントシェーダに渡すデータは

struct v2f
{
    float4 vertex : SV_POSITION;
};

という具合にfloat4一つなので、 そこの転送や補間計算もおそらくボトルネックにはなりません。 よって、純粋にフラグメントシェーダからレンダーターゲットへの書き込みの 性能を測ることができます。

ただし、アルファブレンドは有効ですから、

  • レンダーターゲットから現在のピクセルを読む
  • 出力する色と線形補間する
  • レンダーターゲットに書きこむ

の3段階必要で、単純な書き込み性能だけを測ることはできません。 レンダーターゲットはRGBA32ですので、 4バイト読んで、補間して、4バイト書きこむ、ということになります。 1024x1024は4MBですから、 1枚書く度に、4MBの読み込みと4MBの書き込みが発生します。 仮にスコアが100であれば、40msごとに読み込み400MB、書き込み400MBが発生し、 1秒間あたりで見れば、1000/40=25を乗じて読み書き10GBのメモリアクセスが発生します。 メモリがそれだけの性能を持っている、ということであり、 このテストはメモリの性能も同時に見ていると言えます。

さて、測定結果を見ましょう。

機械 Color
iPhoneX 993
XperiaXZ premium 429
Nexus5 117
AndroidOne S2 22.9

安物と高級品で、性能に50倍の差があるというのは、なかなか大変ですね。 にも関わらず同じ製品を売るわけですから。 また、高級機は古くなっても高級機であり、 2013年のNexus5は2017年のS2の5倍の性能を持っています。

ところで、このテストはメモリの性能も同時に見ている、と書きましたが、 iPhoneXは速すぎやしませんかね? 「速いメモリは高くてたくさん積めない」と先程書きました。 さすがに50倍速いメモリをギガ量積んでいるとは考えにくいのでは?というわけです。

こういう時には何か種があります。 おそらくiPhoneXはアルファブレンド をメモリアクセスなしでやれるハードウェアなのでしょう。 タイルベースディファードレンダリング方式(TBDR: tile based defered rendering) という技術がこれを実現します。これに関しては後ほど詳しく書きます。

ColorZ

Colorとほぼ同じですが、ZテストとZ書き込みを有効にしたものです。 Zバッファは32bitでしょうから、読み込みに4バイト、書き込みで4バイトの、 計8バイト分の負荷が増します。 より強くメモリの性能の制限を受けるわけです。

機械 Color ColorZ
iPhoneX 993 946
XperiaXZ premium 429 422
XperiaZ5 Compact 366 282
Nexus5 117 116
AndroidOne S2 22.9 18.2

上の3つはほとんど遅くなっていません。 メモリの性能に余裕があって足を引っぱらないか、 前述のTBDRのような種があるか、さてどちらでしょうか。

一方安物のS2や、高級機のはずのXperiaZ5 Compactでは明らかに性能が落ちています。 メモリが足を引っぱっているのかもしれませんし、Zテストの処理自体が遅いのかもしれません。

AlphaTestColor

Colorにdiscard命令を足したものです。discardを呼ぶと、 その画素の描画はされません。 discardは大抵の場合、アルファ値が一定以下なら描画をスキップする 「アルファテスト」という処理の実現に使われます。

fixed4 frag (UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
{
    vpos.xy *= 1.0 / 128.0;
    if (frac(vpos.x + vpos.y) < 0.5)
    {
        discard;
    }
    return fixed4(1.0, 0.0, 0.0, 0.5);
}

vposを元にdiscardするかどうかを分岐させています。

f:id:hirasho0:20190131192546p:plain

vpos.xyにはピクセルの座標が整数で入っています。 これを128で割ると、その小数部は0から1に増えるのを128ごとに繰り返します。 これを0.5以下かどうかで分岐すると、こういう斜めの模様ができます。 アルファ値で分岐してないのでアルファテストではないですね。 名前を間違えました。

さて、測定結果です。

機械 Color AlphaTestColor
MacBookPro13''(Chrome) 88.5 167
iPhoneX 993 508
AndroidOne S2 22.9 6.20

Colorと比べることで、どれくらいdiscardが苦手かがわかります。 例えばiPhoneXはほぼ半分に落ちます。 S2はさらに苦手で、1/3以下に落ちています。 discardが苦手なのか、計算が増えたせいで遅いのかはわかりませんが、 計算はわずか4か5命令なので、たぶんdiscardでしょう。 これらの機械では、discardするくらいなら アルファ0で塗ってしまった方が速いかもしれません。

しかし一方、MacBookProでは倍くらい速くなっています。 ほぼ半分の面積をdiscardしているので、「塗らなくて済んだ分だけ速くなった」 と言えそうです。IntelのGPUはそういう特徴があるのでしょう。 こういう所もGPUごとに違ってくるわけです。

なお、アルファテストがいかに苦手でも、必要な時には使うしかありません。 例えばビルボードの描画結果をZテストに反映させたければ、 アルファテストは必要です。アルファ0で塗ってもZバッファには書きこんでしまうからです。 あまりにdiscardが遅いようであれば、頂点が増えてでも ビルボードをやめる、という選択もありえます。

HeavyCalc

ひたすら大量の計算をさせるテストです。 テクスチャを読まないので、メモリ性能の影響を受けづらく、 素の計算性能が推測できます。 CPUのフィボナッチと同じ目的のテストです。

シェーダの計算の中身は何でもいいのですが、最適化で消されそうな計算ではマズいのは フィボナッチの時と同じです。詳細は単なる趣味なので触れませんが、 こんな絵が出ます。

f:id:hirasho0:20190131192549g:plain

結果を見てみましょう。

機械 HeavyCalc
i7-8700K,GeForceGTX 1080 1430
MacBookPro13''(Chrome) 37.0
iPhoneX 37.9
XperiaXZ premium 32.0
Nexus5 10.0
AndroidOne S2 1.02

GeForceが圧倒的で1000を超えていますが、スマホは40がせいぜいです。 MacBook Proも同レベルですので、 UnityをノートPCでお使いの方は、Editor上でGPU性能を見れば、 ハイエンドのスマホとそう大きくは違わないだろう、ということになります。

なお、S2は1程度と出ていますが、これは「2より小さい」 程度とお考えください。現状の実装は結果が2以下だと あまり信頼できません。「とりあえずショボい」ということでOKです。

さて、スマホというくくりで見ても計算性能が40倍違う、 ということは気に留めておいて良いかと思います。 複数機種向けに作る時は、下に合わせるのが基本です。 そうしなければ大変なリスクを背負いこむことになります。 高級機では解像度やフレームレートが上がる、 という程度で済ませるか、別の絵が出るように作り込むかは、 製品の性格と、開発者のこだわり次第かと思います。

CoherentTexture

単純にテクスチャを貼ります。何を見ているかと言うと、 「キャッシュに乗る状況でのメモリ読み出し性能」です。

fixed4 frag (v2f i) : SV_Target
{
    fixed4 c = tex2D(_MainTex, i.uv);
    c.a = 0.5;
    return c;
}

通常メモリアクセスという奴は、32バイトとか64バイトとかの 塊で行われます。コンビニでアイスを2個買うならば、 2個一度に買うでしょう。1個づつ買って2往復はしたくありません。それと同じです。

どこかのピクセルの処理で例えば64バイト読めば、 そこには隣接したピクセルのデータも入っており、 これがキャッシュに入ります。 隣のピクセルを処理する時にはキャッシュから読むだけで済み、 メモリアクセスが発生しない確率が高いのです。 普通にテクスチャを貼ればほぼそうなるので、 「綺麗にメモリアクセスした時の性能」が測れます。

例えば、1024x1024のテクスチャの全域を画面に貼るとしましょう。 総容量は4MBです。 キャッシュメモリが4MBあれば、二回目からは一切メモリアクセスが発生しませんので、 メモリ読み出し総量は4MBで済みます。

しかし、実際のところそんなにキャッシュが大きくはないでしょうから、 おそらくは描いた回数だけ丸ごと読むことになるでしょう。 スコアが100であれば400MBの読み出しが追加され、 1秒あたり25倍して10GBとなります。 おそらく10GB/s程度の性能はあるのだろう、ということが言えるわけです。

なお、メモリだけを測れるわけではなく、 当然tex2D命令自体の負荷も関わってきます。 そこにはテクスチャフィルタの負荷も含まれるでしょう。 このテストではフィルタはバイリニアに設定しているので、 もしバイリニアフィルタをかける速度が遅ければ、それによってスコアは落ちます (ただしARGB32のバイリニアがタダでかけられない機械を私は見たことがありません)。

さて結果を見てみましょう。

機械 Color CoherentTexture
i7-8700K,GeForceGTX 1080 4490 4360
iPhoneX 993 450
XperiaXZ premium 429 187
Nexus5 117 68.0
AndroidOne S2 22.9 15.4

Colorと比べた時の落ち込み方は、機械によって違います。 iPhoneXやXperiaXZは半分以下になりますが、S2やNexus5は半分までは落ちません。 GeForceはほとんど落ちません。 メモリの性能に余裕があるかどうかが大きい気がしますが、正確に知るには別のテストが必要です。 とはいえ実際上は 「一番遅い奴はこれくらいしか塗れない」ということがわかればオーケーでしょう。

さて、「テクスチャを貼ってアルファブレンドして出す」というのは、 2Dゲームで最も頻度が高い処理です。 したがって、このテストの結果は、 実際のゲームでどれくらいのことができるのかを推測するのに役立ちます。

例えばS2で60FPSのゲームを作る場合を考えてみましょう。 15.4に16.6/40=5/12を掛ければ6.42です。 1024x1024を6回塗ったらもうギリギリということになります。 さらに、画面解像度は1024x1024ではありません。 1280x720なら1024x1024より小さいのでいいですが、 もし1920x1080であればピクセル数が倍になりますから、3回でギリギリということになります。

東京プリズンではこれを鑑みて、フレームレートが欲しい局面では、 平均塗り重ね回数が3程度になることを目指しました。 とはいえ、これは実質無理です。ちょっと大きいエフェクトが出たり、 カットインが入ったり、ポップアップウィンドウが出たりすれば、 それだけで1から2画面分は増えてしまいます。 となれば、解像度を下げるのが最も簡単です。 弊社東京プリズンは、幅768程度まで解像度を下げられるようにしています。

f:id:hirasho0:20190131192614p:plain

ちなみに、測定結果からメモリ性能の推測ができる、というお話ですが、 S2の場合、4バイト×15.4x1024x1024を40msに読み出しますから、 秒間1.5GB以上の性能はありそうに思えます。 iPhoneXであれば、4バイト×450×1024x1024で45GB/sです。 そんなに大きいんですか?本当に? もしかしたら、これに関しても何か種があるのかもしれません。 例えばメモリからGPUに運んでくる間テクスチャを圧縮する、 というような工夫があるかもしれませんし、 キャッシュをうまく使っているのかもしれませんね。 例えばCPUの二次キャッシュをGPUからも使える、 という仕掛けであれば、メガバイト量のキャッシュになります。

IncoherentTexture

CoherentTextureの頂点シェーダにわずかな細工をしたものです。

o.uv = TRANSFORM_TEX(v.uv, _MainTex) * 16.0;

uvの計算後に16を掛けています。それだけです。 これで結果がこうなります。

機械 CoherentTexture IncoherentTexture
i7-8700K,GeForceGTX 1080 4360 281
MacBookPro13''(Chrome) 54.6 14.0
iPhoneX 450 5.00
XperiaXZ premium 187 2.08
AndroidOne S2 15.4 0.94

CoherentTextureと比べてください。無惨の一言ですね。

このテストは、キャッシュが効かない時のテクスチャ読み出し性能を測るものです。 CoherentTextureの場合、隣のピクセルを塗る時には、隣のテクセルを取ってくれば済みました。 だからほぼキャッシュに乗っており、メモリアクセスはあまり発生せずに済んだわけです。

しかし、今回はUVが16倍されています。隣のピクセルを塗る時に取ってくるテクセルは、 16テクセルも離れているのです。 それでもメモリの読み出しには「単位」がありますから、 例えば4x4の領域をまとめて持ってくることは変わりません。 そして、そのうちの1つしか使わないまま次へ行くことになります。 メモリアクセスの効率がガタ落ちするわけです。 iPhoneXやXperiaXZでは1/100近くまで落ちています。

一方S2で1/16程度、MacBook Proで1/4程度にしかならない理由はわかりませんが、 例えば「メモリを読む単位」が小さいのかもしれません。 極端な話、4バイトづつ読めるメモリであれば、無駄な転送は発生しません。 iPhoneXは一度にごっそり持ってくることで性能を上げていて、 それが仇になったのかもしれません。 あるいは、メモリの待ち時間(レイテンシ)の問題かもしれません。

さて、実際の状況でこういう性能低下が起こることはあるのでしょうか?

あります。縮小です。

絵を1/2に縮小して貼ると、隣の画素を塗る時に取ってくるテクセルは2画素隣になります。 もし2x2の単位でメモリからキャッシュに持ってきていれば、 そのうちの3/4をドブに捨てることになります。 1920x1080の端末用に画像を用意して、これを960x540の端末で描画すれば、この状況が生まれます。 今回のテストは1/16に縮小したことに相当しますから、 もし16x16の単位でメモリから持ってきていれば、 255/256をドブに捨てることになるわけです。性能が1/100になっても不思議はありません。

だからミップマップというものがあるのですよ!

前もって縮小した画像を用意しておけば、きちんと隣のテクセルを使うことができます。 スマホの場合、機械の解像度は様々ですから、 性能や品質、そして電力を犠牲にしたくないのであれば、ミップマップを用意すべきです。 ただ、それによって容量が最低でも1.25倍になってしまうので、 悩むのはよくわかります。

ちなみに、弊社東京プリズンではミップマップを使用していません。 「どうせ縮小すれば塗り面積自体が減って軽くなるんだから気にしなくていいよね」 ということなのですが、半分以下に縮小すると汚なくなりますし、 解像度を落としてもあまり速くならなくなります。 下の機械で目標FPSを出すためには、根本的に塗り面積が少ない絵作りをするしかなく、 結果的に高級機で出せる絵も変わってしまうことでしょう。できれば用意したいところです。

DependentTexture

まずはフラグメントシェーダをご覧ください。

fixed4 frag (v2f i) : SV_Target
{
    fixed4 c = tex2D(_MainTex, frac(i.uv + float2(0.5, 0.5)));
    c.a = 0.5;
    return c;
}

CoherentTextureとの違いは、uvに0.5を足しているところだけです。 結果はこうなります。

機械 CoherentTexture DependentTexture
iPhoneX 450 339
AndroidOne S2 15.4 12.8
XperiaXZ premium 187 181
iPad mini4 120 112

足し算を1個足しただけなのに、無視できないほど遅くなっている機械がいます。 iPhoneXとS2です。残りは落ちたかどうかよくわかりません。

過去のPowerVRには「フラグメントシェーダを実行する前にtex2Dできる場合は、先に発行しておき、 メモリからデータが到着してから実行を開始する」 という仕掛けがありました。 シェーダでuvを計算してしまうと、実行を開始してからデータの到着を待つことになり、 派手に遅くなっていたのです。 2012年のGDCにおいて、Infinity Bladeの絵に関する講演 が行われており、その中でも「UV計算は頂点でやれ」と言われていました。 S2が搭載するadreno308や、iPhoneXの中身にも、 似たような事情があるのかもしれません。

このテストはそういう弱点があるかどうか調べるためのものです。

OpaqueColor

Colorを不透明、つまりアルファブレンドなしで描画したものです。 結果はこうなります。

機械 Color OpaqueColor
iPhoneX 993 4100
XperiaXZ premium 429 430
iPad mini4 148 655
Nexus5 117 117
AndroidOne S2 22.9 23.9

意味不明に速い奴がいます。iOSの2つです。 アルファブレンドしないだけで4倍速い、というのは普通に考えてありえません。 これは、TBDRであれば説明がつきます。

不透明の描画の場合、最後に塗ることになる三角形だけを塗れば、 それで同じ結果になります。 事前に「どのポリゴンを最後に塗るか」がわかれば、 無駄なフラグメントシェーダの実行を省略できるのです。 しかし、普通はそれができません。CPUからやってきた頂点の順番に 描画してしまうからです。 そこで、大抵は「手前のものから先に描く」という工夫をし、 できるだけZテストで捨てられるようにします。 Unityも不透明なものは手前から描きますね。 しかし、TBDRなハードウェアであれば、 こんなことをしなくても確実に一番手前のものだけを塗ることができます。

とはいえ、仕組みは相当面倒です。 まず画面を16x16とかの小さな四角形に分割し、 それぞれに描画される三角形のリストを作り、 一番手前に来る三角形を探し、それだけを塗ります。 余計な手間を受け入れる代わりに、 不透明なものに関しては、 各ピクセル一回しかフラグメントシェーダを実行しなくて済みます。

半透明であればフラグメントシェーダは全て実行しないといけないので 利点の大半が消えますが、 それでもアルファブレンドやZテストが高速にでき、 Zバッファ用のメモリがいらない、という利点は残ります。 16x16分だけアルファブレンドやZテスト用のメモリがあれば良く、 それくらいの量ならGPUの中に用意できるので、メモリに取る必要がないのです。 実際にレンダーターゲットに書き込むのはタイルの描画完了後であり、 途中はメモリにアクセスしません。 詳しい説明は、先程のInfinity Bladeの資料中にもありますし、 PowerVRの公式文書にもあります。 しかし、今となっては知る必要もないことでしょう。

今回のテストのほとんどがアルファブレンド有効なのは、 TBDRな機械の性能も測りたいからです。 例えばHeavyCalcのような重いシェーダでも、TBDRな機械であれば最後の一回しかシェーダが走らず、 一見爆速に見えてしまいます。半透明にせざるを得ないわけです。

というわけで、このテストは機械がTBDRかどうかを知るためのテストです。 また、TBDRでない機種においては、アルファブレンドによる性能低下を 知ることができます。例えばS2ではわずかにスコアが上がっています。 この差分がアルファブレンドの負荷と推測できます。

終わりに

このように、残念な見掛けの雑なベンチマークでも、 いろいろなことがわかります。そして、それは ハードウェアの特徴を反映しているわけです。

そして最も大切なことは、スマホの性能の幅はムチャクチャ広い ということです。誰に向けて売るゲームなのか、 ということはよく考えないとけません。 「2年に一回iPhoneを買う人」が客なら最新技法をふんだんに使えますが、 「一番安いのをY!mobileで買ってきて、ガラスが割れたまま4年使ってる人」 も客にしたいのであれば、下に合わせざるを得ないのです。

しかも、スマホが成熟するにつれて、低価格帯の機種はほとんど性能が伸びなくなっています。 S2は404KCと同じメーカーの2世代後の機械ですが、大して速くなっていません。 今後も低価格帯の機械はさして速くならないでしょう。 性能が100倍違う機械に同じプログラムを提供せねばならない、ということを 真面目に考える必要があります。 下の機械でも十分に動きつつ、上の機械でもそれなりな満足が得られるような方策を、 できるだけ安く実現せねばなりません。なかなか難しい時代であると言えます。

なお、このベンチマークはまだまだ完成したわけではなく、 暇を見てテストを足したり、改善していきたいと思っています。 例えば足したいテストとしては、

  • ストレージ性能の測定
  • 2のべき乗でないサイズのテクスチャで性能が落ちるかどうか
  • 何回のDrawCallに耐えられるか

といったものがあります。DrawCallについては、 Unityの下にあるOpenGLやMetalの出来で大きく変わるはずで、 そこにどれくらいの差が出るかは確認しておきたいところです。

また、数字が落ちつくまでの時間ももっと短くしたいですし、 結果をtwitterに投稿できたりしてもいいかなと思っています。 とはいえ、正直面倒くさいので、どなたかやっていただけると幸いです。 コードはgithubに置いてありますので

ところで、このキャラ何?

f:id:hirasho0:20190131192607j:plain

起動直後に出ている若干アレなキャラクターは、名前を「コンチ」と言います。 弊社のオリジナルキャラクターです。 サイトがございますので、おヒマな方はご覧ください。

しばらく最初の画面を眺めていただけると、 無駄にレンダリングが凝っていることがおわかりになるかと思います (テキトーに画像から法線や粗さを生成して照明計算をしております )。

なお、実のところ、「ベンチマークを作ると決めてからコンチを使った」 のではなく、「コンチベンチ」という言葉が思いついたのが先です。 コンチがいなければ、ベンチマーク自体作っていなかったと思います。

おまけ

公開後に頂いたデータを並べておきます。

CPU

機械 ブラウザ 発売 FibonacciInt FibonacciFloat HeapSort QuaternionIntegration
i7-3770 GeForceFTX960 chrome - 0.065 0.078 0.881 0.389
XBoxOneX Edge - 0.141 0.198 4.77 1.42
iMac2017 chrome 2017 0.051 0.059 1.13 0.422
MacBookPro 15'' 2017 chrome - 0.051 0.058 1.12 0.384
iPhoneXS Max chrome - 0.137 0.136 1.18 0.386
Pixel3 XL chrome 2018 0.117 0.132 2.526 1.005
XperiaXZ2Premium chrome - 0.128 0.130 2.53 0.987
OnePlus6 chrome - 0.141 0.128 2.57 0.998
Zenpad 3S 10 chrome - 0.159 0.216 3.638 1.52
XperiaXZ1 chrome - 0.183 0.198 3.56 1.49
iPhoneSE safari - 0.188 0.187 2.51 0.643
OculusGo chrome - 0.208 0.281 6.275 1.77
SC9853i chrome - 0.575 0.624 10.7 3.11
FTJ162E-RAIJIN firefox - 0.328 0.420 8.46 6.79
Huawei P20 lite chrome - 0.528 0.555 7.751 3.839
ElephoneA4pro chrome - 0.467 0.501 8.52 3.46
ElephoneA4pro firefox - 0.790 0.446 7.49 5.09
Priori4 chrome 2017 0.778 0.814 11.4 5.77
HoloLens Edge - 0.876 1.22 13.0 7.76
FMV-BIBLO NF/C40 chrome 2008 0.162 0.209 2.96 0.993
tpad P20HD chrome 2020 0.733 0.577 8.735 3.577
Android One S6 chrome 2019/12 0.412 0.466 10.146 3.112
ZenFone Max Pro (M1) chrome 2018/12 0.244 0.302 5.957 2.000
Android One S3 chrome 2018/1 0.857 1.07 10.266 8.521
ADP-503G chrome 2020/7 2.156 2.430 15.056 6.088

GPU

機械 GPU Color ColorZ AlphaTestColor HeavyCalc CoherentTexture IncoherentTexture DependentTexture OpaqueColor
i7-3770 GeForceFTX960 GeForceGTX960 1450 1300 1610 402 1430 47.9 1430 1450
XBoxOneX - 787 569 801 270 578 53 574 801
iMac2017 RadeonPro555 508 488 524 105 340 33.1 339 521
MacBookPro 15'' 2017 RadeonPro560 547 519 558 150 339 18.8 339 556
iPhoneXS Max AppleA12 1310 1270 928 58.5 559 11.0 432 5430
Pixel3XL Adreno630 417 425 288 37.9 258 5.02 227 418
XperiaXZ2Premium Adreno630 435 432 386 68.6 328 5.97 329 437
OnePlus6 Adreno630 431 431 386 61.0 327 5.97 326 436
Zenpad 3S 10 PowerVR GX6250 147 146 54.1 5.00 67.0 1.00 67.0 549
XperiaXZ1 Adreno540 430 421 369 32.0 179 2.01 189 432
iPhoneSE AppleA9 314 316 160 21.0 241 3.03 197 1600
OculusGo Adreno530 226 221 197 17.0 107 1.00 108 231
SC9853i Mali-T820 51.1 50.2 15.1 1.98 44.1 1.02 19.0 101
FTJ162E-RAIJIN Mali-T860 19.7 30.7 13.5 2.01 28.5 1.00 14.6 62.3
Huawei P20 lite Mali-T830 65.9 64.5 37.7 4.95 61.1 0.98 42.2 130
ElephoneA4pro Mali-G71 45.1 42.8 40.0 3.99 44.5 2.01 42.9 101
ElephoneA4pro(firefox) Mali-G71 19.4 19.4 16.3 2.00 18.9 1.00 18.7 43.2
Priori4 Mali-T720 6.24 1.35 3.00 0.94 3.02 0.97 3.00 8.01
HoloLens - 26.1 19.3 28.1 1.26 16.6 0.47 15.4 29.1
FMV-BIBLO NF/C40 GMA 4500M 9.42 7.48 9.32 0.64 7.34 0.36 7.15 10.03
tpad P20HD PowerVR GE8322 81.07 78.64 36.02 2.77 60.29 0.26 47.31 471.01
Android One S6 PowerVR GE8320 92.92 93.96 41.50 3.42 72.67 1.00 58.02 702.96
ZenFone Max Pro (M1) Adreno 512 126.51 122.64 48.62 4.87 78.75 1.17 75.14 126.53
Android One S3 Adreno 505 59.16 55.40 26.38 2.07 36.48 0.55 35.17 52.46
ADP-503G Malii-T720 18.33 17.62 5.64 0.56 15.92 0.34 7.26 18.66

わかること

  • iPhoneXS MaxとiPhoneSEはTBDR臭いですね。Maliは単にアルファブレンドが遅いだけな印象。
  • Priori4は最低記録を更新しました!しかも2017年発売!メモリも2GB!
    • サイトには「クアドコアでサクサク」と書かれており、買う人が低性能と認識していない可能性大。しかも結構台数出てます。
    • GLES3対応で世代としては十分新しい。動かないわけではないが遅い、をどう扱うか。私ならこいつをテスト端末に入れますね。個人的には。
  • Mali-T820のDependentTextureの落ち込み方が気になります。
  • ブラウザでGPU性能は変わらないだろうと思っていましたが、Elephone見るとfirefoxで性能が落ちているので、そういうこともあるのかも。
  • XBoxOneXはCPUが安いコア8個なのでCPUは低く出てますが、それでもスマホのハイエンド並にはあります。そしてGPUはさすがに高い。HeavyCalcの高さが素の演算能力を表しており、コンピュートシェーダ性能で比べればiPhoneXなどとは比較にならない強さ。GPUで計算しろという強いメッセージを感じる結果ですね。

この記事の著者・平山のインタビュー

この記事の著者・平山のインタビューをカヤックサイトで公開しています。ぜひご覧ください!

www.kayac.com