← ノート

ビルドフラグ一つで20倍速く

2026年8月4日 · Recilic 開発ノート

最初のメッシュが動き出した翌朝、それは 61.9 GB の写真ライブラリを 1.2 MB/s で部屋の向こうへ送っていました。完了まで14時間。朝食の頃には 23.8 MB/s になっていた——そして効いた修正は、少し気恥ずかしいほど地味なものでした。

MacBook と Mac mini の間、23.8 MB/s の Live mesh

容疑者たち

思い当たる仮説はどれもプロトコルの話でした。各シャードは厳密に一つずつ書かれていた——送信し、相手の応答を待ち、次を送る——つまりスループットは帯域ではなく往復遅延に縛られていた。約19,000枚の写真では、ほぼすべてのファイルが単一チャンクで、そのシャードごとに往復を丸ごと払う。どちらも事実で、どちらも直す価値がある。しかしどちらも主役ではありませんでした。

そこへ、一撃で謎を割る問いが来ました:「ちょっと待って、この2台は本当に LAN で話しているの?VPN 経由じゃなくて?」確かめるには生のリンクを測るしかない。というわけで、同じ2台の間で SSH 経由に 100 MB を流す。20 MB/s。ネットワークは問題なし。Recilic は、すでに持っている道の6パーセントしか使っていなかった。

格好のつかない答え

開発ビルドは Debug ビルドでした。そこでホットパスを両構成でベンチマークしました。各段階に 8 MB を通します:

消失訂正符号の符号化(自作 Swift):3.2 MB/s → 142.9 MB/s  (44×)
ChaChaPoly 封印(CryptoKit):958.8 → 958.5 MB/s  (1.0×)
SHA-256(CryptoKit):3260 → 3156 MB/s  (1.0×)
チャンク分割(自作 Swift):1965 → 2080 MB/s  (1.1×)

差はすべて一箇所に集まっていました:GF(256) の符号化器です。暗号は元から遅くない——CryptoKit はシステム内で最適化済みに出荷されるので、ビルド設定では傷つけられません。しかし符号化器は私たちの Swift で、1バイトごとにルックアップテーブル経由の乗算をします。最適化を有効にすると gfMul は緊密なループにインライン化され、Data の参照カウント処理は消え、whole-module optimization がファイルをまたいでテーブルをインライン化します。-Onone では、1バイトごとに本物の関数呼び出しと参照カウントを支払います。

なぜビルドフラグが44倍の価値を持つのか

660億通の封筒にスタンプを押すことを想像してください。符号化器が直面しているのはほぼそれです:61.9 GB は 660 億バイトで、そのすべてを4回掛け算する——合計で数千億回の微小な操作です。操作そのものは些細:表を2回引いて組み合わせるだけ。あなたの午後を決めるのは、各操作を取り囲む事務手続きです。

Debug モードはこうやっています:封筒1通ごとに、備品室まで歩いてスタンプを取り、戻って押し、スタンプを返し、台帳に一行書く。押す作業は一瞬。歩くのが全部です。

Release モードはスタンプを手に持ったまま、台帳を捨てます。

用語に戻すと:備品室への往復が関数呼び出しで、最適化器はそれをループ内へインライン化します;台帳が自動参照カウント(ARC)で、最適化器は不要だと証明して削除します。どちらもプログラムの計算結果は変えません——1バイトごとにどれだけ儀式が付くかだけを変えます。

だから Apple の暗号は無傷なのです:出荷時から封をされ、すでに「スタンプを手に持った」状態。ビルド設定は内側に手を伸ばせません。debug 税を払うのは自分で書いたコードだけ——しかも1バイトごとに払います。

そして数字はぴったり合います:3.2 MB/s の符号化器から I/O とフレーミングのオーバーヘッドを引けば、観測していた 1.2〜2 MB/s。142.9 MB/s の符号化器は 20 MB/s のネットワークを大きく上回る——だからボトルネックは回線へ移りました。本来あるべき場所です。

最適化ありで再ビルド:1.2 → 23.8 MB/s。SSH で測った 20 MB/s を上回っています。14時間かかっていたバックアップが約45分に。ボトルネックは Wi-Fi リンク——本来あるべき場所へ戻りました。

それでも作る価値があった修正

プロトコル側の仮説は間違いではなく、ただ順位を抜かれただけ——そして結局リリースしました。ラインレートに達すると、残った遅延が本当に効いてくるからです:

シャードの並行書き込み。チャンクの6シャードを直列ではなく並列に書きます。ひとつが失敗しても group 全体を drain し、着地したシャードはすべてクリーンアップ対象に登録される——書きかけのバックアップが孤児を残してはいけません。

コネクションプール。ピアごとに4チャネル。並行書き込みの往復が1本のパイプの後ろで並ぶのではなく重なります。

実使用から出た2つの傷。転送中にノートがスリープすると、以前は死んだソケットの上でバックアップが永遠に止まっていた——今は TCP キープアライブとリクエスト単位の期限で1分以内に「ピア利用不可」として現れ、次のバックアップが再接続します。もうひとつ、起動後の最初の接続は macOS がローカルネットワーク権限のキャッシュを再構築する間に一度だけ失敗しうる;今は自動でリトライするので誰も気づきません。

ついでに直したバグ:前提条件が欠けた状態で「Back Up Now」を押すと——何も起きない、しかも無言で。押せそうに見えて沈黙で答えるボタンは、エラーメッセージより悪い。今は何が足りないかを正確に言います。

教訓を書き残す

Debug ビルドで性能問題を診断してはいけない。自分のプロトコルを疑う前に生のリンクを測ること——そして誰かが「そもそも正しいネットワークを使ってる?」という素朴に聞こえる問いを投げたら、測りに行くこと。その問いには20倍の価値がありました。

Recilic 0.1.0 は Mac App Store でファウンディング価格 $9.99。メッシュは無料アップデートとして——もちろんラインレートで——出荷します。