ジョン・カーマック流の「パフォーマンス最優先」マインドセットの実践ガイド:プロファイリング、フレームタイム予算、トレードオフ、複雑なリアルタイムシステムを出荷する方法まで。

ジョン・カーマックはゲームエンジン界の伝説として扱われがちですが、有用なのは神話ではなく繰り返し再現できる習慣です。これは個人のスタイルをそのまま真似する話でも「天才の一手」を想定する話でもありません。締め切りや複雑さが積み重なる状況で、より速く滑らかなソフトウェアを安定的に作るための実践的原則の話です。
パフォーマンスエンジニアリングは、実機の実際の条件下でソフトウェアが速度目標を満たすようにすること—正しさを壊さずに—です。これは「何が何でも速くする」という話ではありません。規律あるループです:
このマインドセットはカーマックの仕事に何度も現れます:データで議論し、変更は説明可能に保ち、維持しやすい手法を好む。
リアルタイムグラフィックスは厳しい締め切り(各フレームごと)を持つため容赦がありません。締め切りを守れなければ、ユーザーは即座にスタッター、入力遅延、不均一な動きとして感じます。他のソフトウェアはキューやロード画面、バックグラウンド作業で非効率を隠せますが、レンダラは妥協できません:時間内に終えるかどうかだけです。
この教訓はゲーム以外にも一般化します。UI、オーディオ、AR/VR、トレーディング、ロボティクスなど、遅延に厳しいシステムは予算思考、ボトルネックの理解、突発的なスパイクの回避から恩恵を受けます。
フレームタイム(あるいはレイテンシ)予算の立て方、最適化前のプロファイリングのやり方、修正する「一つのこと」の選び方、そして回帰を防いでパフォーマンスを後回しのパニックでなく日常運用にするためのチェックリストやヒューリスティクスが手に入ります。
カーマック流のパフォーマンス思考は単純な切り替えから始まります:主要単位として「FPS」を語るのをやめ、フレームタイムで語り始めることです。
FPSは逆数で(「60 FPS」は良い、「55 FPS」は近いと感じられる)感覚に基づく指標ですが、ユーザー体験は各フレームにかかる時間と、その時間の一貫性に左右されます。16.6 msから33.3 msに跳ねると、平均FPSが見かけ上悪くなくても即座に違和感が出ます。
リアルタイム製品は「レンダを速くする」以外の複数の予算を持っています:
これらの予算は相互に影響します。GPU時間を節約するためにCPU負荷の高いバッチングを入れると裏目に出ることもあり、メモリを削るとストリーミングや解凍コストが増えることもあります。
もし目標が60 FPSなら、総予算は1フレームあたり16.6 msです。大まかな内訳は次のようになるかもしれません:
CPUかGPUのどちらかが予算を超えればフレームを落とします。だからチームは「CPUバウンド」「GPUバウンド」と話すのです—ラベルではなく、次にどこからミリ秒を取るかを決めるための手段です。
重要なのはハイエンドPCでの最高FPSを追いかけることではありません。ターゲットとするオーディエンス(対象ハード、解像度、バッテリー制限、サーマル、入力応答性)にとって何が十分速いかを定義し、それを管理・擁護できる明示的な予算として扱うことです。
カーマックのデフォルトの動きは「最適化する」ではなく「検証する」です。リアルタイムのパフォーマンス問題はもっともらしい説明が山ほどあり、その多くはあなたのビルドや機材では間違っています。プロファイリングによって直感を証拠に変えます。
プロファイリングを最終手段ではなく第一級の機能として扱ってください。フレームタイム、CPU/GPUのタイムライン、そしてそれらを説明するカウント(トライアングル数、ドローコール、状態変更、アロケーション、可能ならキャッシュミス)を取得します。目的は一つの問いに答えること:時間は実際にどこに行っているのか?
有用なモデル:遅いフレームごとに一つの要素が制約になっていることが多いです。GPUが重いパスで詰まっているのか、CPUのアニメーション更新が詰まっているのか、メインスレッドが同期で止まっているのか。まずその制約を見つけ、他はノイズと見なします。
規律あるループがムダな改変を防ぎます:
改善が明確でないなら、それは助けになっていないと仮定してください—次のコンテンツ追加で残らない可能性が高いです。
パフォーマンス作業は自己欺瞞に陥りやすいです:
まずプロファイリングすることで努力が集中し、トレードオフが正当化され、変更はレビューで説明しやすくなります。
リアルタイムのパフォーマンス問題はごちゃごちゃしているように見えますが、実は同時に多くの作業が進んでいるからです:ゲームプレイ、レンダリング、ストリーミング、アニメーション、UI、物理。カーマックの本能はノイズを切り裂き、現在フレームタイムを決めている支配的制約を特定することです。
遅延の大部分は次のいくつかのバケットに入ります:
ラベル付けが目的ではなく、適切なレバーを選ぶことが目的です。
いくつかの高速な実験で、何が支配的か分かります:
10のシステムをそれぞれ1%削るより、毎フレーム繰り返される最大のコストを見つけて潰す方が勝ちます。単一の4 msのオフエンダーを取り除く方が、週単位のマイクロ最適化より効果的です。
大きな石を直すと、次に大きな石が見えてきます。それは正常です。パフォーマンス作業はループです:測る→変える→再測定→優先順位付け。目標は完璧なプロファイルではなく、予測可能なフレームタイムに向けた着実な進捗です。
平均フレームタイムが良くても、体験が悪いことがあります。リアルタイムグラフィックスは最悪の瞬間で評価されます:大爆発で落ちるフレーム、新しい部屋に入ったときのヒッチ、メニュー開閉時の突然のスタッター。これがテール遅延です—まれだが許容できない遅いフレーム。
たとえ通常は16.6 ms(60 FPS)で動作していても、数秒ごとに60–120 msにスパイクするなら体感は「壊れている」となります。人間はリズムに敏感で、長いフレームひとつで入力予測性、カメラ動作、音声/映像の同期が壊れます。
スパイクは均等に分散されない作業から生じます:
高コストな作業を予測可能にする:
平均FPSのラインだけを描かないでください。フレームごとのタイミングを記録して可視化します:
最悪の1%のフレームを説明できないなら、パフォーマンスを本当に説明したことにはなりません。
すべてを一度に得られると装わない瞬間からパフォーマンス作業は容易になります。カーマック流はチームにトレードオフを声に出して名前を付けさせます:何を得て、何を払って、誰が違いを感じるのか。
多くの決定は次の軸上にあります:
ある変更が1つの軸を改善して3つにコストを課すなら、それを文書化してください。「これでソフトシャドウが柔らかくなるがGPUに0.4 ms、VRAMに80 MBを追加する」は有用な表現です。「見た目が良い」だけでは不十分です。
リアルタイムグラフィックスは完璧を目指すものではなく、目標を一貫して満たすことが重要です。次のような閾値に合意してください:
チームが例えば「1080p、ベースラインGPUで16.6 ms」という目標に合意すれば、議論は具体化します:この機能は予算内か、それとも他を削るべきか?
不確かなら元に戻せる選択を:
可逆性はスケジュールを保護します。安全側で出荷して、野心的なものはトグルの向こうに残しておけます。
目に見えない小さな平均改善に多くの工数を投じないでください。1%の平均改善が1ヶ月の複雑さに見合うことは稀です—ただし、それがスタッターを無くす、入力遅延を改善する、致命的なメモリクラッシュを防ぐなら別です。プレイヤーが即座に感じる変更を優先し、残りは後回しにします。
プログラムが正しいとき、パフォーマンス作業は劇的に楽になります。多くの「最適化」時間は実際には正しさのバグ追跡に費やされています:重複作業で生じた偶発的なO(N^2)ループ、フラグのリセット忘れで2回実行されるレンダーパス、フレームタイムを徐々に悪化させるメモリリーク、ランダムなスタッターを生むレースコンディションなど。
安定で予測可能なエンジンはクリーンな計測を与えます。挙動が実行ごとに変わるとプロファイルを信用できず、ノイズを最適化してしまいます。
規律ある工学プラクティスが速度を助けます:
多くのフレームタイムスパイクは「ハイゼンバグ」です:ログを足したりデバッガで止めると消える。解決策は決定論的な再現です。
小さな制御されたテストハーネスを作ります:
ヒッチが出たら、それを100回再生できるボタンが欲しい—「10分後に時々出る」という曖昧な報告ではなく。
スピード作業は小さくレビュー可能な変更が向きます。大きなリファクタは多くの失敗モードを同時に生みます:回帰、新たなアロケーション、隠れた追加作業。差分が小さければ「フレームタイムが何で、なぜ変わったか?」に答えやすくなります。
規律は官僚主義ではなく、測定の信頼性を保ち、最適化を迷信でなく確かなものにするための手段です。
リアルタイムのパフォーマンスは「より速いコード」だけが全てではありません。CPUとGPUが効率よく作業できるように仕事を配置することです。カーマックは繰り返し言っています:マシンは文字どおりで、予測可能なデータが好きで、避けられるオーバーヘッドを嫌います。
現代のCPUは非常に高速ですが、メモリ待ちになると途端に停滞します。データがたくさんの小さなオブジェクトに分散していると、CPUはポインタ追跡に時間を費やし計算が進みません。
有用な比喩:10個の買い物に10回買い物に行くな。1つのカートにまとめて、通路を一度だけ歩け。コードでは頻繁に使う値を近くに集め(配列やタイトにパックした構造体)、1つのキャッシュラインで使うデータを多く取り込めるようにします。
頻繁なアロケーションは隠れたコストを生みます:アロケータのオーバーヘッド、メモリ断片化、不定期のポーズ。たとえ1つずつは小さくても、継続的なストリームはフレームごとに税を課します。
一般的な対策は意図的に地味です:バッファを再利用する、オブジェクトプールを使う、ホットパスでは長寿命アロケーションを好む。目的は巧妙さではなく一貫性です。
フレームタイムのかなりの部分はブックキーピングに消えます:状態変更、ドローコール、ドライバ作業、システムコール、スレッド同期など。
バッチングはレンダリングとシミュレーションの「大きなカート」版です。多数の小さな操作を発行する代わりに類似作業をまとめ、コストの高い境界を越える回数を減らします。多くの場合、オーバーヘッドを削ることがシェーダーやインナーループを微調整するより大きな勝利を生みます。
パフォーマンス作業は単に速いコードを作ることだけでなく、コードを少なく持つことでもあります。複雑さには毎日支払うコストがあり:バグの特定に時間がかかり、修正にはより注意深いテストが必要になり、反復が遅くなり、回帰が稼働頻度の低い経路から忍び込むことがあります。
「巧妙な」システムは美しく見えるかもしれませんが、締め切り前に特定のマップ、GPU、設定コンボだけでスパイクが出ると問題になります。余計なフィーチャーフラグ、フォールバック、特例は挙動の組み合わせを増やし、理解と計測の負担を増やします。この複雑さは開発者時間を浪費するだけでなく、ランタイムオーバーヘッド(余分な分岐、アロケーション、キャッシュミス、同期)として現れることが多く、手遅れになるまで見えにくいです。
良いルール:パフォーマンスモデルを数分でチームメイトに説明できないなら、おそらく信頼して最適化できません。
単純な解決には二つの利点があります:
時には最速の道は機能を削ること、オプションを減らすこと、複数のバリアントを統合することです。機能が少なければコードパスが少なくなり、状態の組み合わせが減り、パフォーマンスが密かに劣化する場所が減ります。
コードを削ることは品質向上でもあります:生成し得るバグを生むモジュールを削除してしまえば、そのバグは存在しません。
パッチ(外科的修正)を選ぶとき:
リファクタ(構造の簡素化)を選ぶとき:
簡潔さは「野心がない」ことではありません。プレッシャー下でも理解できる設計を選ぶことです—パフォーマンスが最も重要なときに。
パフォーマンス作業が定着するのは、劣化を検知できる仕組みがあるときだけです。これがパフォーマンス回帰テストの目的:新しい変更が遅くしていないか、滑らかさを失っていないか、メモリ負荷が重くなっていないかを反復可能に検出することです。
機能テストが「動くか」を答えるのに対し、回帰テストは「速さが保たれているか」を答えます。ビルドが機能的に正しくても、フレームタイムが4 ms増えたりロードが倍になったら良いリリースとは言えません。
ラボは不要です—一貫性が必要です。
目標は完璧な数値ではなく、信頼できるトレンドラインです。
議論しにくい指標を選びます:
シンプルな閾値を定義する(例:p95フレームタイムが5%以上悪化してはいけない)。
回帰はバグとして扱い、オーナーと期限をつけます。
まずバイセクトして導入された変更を特定する。回帰がリリースを阻害するなら素早くリバートし、修正を入れて再適用します。
修正したらガードレールを追加:テストを残し、コードに注記し、期待される予算を文書化する。習慣化が勝利です—パフォーマンスは「後でやるもの」ではなく維持するものになります。
「出荷」はカレンダーのイベントではなく工学的要件です。ラボでしかうまく動かない、手作業で調整しないと目標に届かないシステムは完成とは言えません。カーマックのマインドセットは実世界の制約(ハードの多様性、雑多なコンテンツ、予測不能なプレイヤー行動)を初期仕様として扱います。
リリース間近では完璧より予測可能性が重要です。必須条件を平易に定義します:目標FPS、最悪フレームタイムの閾値、メモリ上限、ロード時間。これらに違反するものを「ポリッシュ」ではなくバグとして扱います。これによりパフォーマンス作業は任意の最適化から信頼性確保に変わります。
すべての遅延が同じ重みではありません。ユーザー視点で重要な問題を先に直します:
プロファイリングの規律が効きます:どの問題が「大きい」かを測定に基づいて選べます。
レイトサイクルでのパフォーマンス作業は危険です。変更は計測器を先に入れ、トグルの背後で有効にし、露出範囲を広げる運用が安全です。デフォルトはパフォーマンスを守る設定を選ぶべきで、見た目が少し控えめでも安定感を優先します。
プラットフォームやティアが複数あるなら、デフォルトは製品的な決定です:豪華に見せるより安定して感じさせる方が良い場合が多い。
トレードオフを成果ベースで翻訳します:「この効果は中級GPUで毎フレーム2 msを消費し、戦闘時に60 FPSを割るリスクがあります」。提案は選択肢にします:解像度を下げる、シェーダーを簡素化する、スポーン率を制限する、あるいは低い目標を受け入れる。制約は具体的な選択肢とユーザーへの影響で示すと受け入れられやすいです。
新しいエンジンや全面書き換えは不要です。カーマック流の思考を採り入れるには、パフォーマンスを可視化し、テスト可能にし、偶発的に壊れにくくする反復可能なループが必要です。
これらの習慣をチーム横断で運用化したいなら、鍵は摩擦を減らすことです:手早い実験、再現可能なハーネス、簡単なロールバック。
Koder.aiはエンジン自体ではなく周辺ツールを構築する際に役立ちます。vibe-codingプラットフォームとして実際にエクスポートできるソースコード(ReactのWebアプリ、GoとPostgreSQLのバックエンド、Flutterのモバイル)を素早く生成できるので、フレームタイムパーセンタイルや回帰履歴、パフォーマンスレビューのチェックリスト用の内部ダッシュボードを速やかに立ち上げ、チャットで要件を進化させながらスナップショットやロールバックも利用できます。
詳しいガイダンスが欲しい場合は /blog を参照するか、チームがどのように運用化しているかは /pricing をご覧ください。
フレームタイムはミリ秒(ms)で表される「1フレームあたりの時間」で、CPU/GPUが実際に行った作業量に直接対応します。
例えば60 FPSを目標とするなら、それをハードな締め切り(16.6 ms)に変換してから、さらに明確な予算に分割します。
例としての出発点:
これらを製品要件として扱い、プラットフォーム、解像度、サーマル、入力遅延などに合わせて調整してください。
まずテストを再現可能にし、何かを変える前に計測してください。
まず「時間がどこに行っているか」を把握してから最適化の判断をしてください。
支配的な制約を切り分ける高速な実験を行ってください。
支配的なコストをミリ秒で名指しできるまで、システムを書き換えないでください。
ユーザーは平均ではなく「最悪のフレーム」を感じます。
次を追跡してください:
平均が16.6 msでも80 msにスパイクするビルドは体感上壊れていると感じられます。
高コストな作業を予測可能にし、分散させることが目的です:
また、スパイクをログ化して再現できるようにすることが重要です。
数値とユーザー影響でトレードオフを明示してください。
例:
合意された閾値に照らして決めます:
不確かなら可逆的な選択(フィーチャーフラグやスケーラブル設定)を優先してください。
挙動が安定していないと計測結果は信用できません。
実践的な手順:
再現できるようにすれば、パフォーマンスの問題をノイズではなく原因に基づいて直せます。
“速いコード”の多くはメモリやオーバーヘッドに関する仕事です。
注力点:
しばしば、オーバーヘッドを削ることがインナーループの微調整より大きな効果を生みます。
パフォーマンスを測定可能で再現性のある習慣にしてください。
回帰が出たらし、担当を決め、リリースを阻害するなら素早くリバートしてください。