Windowsの開発現場では、過去のアーキテクチャ移行や互換性維持の過程で、無数のワークアラウンドが基盤層に実装されてきた。MicrosoftのベテランエンジニアであるRaymond Chenは、x86以外のプロセッサでx86-32向けバイナリを実行するエミュレータを開発していた当時の事例を回顧した。HNの議論を参照すると、このエミュレータはAlphaアーキテクチャ向けのFX!32などの時代と推測される。このソフトウェアは、インタプリタ方式による速度低下を避けるため、元のx86-32コードを解析して動的にネイティブコードへ変換するバイナリトランスレーション(JITコンパイルに相当する仕組み)を採用していた。
開発チームがこのトランスレータをテストしていた際、特定のプログラムにおいて異常なメモリ消費と性能劣化を引き起こす関数が発見された。そのプログラム自体が行おうとしていた処理は、スタック上に約64KBのメモリを確保し、それを初期化するというごく単純な動作である。通常、このような処理はスタックポインタから65536を減算し、数バイトの短いループ命令を用いてメモリ全体に初期値を書き込む手順をとる。
しかし、対象のプログラムをコンパイルしたコンパイラは、ループによる反復処理のオーバーヘッドを嫌い、6万5536回に及ぶ「メモリへの1バイト書き込み命令」へと物理的に展開(ループアンローリング)していた。x86の命令長から換算すると、各書き込み命令は4バイトの容量を持つ。結果として、わずか64KBのデータを初期化するためだけに、実行バイナリ内に256KBものコード空間を占有する命令群が生成されていた。当時の限られたメモリ帯域と命令キャッシュ容量を踏まえると、この巨大なコードの塊がキャッシュラインを一掃し、プロセッサの性能を著しく損なうことは明白であった。
コンパイラの暴走とスタック制約の衝突
このような常軌を逸したコードが生成された背景には、開発者による最適化オプションの乱用と、OSレベルで規定されるメモリ保護機構との衝突が存在する。1990年代のコンパイラにおいても、条件分岐のコストを削減するためにループ展開を行う最適化手法は標準的に用いられていた。開発者がすべてのループを強制的に展開するような極端なフラグ(GCCにおける -funroll-loops に類する指定)を設定した場合、コンパイラは命令サイズ肥大化によるキャッシュペナルティを考慮せず、愚直に命令を羅列し続ける。
この展開を助長した要因として、Windows環境におけるスタックメモリの特異な制約が考えられる。スタックオーバーフローを検知するため、OSはスタックの先端にガードページという保護領域を配置する。プログラムが一度にガードページを超えるサイズ(通常は4KB単位)のスタック領域を確保しようとすると、不正なポインタ演算と区別がつかず、アクセス違反による強制終了を引き起こす。これを回避するため、コンパイラは巨大なスタック領域を確保する際、1ページごとに順番にダミーのアクセスを行ってガードページを安全に押し下げる「スタックプローブ(Microsoft C/C++コンパイラにおける _chkstk 関数など)」の呼び出しを挿入するよう設計されている。
問題のプログラムは、この64KBの領域をOSの制約に従って確保しつつ、確実に初期化を行う手順を模索した。その過程でスタックプローブの要件とコンパイラの極端なループ展開が最悪の形で噛み合った結果、単純な代入を繰り返すだけの256KBのコードへと膨れ上がったと推測される。エミュレータ開発チームは、このコードをハードウェア資源に対する「冒涜」と捉えた。そして、バイナリトランスレータ内部に特別な検知ロジックを追加し、この特定の無駄な命令群を見つけた際、元のプログラムの意図を汲み取って同等の短いループ処理へと動的に書き換えるという異例の修正を施した。
基盤ソフトウェアが背負う「アプリケーションの尻拭い」
この一件は、単なる特異なバグ対応の逸話ではない。ソフトウェアの歴史を俯瞰すると、アプリケーション側の非効率な実装やバグを、OS、エミュレータ、ドライバといったより下位のソフトウェアスタックが検知して秘密裏に吸収してきた実態が浮かび上がる。
1990年代の初期のMac向けグラフィックアクセラレータ開発の現場でも、同様の事態が頻発していた。ハードウェア側の描画速度をどれほど向上させても、アプリケーション側が無駄な命令を大量に発行していれば性能は発揮されない。ある著名な表計算ソフトは、セルに文字を描画する前、同じ領域を最大9回にわたって白く塗りつぶす無駄な処理を行っていた。また、DTPソフトのQuarkXPressでは、描画領域の計算ミスを恐れるあまり特殊な文字列渡しを行い、フォントレンダリングエンジン(ATM)が文字列を細かく分割して再計算を繰り返すというバグを抱えていた。これにより、描画処理の大部分の時間がCPUの乗算命令による文字幅の計算に費やされ、高価な24ビットカラー対応のアクセラレータハードウェアはアイドル状態のまま放置されていた。アクセラレータの開発陣は、対象のアプリケーション開発元にコードの修正を直接促しつつも、QuickDrawなどのAPIコールをフックし、無駄な塗りつぶし命令をハードウェア側で高速に処理して破棄することで、表面上の体感速度を保っていた。
現代のGPUドライバは、このアプローチを巨大なスケールで組織化している。ドライバのパッケージ内には、特定のゲームタイトルが抱える最適化不足やバグを回避するための膨大なプロファイルが含まれている。実行ファイルの名称(例として hl2.exe や quack3.exe など)を判定して対象ソフトウェアを特定し、ゼロ除算によるクラッシュを未然に防ぐために数学関数の結果を書き換えたり、テクスチャの精度を落としてベンチマークスコアを引き上げたりといった操作が日常的に行われている。基盤ソフトウェアは、表層のアプリケーションが正常かつ高速に動作しているように「見せかける」ため、設計本来の責務を超えた修正ロジックを背負い続けている。
計算資源の豊富さが隠蔽する構造的負債
ハードウェアの処理能力とメモリ容量が飛躍的に向上したことで、現在では256KBの冗長な初期化コードが存在しても、ユーザーがその遅延を体感することは難しい。しかし、根本的な非効率性が解消されたわけではなく、強力なハードウェアのリソースによって力技で覆い隠されているに過ぎない。
ファイル読み込み処理における非効率性も、ハードウェアに依存して放置されやすい問題の一つである。あるゲームソフトが数MBのデータを読み込む際、C標準ライブラリの fread 関数の引数指定を fread(data, 1, 65536, fptr) (1バイトの読み込みを6万5536回要求する)と記述した事例がある。これを fread(data, 65536, 1, fptr) (64KBの読み込みを1回要求する)と記述していれば、OSへのシステムコールは最小限で済む。しかし前者の記述では、内部の実装やバッファリングの状態によっては、OSの ReadFile APIを6万回以上呼び出す挙動に展開される場合がある。この非効率なコードは、通常のローカルストレージ環境では問題として顕在化しにくい。だが、システムコールをフックしてネットワーク越しにデータを取得するような特殊な仮想ファイルシステムを経由した途端、10秒で終わるはずのロード時間が数分へと膨れ上がり、致命的なボトルネックを引き起こす。結局、基盤側の開発者がファイル読み込みAPIのフック内に専用のキャッシュ機構を実装することで、このゲームの遅延は解消された。
ゲームのロード時間に関する事例としては、GTA Onlineの長期にわたるロード遅延問題も記憶に新しい。このゲームは、数MBのJSONファイルを解析する際、先頭から1文字ずつ読み直して文字列長を測定するという、計算量が二次関数的に増大するアルゴリズム(Schlemiel the Painter's Algorithm)を使用していた。後に外部のハッカーがバイナリパッチを作成し、読み込み位置を記憶させるだけでロード時間が10分以上から3分未満へと劇的に短縮された。
今後、表面上の要件を満たすコードを大量に出力するAI支援開発が普及するにつれ、実行可能ではあるものの背後で無駄な処理を繰り返すコードの割合は増加していく。それらの非効率性をプロファイリングして修正する責任を開発者自身が負わない場合、誰がその負債を引き受けるのか。Linux上でWindowsゲームを動作させるProtonやWineといった互換レイヤーでも、特定のゲームが抱える最適化不足を吸収するため、OSのAPIコールの単位でハックを導入する事例が増加している。エミュレータやドライバといった基盤層がヒューリスティクスを用いてアプリケーションの粗を隠し続ける構造は、システム全体の複雑性を際限なく引き上げ、予期せぬ不具合を誘発する要因として残り続ける。