UnrealEngine　PGOによる最適化　仕組み解説～実践

環境：UE4.27、VisualStudio2022

仕組みは知ってるからUEでの設定方法を知りたいんじゃーという方は[こちら](#unrealengineでpgoを有効にする方法)まで読み飛ばしてください。

# はじめに
Profile Guided Optimizationを略してPGOと呼びます。日本語では「プロファイルに基づく最適化」といいます。
CPUの処理を最適化するコンパイラの機能です。

今日はこのPGOとはどういうものなのか？UnrealEngineで有効にするにはどうすればいいのか？を話したいと思います。

# 「プロファイルに基づく最適化」とは？
ざっくり言うと、ゲーム（アプリ）を実際にプレイし、情報を集めてそれを元に最適化する仕組みです。最適化の手順はこうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/0ccd7261-138c-81f8-a9da-9bfbca2cffcd.png)


# やってみよう
UEでのやり方は後で説明するとして、まずは簡単なプロジェクトで試してみましょう。
VisualStudio2022で新しいコンソールアプリを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/affe5b58-5d29-e039-df76-162ebfff4e7d.png)
次に、こういうコードを最適化してみます。
```cpp
#include <iostream>

int main()
{
	int input;
	std::cin >> input;

	constexpr int size = 10000;
	int array[size] = { 0 };

	int sum = 0;
	for (int i = 0; i < size; ++i)
	{
		if (array[i] == input) { continue; }

		sum += array[i];
	}

	std::cout << sum;
}
```
インストルメントビルドします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/f9ae1d07-cfa7-36d8-8afb-1c80afb4511d.png)
インストルメントされたexeを実行してpgcファイルを作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/1702fb37-dbb4-cc6a-4bb9-7855bec16096.png)
exeと同じ場所にpgcファイルができました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/2013a71a-2115-9b01-c40b-507ecfa0b847.png)
ではこれを使って最適化してみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/d442c3af-bfe5-9766-b591-ca88c37984ba.png)
できました！超簡単ですね。
デバッグありで実行してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/b88dc82e-0b47-a80a-e56b-494f89a2a09b.png)
カウントダウンが始まりますｗ焦らずに「ビルドを続行しない」を押します。VSはデバッグ実行時に勝手にビルドが走りますがビルドしちゃうと最適化が無効になっちゃうよという警告です。ビルドを続行しなければ問題ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/702105c7-32c6-692b-a421-42b25615240d.png)
ビルドエラーが出ますが気にせず「はい」でアタッチした状態で実行できます。

# PGOを適用するとどういう最適化が行われるのか？
### ・関数の適切なインライン化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/190d8472-b99a-7b0d-e90f-8c27fa54e469.png)
コンパイラは自動でインライン化してくれますがよく呼び出される関数はインライン化し、そうでもない関数はインライン化してほしくないですよね。

### ・分岐命令における分岐予測の向上
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/719ef93e-27c2-2006-2922-b264d89611c4.png)
たまにしか起きないnullチェックのif文もこのコードがたまにしか通らないというのはコンパイラには理解するのが難しいです。

こういうのを実際に実行した時の状況を元に最適化してくれます。
＊他にも色々やってると思います

# 分岐予測ってなに？
なぜ分岐を予測できると速くなるのでしょうか？
それは命令キャッシュミスによるストールを防げるからです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/7354671f-9697-f7c5-32a7-2f7a660f580d.png)
コンパイルされた命令は「メインメモリ」からロードされてCPUで実行されます。その時、より高速に読み込みができる「キャッシュ」にコピーが保存されます。キャッシュは高速に読み込みできる代わりに容量が小さく、プログラムを全部保存しておくことは出来ません。
近くの命令はキャシュにある可能性が高いですが、分岐命令によってジャンプした先が遠い場所にある場合キャッシュに乗っていない可能性があります。
そうするとメインメモリからロードする必要がでてきます。これがストールで、これが沢山おきるとパフォーマンスが下がってしまいます。

ようは、分岐命令ではなるべく分岐せずにシーケンシャルに処理したほうが効率的ってことになります。
PGOはそうなるように最適化してくれます。

