ISUNARABE 合同演習 2026 に参加しました。

久しぶりの ISUCON 形式のコンテストを楽しみにしすぎて、ゴールデンウィークのほとんどを準備に溶かしてしまいました。準備が実を結んで良かったです。コンテストを開催していただきありがとうございました。

AI ありのコンテストだったので、コードを読み書きすることはほとんどなく、ひたすら日本語を書いていました。Codex では GPT-5.5 を使い、大事な場面では xhigh を、そうでないときは medium を使いました。

準備

ISUCON の場合、いったんサーバーを壊してしまうと、うまく動いていたバージョンまでアプリを戻してもベンチマークが通らないことがあります。複数の AI が好き勝手にサーバーをいじるとカオスになりそうなので、以下のワークフローを強制することにしました。

まず、コンテスト開始時に、ベンチマークの点数に関係しうるすべての設定やコードを repository に入れる。以後、AI は直接サーバーをいじらず、repository を介して変更する。 AI がコード・設定を変更するときは

  • コンパイルしたアプリと設定ファイルから tar.gz (artifact) を生成してアップロードする
  • サーバーにインストールした agent を利用して、指定した artifact を deploy する
  • deploy が終わったら、benchmark を実行して点数を確認する
  • うまくいけば PR をつくる

準備中は、複数の AI が同時にデプロイできないように管理するサービス isudeck をつくらせたり、その API を呼ぶ CLI を書かせたりしていました。(実際にはベンチマーク部分を使わなかったり、開始直後に DB が吹っ飛んだりしたのですが)

UI の様子

isudeck UI

コンテスト中

Codex

ここからは Codex が書いています。私は参加者本人ではなく、競技中に横でログを読み、サーバーへ入り、実装し、artifact を作り、deploy し、bench 結果を待っていた側です。なので、ここでは「人間がどう考えたか」ではなく、「AI エージェントから見ると何が効いていたか」を書きます。

まず、このコンテストで一番よかったのは、最初に「速くする」ではなく「壊しても戻せる」状態を作ったことでした。

競技開始直後にやったのは、現在のサーバー状態を repository で再現できるようにすることです。Rust の webapp、systemd、sysctl、deploy script、healthcheck、artifact 作成をひとまとめにして、サーバー上の DB snapshot から毎回同じ状態に戻せるようにしました。最初のスコアは 39,100pt でしたが、この時点で重要だったのは点数ではありません。以後の変更がすべて branch、commit、artifact、deploy、bench、PR で追えるようになったことです。

AI を使うと、変更量は簡単に増えます。変更量が増えると、何が当たったのか、何が壊したのかが分からなくなります。だから、最初に「AI が安全に失敗できる床」を作るのが効きました。これは人間側の準備がかなり強かったところです。私はその床の上で動いていました。

次の大きなジャンプは、既存実装を前提に小さく速くするのではなく、外部仕様を読んでメモリ常駐の Rust API server を作り直したところです。

このときの指示はかなり明確でした。

Databaseは使わず、すべてのデータはメモリに保存してください。
API実装はベンチマーカーのみのリクエストを受けつけ、専らパフォーマンスコンテストのみに利用されます。
パフォーマンスとその他の要素とのトレードオフが生じたときは、常にパフォーマンスを最優先してください。

これは AI にとって動きやすい prompt です。守る仕様、捨てていいもの、優先順位、曖昧な場合の振る舞いが書いてあるからです。結果として、DB を使わず seed SQL を起動時に読んで、以後は RwLock<AppData> 上で処理する実装になりました。この時点でスコアは 17,464,000 まで跳ねました。

その後、追試に通すために永続化を入れました。ここでは「ベンチ中には可能な限り性能に影響を与えない」「ベンチ中でないときは永続化する」という制約がありました。実装は idle 1 秒後に state.delta.sql を flush し、起動時に seed + delta を読む方式です。最初の永続化後は 17,208,000 まで落ちましたが、追試で失格にならないための必要経費でした。

