環境:UE4.27、VisualStudio2022
仕組みは知ってるからUEでの設定方法を知りたいんじゃーという方はこちらまで読み飛ばしてください。
はじめに
Profile Guided Optimizationを略してPGOと呼びます。日本語では「プロファイルに基づく最適化」といいます。
CPUの処理を最適化するコンパイラの機能です。
今日はこのPGOとはどういうものなのか?UnrealEngineで有効にするにはどうすればいいのか?を話したいと思います。
「プロファイルに基づく最適化」とは?
ざっくり言うと、ゲーム(アプリ)を実際にプレイし、情報を集めてそれを元に最適化する仕組みです。最適化の手順はこうです。
やってみよう
UEでのやり方は後で説明するとして、まずは簡単なプロジェクトで試してみましょう。
VisualStudio2022で新しいコンソールアプリを作成します。
次に、こういうコードを最適化してみます。
#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;
}
インストルメントビルドします
インストルメントされたexeを実行してpgcファイルを作ります
exeと同じ場所にpgcファイルができました
ではこれを使って最適化してみましょう
できました!超簡単ですね。
デバッグありで実行してみましょう。
カウントダウンが始まりますw焦らずに「ビルドを続行しない」を押します。VSはデバッグ実行時に勝手にビルドが走りますがビルドしちゃうと最適化が無効になっちゃうよという警告です。ビルドを続行しなければ問題ありません。
ビルドエラーが出ますが気にせず「はい」でアタッチした状態で実行できます。
PGOを適用するとどういう最適化が行われるのか?
・関数の適切なインライン化
コンパイラは自動でインライン化してくれますがよく呼び出される関数はインライン化し、そうでもない関数はインライン化してほしくないですよね。
・分岐命令における分岐予測の向上
たまにしか起きないnullチェックのif文もこのコードがたまにしか通らないというのはコンパイラには理解するのが難しいです。
こういうのを実際に実行した時の状況を元に最適化してくれます。
*他にも色々やってると思います
分岐予測ってなに?
なぜ分岐を予測できると速くなるのでしょうか?
それは命令キャッシュミスによるストールを防げるからです。
コンパイルされた命令は「メインメモリ」からロードされてCPUで実行されます。その時、より高速に読み込みができる「キャッシュ」にコピーが保存されます。キャッシュは高速に読み込みできる代わりに容量が小さく、プログラムを全部保存しておくことは出来ません。
近くの命令はキャシュにある可能性が高いですが、分岐命令によってジャンプした先が遠い場所にある場合キャッシュに乗っていない可能性があります。
そうするとメインメモリからロードする必要がでてきます。これがストールで、これが沢山おきるとパフォーマンスが下がってしまいます。
ようは、分岐命令ではなるべく分岐せずにシーケンシャルに処理したほうが効率的ってことになります。
PGOはそうなるように最適化してくれます。
実際にそうなっているか見てみましょう
先程試したコードをPGOなしでビルドした逆アセンブリがこちら
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
ループ部分が展開されて5回を1セットにしたループになっていますね。
ではPGO最適化したコードを見てみましょう。
コンソールの入力で1をいれてpgcを作って最適化してみます。
inputには1が入ります。
あれ?さっきと全く同じで変化ありませんでした。
今度はpgcを一旦消してinputに0を入れて最適化してみましょう。
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 は等しくないとき
に該当のアドレスにジャンプする命令です
実行時の入力によって最適化の結果が変わったのが確認していただけたと思います。
逆に、入力0で最適化したexeで0以外の入力をすると2回ジャンプしないといけないので逆に遅くなりそうです。
インストルメントビルドで実行する時は本番に近い入力でプロファイルを取ったほうが良さそうです。
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」
対処方法:FScriptNameを「PRAGMA_DISABLE_OPTIMIZATION / PRAGMA_ENABLE_OPTIMIZATION」で囲む
PRAGMA_DISABLE_OPTIMIZATION
struct FScriptName
{
...
}
PRAGMA_ENABLE_OPTIMIZATION
以上。