# 実際にそうなっているか見てみましょう
先程試したコードをPGOなしでビルドした逆アセンブリがこちら
```cpp
int main()
{
// ～省略～
	int sum = 0;
00007FF7CE06104C  xor         edx,edx  
00007FF7CE06104E  mov         r9d,7D0h  
	{
		if (array[i] == input) { continue; }
00007FF7CE061054  mov         r8d,dword ptr [rax-4]  
00007FF7CE061058  cmp         r8d,ecx  
00007FF7CE06105B  je          main+60h (07FF7CE061060h)  

		sum += array[i];
00007FF7CE06105D  add         edx,r8d  
	{
		if (array[i] == input) { continue; }
00007FF7CE061060  mov         r8d,dword ptr [rax]  
00007FF7CE061063  cmp         r8d,ecx  
00007FF7CE061066  je          main+6Bh (07FF7CE06106Bh)  

		sum += array[i];
00007FF7CE061068  add         edx,r8d  
	{
		if (array[i] == input) { continue; }
00007FF7CE06106B  mov         r8d,dword ptr [rax+4]  
00007FF7CE06106F  cmp         r8d,ecx  
00007FF7CE061072  je          main+77h (07FF7CE061077h)  

		sum += array[i];
00007FF7CE061074  add         edx,r8d  
	{
		if (array[i] == input) { continue; }
00007FF7CE061077  mov         r8d,dword ptr [rax+8]  
00007FF7CE06107B  cmp         r8d,ecx  
00007FF7CE06107E  je          main+83h (07FF7CE061083h)  

		sum += array[i];
00007FF7CE061080  add         edx,r8d  
	{
		if (array[i] == input) { continue; }
00007FF7CE061083  mov         r8d,dword ptr [rax+0Ch]  
00007FF7CE061087  cmp         r8d,ecx  
00007FF7CE06108A  je          main+8Fh (07FF7CE06108Fh)  

		sum += array[i];
00007FF7CE06108C  add         edx,r8d  
00007FF7CE06108F  add         rax,14h  
	for (int i = 0; i < size; ++i)
00007FF7CE061093  sub         r9,1  
00007FF7CE061097  jne         main+54h (07FF7CE061054h)  
	}

	std::cout << sum;
00007FF7CE061099  mov         rcx,qword ptr [__imp_std::cout (07FF7CE062098h)]  
00007FF7CE0610A0  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7CE062088h)]  
}
00007FF7CE0610A6  xor         eax,eax  
00007FF7CE0610A8  mov         rcx,qword ptr [rsp+9C70h]  
00007FF7CE0610B0  xor         rcx,rsp  
00007FF7CE0610B3  call        __security_check_cookie (07FF7CE0610D0h)  
00007FF7CE0610B8  add         rsp,9C88h  
00007FF7CE0610BF  ret  
```
ループ部分が展開されて５回を１セットにしたループになっていますね。
ではPGO最適化したコードを見てみましょう。
コンソールの入力で1をいれてpgcを作って最適化してみます。
inputには1が入ります。