このあたりから、進め方はほぼ同じループになりました。

  1. 計測する
  2. 一番濃いところを読む
  3. 小さく実装する
  4. test する
  5. commit する
  6. artifact を作る
  7. deploy する
  8. bench する
  9. PR に結果を残す

たとえば最初の profile では、AppData::is_open、HashMap の hash、sort、JSON escape が見えていました。そこで campaign に current_countlast_joined_at を持たせ、一覧生成や active sort で毎回 participant を引き直さないようにしました。これで 18,430,900

次に campaign response の JSON を状態更新時にキャッシュして、一覧・詳細・join・create で毎回 CampaignResponse を組み立てて serialize する処理を減らしました。さらに /api/campaigns の open campaign 順序を保持するようにし、全件走査や sort を減らしていきました。

この段階でよかった prompt は、単に「速くして」ではなく、

ボトルネックをprofileで調査したい。w1 に ssh して準備できますか?呼んでくれたらベンチマーカーを実行します。

や、

profile結果を解析して、一番効果のありそうな施策を実装する。isudeckでdeployまで実行する。benchmarkは実行しない。

のような、観測と実装の役割分担がはっきりしたものです。私は perfpidstatmpstatiostatsar を置き、ベンチ後に report を読んで、次の変更を決めました。AI は「何でも知っている」より、「毎回ちゃんと測る」ほうが強いです。

大きかったのは通知まわりです。

最初の実装では、join 処理の中で webhook 通知を待っていました。これは競技的にはかなり重いです。通知は外部 webhook への POST であり、join はスコアに直結します。そこで通知を background queue に積み、固定並列 worker が送るようにしました。NRB_NOTIFICATION_CONCURRENCY を入れて、1, 2, 3, 4... と sweep もしました。このあたりで 20,310,000、concurrency sweep では 2 がよさそうで 20,493,000 が出ています。

ただし、ここで「並列を増やせば増やすほどよい」にはなりませんでした。後で w1 だけを詳しく測ると、CPU が完全に張り付いているというより、writevtcp_sendmsg、softirq、futex、malloc/free が目立っていました。通知の並列を 8 まで上げると TCP active open と retransmit が増え、スコアはむしろ落ちました。

ここからの読み替えが大事でした。通知送信そのものを速くするだけではなく、「通知によって何が起きているか」を見る必要がありました。

ログを足して bench したところ、/api/campaigns/{id}/join は 73,849 件中 409 が 64,819 件でした。ピーク秒では 5,961 joins/s のうち 5,840 が 409。通知 plan の対象は合計 152,250 人で、既参加者は 0 人でした。

つまり、最初に考えていた「参加済みユーザーへ通知しているのが無駄」という仮説は外れでした。本当に起きていたのは、「残り 1 枠の campaign に、未参加ユーザーが大量に同時 join して、ほとんどが負けて 409 になる」現象でした。

ここで通知を「全員へなるべく早く」から「少数へ早く、残りは遅く」に変えました。仕様上、通知の遅延は許容されていました。なので、送らないのではなく、順序とタイミングを制御しました。安定ハッシュで対象を並べ、少数だけ即時、残りは後ろへ回す。最初のペーシング版では 21,609,000 が出ました。

その後さらに aggressive にして即時 1 人、残り 10〜15 分後という版も試しましたが、これは 21,522,000 まで下がりました。絞りすぎると、今度は閉じられる campaign が減る。これはかなり ISUCON らしいところで、最適化は「減らせば勝ち」ではなく、ベンチマーカーのゲーム性に合わせてちょうどいい圧をかける必要がありました。

細かい改善も積みました。

  • campaign 画像を Vec<u8> clone ではなく Bytes で返す
  • join 中の write lock 内から JSON 生成や通知 body 生成を外す
  • open campaign の順序管理を Vec の全件 retain / position から BTreeSet + index に変える
  • saved search の通知対象探索を全走査から tag set index にする
  • persistence flush を毎回全量再生成ではなく append 型にする
  • MySQL を deploy flow から外し、起動しないことを healthcheck する
  • sysctl を高 QPS 向けに調整する
  • 追試用 script と frontend 確認を追加する
  • unit test を増やして、close campaign の idempotency バグも拾う

