【Unity】プロファイリングを効率良く進めるための3つのTips


https://qiita.com/advent-calendar/2021/sumzap

本記事は、[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap)の12/25の記事です。

## はじめに
アプリを安定してストレスなく動作させるため重要なパフォーマンスチューニングですが、
プロファイリング・修正・ビルドを繰り返し繰り返し行う根気の必要な作業です。
今回はそのパフォーマンスチューニングのうち、プロファイリング工程を円滑に進めるためのTipsを3つ紹介したいと思います。

## 1. プログラムから計測のタイミングをコントロールする
Unityの[Profiler API](https://docs.unity3d.com/ja/2020.3/ScriptReference/Profiling.Profiler.html)を活用することで、プログラムでプロファイリングの開始・終了をコントロールできます。

以下のコードでは、シーンの初期化処理の始めから終わりまでを計測しています。

```csharp
public class Scene
{
    ...

    public async Task InitializeAsync()
    {
        // 計測を開始する
        Profiler.logFile = LOG_FILE_PATH;
        Profiler.enableBinaryLog = true;
        Profiler.enableLog = true;

        await _componentA.InitializeAsync();
        await _componentB.InitializeAsync();

        // 計測を終了する
        Profiler.logFile = null;
        Profiler.enableLog = false;
    }
}
```

プロファイラウィンドウから手で計測をコントロールする場合はタイミングを合わせるのが難しいため、どうしても余分に計測しがちですが、
プログラムからであれば関心のある処理を過不足なく計測することができます。

Profile Analyzerでパフォーマンスチューニング前後のデータの比較を行う場合も、
過不足なくデータが取れていれば、比較対象のフレームを改めて指定する必要がないのでスムーズに分析ができると思います。

計測したデータは `Profiler.logFile` で指定したパスへ書き出されますが、
これはAndroidであれば、[adb pullコマンド](https://developer.android.com/studio/command-line/adb?hl=ja#copyfiles)やUSB ファイル転送モードで端末を接続することで取得できます。

## 2. Deep Profilingと通常のProfilingを併用する
Deep Profilingはメソッド1つ1つの詳細な計測ができるため、
非同期メソッド等、メソッド呼び出しのネストが深い箇所のボトルネックの洗い出しには必須とも言えます。

しかしながら常にDeep Profilingオンで良いかと言うと、そうとも言えない場合があります。

たとえば以下のコードのような、ファイルの中身を読み込む処理と、データを復号する処理があるとします。

```csharp
public byte[] LoadBytes(string path)
{
    using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
    var bytes = new byte[stream.Length];
    var count = bytes.Length;
    var offset = 0;

    while (count > 0)
    {
        var n = stream.Read(bytes, offset, count);
        if (n == 0) break;

        offset += n;
        count -= n;
    }

    return bytes;
}

public string Decrypt(byte[] data, byte[] key, byte[] iv)
{
    using var aes = Aes.Create();
    aes.Key = key;
    aes.IV = iv;

    using var decryptor = aes.CreateDecryptor();
    using var source = new MemoryStream(data, false);
    using var decrypt = new CryptoStream(source, decryptor, CryptoStreamMode.Read);
    using var reader = new StreamReader(decrypt, Encoding.UTF8);

    return reader.ReadToEnd();
}
```

この処理をDeep Profilingオフとオンでそれぞれプロファイリングすると、
**Deep Profilingオフ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/9fdc5629-b3db-9e85-4291-d25576e17d88.png)
**Deep Profilingオン**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/a0b33afb-c423-0a3f-ab70-8d7e82296842.png)
となります。
Deep Profilingのオンとオフで比較した場合、Decryptメソッドの処理時間が `+ 1494.19ms` と大幅に増えているのに対し、
LoadBytesメソッドの処理時間は `+ 2.79ms` でしかなく、大きな差が出ています。

この差を詳しく見るため、Deep Profilingオンのときのそれぞれの処理を展開してみると、
**Decrypt**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/ffdcf0a8-a661-9b10-9794-087af24ba0ee.png)
**LoadByes**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/924fe610-3a34-c6b5-882e-dbf48f903b1b.png)
処理内でメソッドを呼び出している数（`Calls`）に大きな差があることが読み取れます。
恐らくDeep Profilingの計測のオーバーヘッドが積み重なっていると思われますが、
このようにメソッド呼び出しの多い処理はそうでない処理に比べて、処理時間が誇張されてしまうことに注意が必要です。

したがって、Deep Profilingオンで見つけたボトルネックを直したけれどもリリースビルドではそれほど改善されていなかった、という事態を避けるためにも、
Deep Profilingだけではなく[CustomSampler](https://docs.unity3d.com/ja/2020.3/ScriptReference/Profiling.CustomSampler.html)や[ProfileMarker](https://docs.unity3d.com/2020.3/Documentation/ScriptReference/Unity.Profiling.ProfilerMarker.html)を使用した通常のProfilingも併用すると良いでしょう。

## 3. Standaloneプロファイラを活用する
Unity 2020より、プロファイラを独立したプロセスで動作させる[機能](https://docs.unity3d.com/ja/2020.3/Manual/ProfilerWindow.html#standalone)が追加されました。

**Window > Analysis > Profiler (Standalone Process)**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/3704e08c-4444-a490-9f11-1d30843edee8.png)

またPackage ManagerでProfile Analyzerをインストールしていれば、
プロファイラウィンドウの**Window > Analysis > Profile Analyzer**より、独立したプロセス上でProfile Analyzerを扱えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/da0c4664-3da1-a6f3-1443-efadf8a5ef5d.png)


これにより、プロファイリングデータのロード・分析中にアセットの修正やスクリプトの編集を行ったり、
また逆にアプリのビルドやスクリプトのコンパイルの影響を受けずに分析を進められます。

ともすればGB単位にもなるプロファイリングデータをロード・分析するのは時間がかかりますので、是非この機能を活用されてはいかがでしょうか。

Unity 2019以前ではこの機能はありませんが、
別のUnityプロジェクトを作成し、そこでプロファイラウィンドウを開くことで代替できます。
（ただしプロファイリング対象とプロファイラの両プロジェクトでUnityバージョンを合わせる必要が有ります）

## まとめ

- プログラムで計測の開始と終了をコントロールし、安定した正確な計測をしよう
- Deep Profilingの計測のオーバーヘッドを知り、通常のProfilingも併用してボトルネックを正確に特定しよう
- Standaloneプロファイラを使って分析と改善を円滑に進めよう

以上、この記事が何か1つでもプロファイリングの助けになれれば幸いです。