あれ？さっきと全く同じで変化ありませんでした。
今度はpgcを一旦消してinputに0を入れて最適化してみましょう。
```cpp
int main()
{
// ～省略～
	int sum = 0;
00007FF69AC0104C  xor         r10d,r10d  
00007FF69AC0104F  mov         r9d,7D0h  
	{
		if (array[i] == input) { continue; }
00007FF69AC01055  mov         edx,dword ptr [rax-4]  
00007FF69AC01058  cmp         edx,ecx  
00007FF69AC0105A  jne         main+0ADh (07FF69AC010ADh)  
00007FF69AC0105C  mov         edx,dword ptr [rax]  
00007FF69AC0105E  cmp         edx,ecx  
00007FF69AC01060  jne         main+0B2h (07FF69AC010B2h)  
00007FF69AC01062  mov         edx,dword ptr [rax+4]  
00007FF69AC01065  cmp         edx,ecx  
00007FF69AC01067  jne         main+0B7h (07FF69AC010B7h)  
00007FF69AC01069  mov         edx,dword ptr [rax+8]  
00007FF69AC0106C  cmp         edx,ecx  
00007FF69AC0106E  jne         main+0BCh (07FF69AC010BCh)  
00007FF69AC01070  mov         r8d,dword ptr [rax+0Ch]  
00007FF69AC01074  cmp         r8d,ecx  
00007FF69AC01077  jne         main+0C1h (07FF69AC010C1h)  
00007FF69AC01079  add         rax,14h  
	for (int i = 0; i < size; ++i)
00007FF69AC0107D  sub         r9,1  
00007FF69AC01081  jne         main+55h (07FF69AC01055h)  
	}

	std::cout << sum;
00007FF69AC01083  mov         rcx,qword ptr [__imp_std::cout (07FF69AC02098h)]  
00007FF69AC0108A  mov         edx,r10d  
00007FF69AC0108D  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF69AC02088h)]  
}
00007FF69AC01093  xor         eax,eax  
00007FF69AC01095  mov         rcx,qword ptr [rsp+9C70h]  
00007FF69AC0109D  xor         rcx,rsp  
00007FF69AC010A0  call        __security_check_cookie (07FF69AC010E0h)  
00007FF69AC010A5  add         rsp,9C88h  
00007FF69AC010AC  ret  

		sum += array[i];
00007FF69AC010AD  add         r10d,edx  
00007FF69AC010B0  jmp         main+5Ch (07FF69AC0105Ch)  
00007FF69AC010B2  add         r10d,edx  
00007FF69AC010B5  jmp         main+62h (07FF69AC01062h)  
00007FF69AC010B7  add         r10d,edx  
00007FF69AC010BA  jmp         main+69h (07FF69AC01069h)  
00007FF69AC010BC  add         r10d,edx  
00007FF69AC010BF  jmp         main+70h (07FF69AC01070h)  
00007FF69AC010C1  add         r10d,r8d  
00007FF69AC010C4  jmp         main+79h (07FF69AC01079h)  
```
jeだったところがjneになりましたね。
je　は等しいとき
jne は等しくないとき
に該当のアドレスにジャンプする命令です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/cbe16e6c-7384-f266-072f-849bb8dc4317.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/6817e09b-32b9-6af6-8ecd-2e117ed0d88c.png)

実行時の入力によって最適化の結果が変わったのが確認していただけたと思います。
逆に、入力0で最適化したexeで0以外の入力をすると２回ジャンプしないといけないので逆に遅くなりそうです。

インストルメントビルドで実行する時は本番に近い入力でプロファイルを取ったほうが良さそうです。

# UnrealEngineでPGOを有効にする方法

## Windows版
### 1.インストルメントビルド
`-PGOProfile`
このオプションを付けてパッケージを作成

例
```
%ROOT_DIR%/Engine/Build/BatchFiles/RunUAT.bat BuildCookRun -project=MyProject.uproject -noP4 -platform=Win64 -clientconfig=Test -utf8output -cook -allmaps -build -pak -stage -package -PGOProfile
```

### 2.ゲームを実行してプロファイルを取る
コンソールコマンドで
- プロファイル開始　`pgo start`
- ゲームをプレイ
- プロファイル終了　`pgo end`

### 3.PGO最適化したパッケージを作成
リンク時最適化も入れたい場合はtarget.csに以下を追加しておく
`bAllowLTCG = true;`

`-PGOOptimize`
このオプションを付けてパッケージを作成

例
```
%ROOT_DIR%/Engine/Build/BatchFiles/RunUAT.bat BuildCookRun -project=MyProject.uproject -noP4 -platform=Win64 -clientconfig=Test -utf8output -cook -allmaps -build -pak -stage -package -PGOOptimize
```

完了！

## 問題が出た場合の対処方法
- パッケージ起動時にクラッシュする場合
.pkgサイズが大きすぎるとだめっぽい
対処方法：最適化のオプションをサイズ優先のものに変更

```
- Result += " -O3";
+ Result += " -Oz";
```

- BPの動作がおかしくなる、特定のノードが実行されない場合
ノード名に日本語が入っていると起きる場合がある↓だと「カスタムイベント_0」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/ab3de8ad-1ec3-3038-a33b-d6e3bcd3c392.png)
対処方法：FScriptNameを「PRAGMA_DISABLE_OPTIMIZATION / PRAGMA_ENABLE_OPTIMIZATION」で囲む
```cpp
PRAGMA_DISABLE_OPTIMIZATION
struct FScriptName
{
...
}
PRAGMA_ENABLE_OPTIMIZATION
```

以上。