1999年夏。アムラーが影を潜め、街中に浜崎あゆみの「A Song for ××」が流れていた頃の話です。
渋谷109前を通りかかると、厚底ブーツにミニスカート、パレオを着た日焼けサロンで焼いた肌の女の子たちが、最新号の「egg」1を片手に談笑していました。駅前では女子高生たちがパラパラを踊り、少し離れた場所では「men's egg」2を読んだ男子たちが、ひろむ、けんう、ヨッシーのファッションを真似て闊歩していました。
そんな喧騒から離れた自宅で、私はTWOTOPで買ったばかりのPentium II 400MHzマシンと向き合っていました。三菱のモニターと合わせて、お年玉を相当つぎ込んだ自慢のマシンです。
「お前、プログラミングできるんだろ?」
夕食時、Life with UNIX3を読んでいる私に父が突然そう切り出しました。
「知り合いのソフトウェアハウスでプログラマーのアルバイトを探してるらしい。興味あるか?」
当時、プログラミングができる人材は今と比べても10倍程は希少種でした。
アルバイト先での仕事
オフィスの風景
翌週、父の知り合いのソフトウェアハウスを訪れた私を待っていたのは、予想外の光景でした。
[オフィスのPC環境]
OS: Windows 98 Second Edition
CPU: Pentium II 450MHz(自宅より速い!)
メモリ: 128MB(贅沢すぎる!)
画面: SETI@homeのスクリーンセーバーが動作中
BGM: 有線放送から流れる「Boys & Girls」
社長が画面を指さしながら説明してくれました。手元には当時流行りのN501iが置かれていました。
「ああ、それSETI@homeっていってね。宇宙人からの信号を探すプロジェクトなんだ。うちのPCも夜中は全部これを動かしてる」
窓の外では、プリクラ帰りの女子高生たちが「TO BE」を歌いながら通り過ぎていきます。宇宙人を探すコンピュータと、街を彩るギャルたち。1999年の日本は、不思議なコントラストに満ちていました。
最初の仕事、画像処理の高速化
社長から最初に与えられた仕事は、社内で使用している画像処理ソフトの高速化でした。Windows GDI4を使った、典型的な画像処理アプリケーションでした。
// 既存のコード(恐ろしく遅い)
void ConvertToGrayscale(BYTE* pSrc, BYTE* pDst, int width, int height)
{
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = (y * width + x) * 3;
BYTE r = pSrc[offset + 0];
BYTE g = pSrc[offset + 1];
BYTE b = pSrc[offset + 2];
// NTSCの輝度計算式
float gray = 0.299f * r + 0.587f * g + 0.114f * b;
pDst[y * width + x] = (BYTE)gray;
}
}
}
640×480の画像を処理するのに3秒もかかっていました。「これじゃ仕事にならない」という社員の声が聞こえてきます。
最初の最適化、浮動小数点を排除
まず、浮動小数点演算を整数演算に置き換えました。
// 第一段階の最適化
void ConvertToGrayscale_Optimized(BYTE* pSrc, BYTE* pDst, int width, int height)
{
for (int i = 0; i < width * height; i++) {
BYTE r = *pSrc++;
BYTE g = *pSrc++;
BYTE b = *pSrc++;
// 固定小数点演算(8ビット精度)
int gray = (77 * r + 150 * g + 29 * b) >> 8;
*pDst++ = (BYTE)gray;
}
}
処理時間は1秒まで短縮。でも、まだ遅い。
MMXへの挑戦、学生バイトの無謀な試み
MMXという希望
オフィスにあるPentium II 450MHzがうらやましくて仕方ありませんでした。自宅のPentium II 400MHzとは、わずか50MHzの差ですが、その差が妙に気になる年頃でした。
でも、ある日気づいたのです。
「MMXって、まだ誰も使いこなしてないんじゃないか?」
当時、MMX対応ソフトは「対応」と謳いながら、実際にはほとんど最適化されていませんでした。これはチャンスかもしれない。
CPUを理解する必要性
MMX最適化に取り組む前に、私はCPUがどのように命令を処理しているのかを調べ始めました。当時はインターネット上の情報も限られていましたが、Intel社のマニュアルを必死に読み込みました。
そこで知ったのは、CPUが命令を「命令流」として処理しているということでした。つまり、プログラムは小さな命令の列に変換され、それらが時間軸に沿って順番に実行されていくのです。
時刻 t0: 命令1 → CPUで処理される
時刻 t1: 命令2 → CPUで処理される
時刻 t2: 命令3 → CPUで処理される
さらに、Pentium IIは「パイプライン処理」を採用していることも学びました。これは、命令の実行が完了する前に次の命令の処理を開始することで、全体的な処理速度を向上させる技術です。
パイプライン処理の例:
時刻t0: 命令1[フェッチ]
時刻t1: 命令1[デコード] 命令2[フェッチ]
時刻t2: 命令1[実行] 命令2[デコード] 命令3[フェッチ]
データ依存関係という壁
しかし、ここで重要な概念に出会いました。「データ依存関係」です。
ある命令の実行結果を次の命令で使用する場合、先行命令の実行が完了するまで後続命令は待たされます。これを「真のデータ依存関係」と呼びます。
mul eax, ebx ; eaxとebxを乗算し、結果をeaxに格納
add ecx, eax ; eaxの値(乗算結果)をecxに加算
この場合、add
命令はmul
命令の結果が出るまで実行を開始できません。当時のPentium IIでは、乗算命令のレイテンシ(実行に要するサイクル数)は4サイクルでした。
MMX実装への道のり
深夜、アルバイトが終わった後、許可をもらってソフトウェアハウスのPCを使わせてもらいました。もちろん、SETI@homeは一時停止です(宇宙人には申し訳ないけど)。
// MMXでの実装
void ConvertToGrayscale_MMX(BYTE* pSrc, BYTE* pDst, int width, int height)
{
// 係数を16ビットにスケールアップ
static const __int64 coeff_r = 0x004D004D004D004D; // 77
static const __int64 coeff_g = 0x0096009600960096; // 150
static const __int64 coeff_b = 0x001D001D001D001D; // 29
int pixels = width * height;
int mmx_pixels = pixels & ~3; // 4ピクセル単位
_asm {
mov esi, pSrc
mov edi, pDst
mov ecx, mmx_pixels
shr ecx, 2 // 4で割る
movq mm5, coeff_r
movq mm6, coeff_g
movq mm7, coeff_b
process_loop:
// 4ピクセル分のRGBを読み込み
movd mm0, [esi] // R1,G1,B1,R2
movd mm1, [esi+4] // G2,B2,R3,G3
movd mm2, [esi+8] // B3,R4,G4,B4
// ここでパイプラインストールが発生!
// メモリからの読み込みは4-5サイクルかかる
punpcklbw mm0, mm0 // バイトをワードに展開
punpcklbw mm1, mm1
punpcklbw mm2, mm2
// さらにデータ依存関係でストール...
}
}
Visual C++ 6.05のインラインアセンブラが、MMX命令を正しく認識してくれず、結局MASMを使って別ファイルにアセンブラコードを書き、リンクする羽目になりました。
技術的挫折と学び
3日間の格闘の末、ついにMMX版が完成しました。期待に胸を膨らませてベンチマークを実行すると...
[ベンチマーク結果]
元のコード: 3.2秒
整数演算版: 1.0秒
MMX版: 0.8秒
たった20%の改善。理論値では4倍速くなるはずだったのに。
落ち込む私に、先輩エンジニアが声をかけてくれました。
「メモリ帯域がボトルネックになってるんだよ」
当時のメモリ(PC100 SDRAM6)の転送速度は800MB/s。640×480×3バイトの画像を読み書きすると、それだけで1.8MBのデータ転送が必要です。CPUがいくら速くても、データが供給されなければ意味がない。
パイプラインの真実
後に勉強してわかったことですが、MMX命令を使っても速度が出なかった理由は他にもありました。
-
パイプラインハザード、MMX命令間にもデータ依存関係があり、先行命令の結果を待つ間、パイプラインに「バブル」(空きサイクル)が生じていた
-
メモリアクセスのレイテンシ、当時のCPUでは、メモリからのロード命令は4-5サイクルかかり、この間後続の命令は待たされる
-
レジスタ不足、MMXレジスタは8個しかなく、複雑な処理では頻繁にメモリとの間でデータをやり取りする必要があった
-
命令実行の並列性の限界、Pentium IIは限定的なアウトオブオーダー実行機能を持っていたが、まだ十分ではなかった
この経験から、私は重要な教訓を学びました。CPUの性能は、単純な演算能力だけでは決まらないのです。メモリアクセス、パイプライン、データ依存関係、すべてが絡み合って全体の性能が決まります。
コードの向こうにいる人
最適化の仕事を通じて学んだ最も重要なことは、技術的なことではありませんでした。
社員の一人が、私の最適化したプログラムを使って言いました。
「おかげで残業が減ったよ。今日は早く帰れる」
たった0.2秒の短縮。でも、それが毎日100回実行されれば、20秒。年間で1時間以上の時間を生み出したことになります。
1999年夏の思い出
SETI@homeへの熱狂
オフィスで見たSETI@home7は、1999年5月に始まったばかりの分散コンピューティングプロジェクトでした。世界中のPCの余剰計算能力を使って、地球外知的生命体からの信号を探すという壮大な試みです。
「もしかしたら、俺のPCが最初に宇宙人を見つけるかもしれない」
この「世界規模のプロジェクトに参加している」という感覚が、インターネット時代の新しい連帯感を生み出していたのです。オフィスのPCの99%の時間は、キーボードの入力を待っているだけ。その余剰能力を宇宙人探しに使うという発想は、後のクラウドコンピューティング8の萌芽だったのかもしれません。
家に帰ればブリタニア
アルバイトから帰宅すると、私にはもう一つの世界が待っていました。
[自宅のPC環境 - TWOTOPで購入]
マザーボード: Intel 440BX チップセット搭載
CPU: Pentium II 400MHz
メモリ: 64MB SDRAM
OS: Windows 98
ビデオカード: Canopus SPECTRA 5400 (TNT2)
サウンド: Sound Blaster Live! Value
モニター: 三菱 RDF171S(17インチCRT)
ゲーム: Ultima Online - The Second Age
起動すると、あの懐かしいMIDI音源による「Stones」9のメロディが流れてきます。この曲を聴くだけで、心がブリタニアへと飛んでいきました。
1999年のUltima Online10、特にT2A(The Second Age)時代のYamatoシャードは、まさに弱肉強食の世界でした。UOR(Renaissance)11アップデート前のこの時期、ブリテイン東の森は新人狩りのPKで溢れかえっていました。
「Corp Por」
画面に赤い文字が現れた瞬間、私のキャラクターは倒れていました。またPK(Player Killer)にやられた。
でも、それがUOの魅力でもありました。リアルな緊張感、失うものがある恐怖、そして時々訪れる親切なプレイヤーとの出会い。
ブリ銀行前の人混みでは、Pentium II 400MHzでもカクカクすることがありました。でも、それも含めてUOの思い出です。
渋谷の夜景とコンパイル時間
深夜、ソフトウェアハウスに残ってコンパイルを待つ間、窓から渋谷の夜景が見えました。センター街のネオンが煌めき、まだ営業中のカラオケ店からは浜崎あゆみの歌声が漏れ聞こえてきます。
Visual C++ 6.0のプログレスバーを眺めながら、ふと思いました。
「あの光の一つ一つに、それぞれの青春があるんだな」
私の青春は、MMX命令とメモリ最適化、そしてブリタニアでの冒険に費やされていました。それは、109で服を選ぶのとは違う形の、でも確かな青春でした。
父への報告
夏が終わる頃、父に報告しました。テレビからは「Boys & Girls」のPVが流れています。
「プログラムを20%速くできた」
父は興味なさそうにビールを飲みながら言いました。
「それで給料は20%上がったのか?」
「...いえ、時給1000円のままです」
「じゃあ意味ないな」
でも、私は知っていました。この夏に得た経験は、時給では測れない価値があることを。学生プログラマーとしては、悪くない時給でした。
今すぐ始められる最適化思考
個人レベルでできること
まず測定する
最適化の前に、必ず現状を測定しましょう。Windows 98時代はGetTickCount()
でしたが、今なら高精度タイマーが使えます。
ボトルネックを見極める
CPUなのか、メモリなのか、それともI/Oなのか。私のMMX最適化の失敗は、この見極めができていなかったからです。
CPUの仕組みを理解する
現代のCPUは、以下のような高度な機能を持っています。
- スーパースカラ実行、複数の命令を同時に実行
- アウトオブオーダー実行、データ依存関係のない命令を前倒しで実行
- 分岐予測、条件分岐の結果を予測して投機的に実行
- 多段パイプライン、命令の処理を複数のステージに分割
これらを理解することで、より効果的な最適化が可能になります。
「なぜ」を大切にする
なぜこの処理が必要なのか。なぜこのアルゴリズムなのか。学生だからこそ、素直に「なぜ」を問い続けられるのです。
おわりに、あの夏の価値
2020年3月、SETI@homeは21年間の活動を終了しました12。残念ながら、宇宙人からの信号は見つかりませんでした。
Ultima Onlineは今も続いていますが、もうT2A時代のような殺伐とした世界ではありません。トラメルの実装により、PKのいない安全な世界が用意されました。
同じ頃、かつてのギャルたちも、それぞれの人生を歩んでいることでしょう。厚底ブーツはスニーカーに変わり、ガラケーはスマートフォンになりました。浜崎あゆみは今も歌い続けていますが、もうパラパラを踊る人はいません。
でも、あの夏、たった20%の最適化に3日間を費やした経験は、確かに私の中に残っています。
今、私のデスクトップには当時の100倍以上の性能を持つCPUが搭載されています。最新のCPUは、何十段ものパイプラインを持ち、複雑な分岐予測器を搭載し、巨大なアウトオブオーダーウィンドウで命令を並べ替えています。でも、あの頃のような「1命令でも速く」という情熱は、薄れてしまったかもしれません。
もしタイムマシンがあったら、1999年の自分に言いたい。
「その20%の最適化は、確かに意味があったよ。でも、次はメモリアクセスパターンとデータ依存関係も考えてね」と。
「それと、ブリ東の森に行くときは、必ずリコールをプレキャストしながらルーンを持っていけ」とも。
あの頃の渋谷は、ギャル文化とテクノロジー文化、そしてオンラインゲーム文化が不思議に共存していた、特別な場所でした。
「Stones」のメロディが、今でも心の中で響いています。
-
egg - 1995年創刊のギャル向けファッション雑誌。90年代後半のギャル文化を牽引 ↩
-
men's egg - 男性向けギャル系ファッション誌。ひろむ、けんう、ヨッシーなどのカリスマを輩出 ↩
-
Life with UNIX - Don LibesとSandy Resslerによる1989年出版のUNIX解説書。UNIX文化やコマンド、システムの内部構造を詳しく解説した名著 ↩
-
Windows GDI - Graphics Device Interface。Windows標準の描画API。汎用性は高いが速度は遅い ↩
-
Visual C++ 6.0 - 1998年リリース。MMXサポートは不完全で、多くの開発者を悩ませた ↩
-
PC100 SDRAM - 100MHz動作のSDRAM。理論帯域幅800MB/s ↩
-
SETI@home - 1999年5月開始の分散コンピューティングプロジェクト。アレシボ天文台の電波望遠鏡データを解析 ↩
-
クラウドコンピューティング - 分散処理の概念が、後にクラウドへと発展 ↩
-
Stones - Ultima Onlineのメインテーマ。作曲はDavid Watson。今でも多くのプレイヤーの心に残る名曲 ↩
-
Ultima Online - 1997年リリースの世界初の大規模MMORPG。Richard Garriottが創造したブリタニアの世界 ↩
-
UOR (Renaissance) - 2000年5月実装。PvPエリアとPvEエリアを分離し、初心者保護を強化したアップデート ↩
-
SETI@home終了 - 2020年3月31日、計算資源をより効率的な手法に振り向けるため休止 ↩