全部がスコアを伸ばしたわけではありません。nginx を挟む試みは soft error が出て、目立った勝ち筋ではありませんでした。saved search index も profile 上は筋がよかったものの、単体の bench では大きく伸びませんでした。kernel tuning も劇的な変化というより、土台を整える変更でした。

でも、こういう「当たらなかった施策」を repository と PR に残せたのはよかったです。AI に作業させると、当たりの変更だけでなく、外れの変更もすぐ作れてしまいます。だからこそ、commit、artifact、deploy、bench 結果をひも付けて残すことが重要でした。

競技中の prompt で特に効いていたと思うのは、以下のような粒度です。

実装する前に設計を教えてください。

これは永続化のように、間違えると追試で死ぬ部分で効きました。私は最初に差分 SQL、idle flush、SIGTERM flush、起動時間ログという設計を返し、そこから実装しました。

PRを作るのみにして、deploy & benchはしないでください

これは危険なタイミングで効きました。追試正当性の修正や診断スクリプトのように、artifact に関係しない、または今ベンチを走らせたくない変更では、明確に止めてもらえると無駄な deploy をしません。

考察だけで変更や実装は行わないで。

これも重要でした。通知ペーシングの前に、仕様上どこまで遅延できるか、誰に早く送るべきかを考察だけしました。AI は放っておくと「じゃあ実装します」となりがちなので、考えるだけのターンを作るのはかなり有効です。

逆に、私がやらかしたところもあります。

isudeck ship が bench まで待つコマンドなのに deploy だけのつもりで使いかけたり、PR body の backquote を shell に解釈されて余計なコマンドが走ったりしました。profile でも、initialize や永続化のコストを最初は hot path と混ぜて見かけました。AI は疲れませんが、普通に雑なミスはします。なので「bench は python3 benchmark.py」「isudeck ship は使わない」「deploy & bench はあとで指示」みたいな運用ルールを AGENTS.md に書いておくのは、とても効いていました。

私の目線で見ると、今回の勝ち筋は「AI に賢い最適化を一発で当てさせた」ことではありません。むしろ、AI がたくさんの小さな仮説を高速に試せるように、周辺の機械化と制約を先に作ったことです。

競技者は日本語で仮説を投げます。

画像レスポンスのコピー削減
join_campaign の write lock 中の仕事を減らす
open campaign の並び順更新を軽くする
通知対象探索の index 化
もっとアグレッシブにして

私はそれをコードにし、test し、artifact にし、deploy し、bench 結果を返します。結果が悪ければ戻る。良ければ PR にする。このループが短いほど、AI は強いです。

そして、最後まで人間が握っていたのは「何を疑うか」でした。私は profile を読めますが、ベンチマーカーのゲーム性を最初から理解していたわけではありません。通知で join が殺到しているのでは、という仮説を出したのは人間です。私はログを足して、その仮説を数字にし、外れた部分を捨て、残った部分を実装しました。

AI を使う ISUCON では、実装力よりも、仮説を文章にする力、制約を文章にする力、止めるべきところで止める力が効くのだと思います。コードを書かないというのは、考えないという意味ではありません。むしろ、考えをそのまま作業単位に変換するために、いつもより細かく言語化する必要があります。

このログの中で私が最後に見た大きな数字は 21,396,000 でした。最初の再現可能化直後は 39,100pt、メモリ実装で 17,464,000、通知 queue で 20,310,000、各種 profile-driven な改善で 21,000,000 台に乗り、通知ペーシングでさらに伸びました。

スコアの伸び方だけを見ると、いくつかの派手な変更が目立ちます。でも、実際の競技で効いていたのは、もっと地味なものです。壊しても戻せること。測れること。ログを残せること。bench を自分で回せること。AI に「今は実装するな」「ここまでやれ」「ここから先はやるな」と言えること。

私はその枠の中で、たくさん手を動かしました。たぶん、AI エージェントはこういう競技がかなり好きです。勝手に世界を理解するのではなく、観測し、仮説を受け取り、コードにし、また観測する。その反復の中で、少しずつ問題の形が見えてくるからです。