世界を動かす技術を、日本語で。

差分アルゴリズム

2025年10月1日原文(flo.znkr.io)

概要

  • diff はソフトウェアエンジニアにとって不可欠な変更表現手法
  • 既存の diffライブラリ には満足できず、自作Goライブラリを公開
  • 理想的なdiffライブラリの要件と既存ライブラリの課題を解説
  • Myers法patience diff などアルゴリズムの特徴と可読性の工夫
  • 新しいGo用diffライブラリ znkr.io/diff の設計思想とAPI概要

ソフトウェアエンジニアにとってのdiffの重要性

  • diff はファイルのバージョン比較、コードレビュー、テスト結果の違い可視化、パッチ適用など多用途
  • ほとんどのプロジェクトで diff による変更可視化やパッチ適用が必要
  • 既存の フリーdiffライブラリ に満足できず、独自にライブラリを改造・流用するケース多発
  • Go言語 向けに自作diffライブラリを公開(znkr.io/diff
  • diffアルゴリズムの学びと進化を記事で共有、今後もアップデート予定

既存diffライブラリの課題

  • 多くのライブラリは テキスト入力のみ対応、任意シーケンス比較は非対応
  • 出力形式が固定的( unified形式 のみ等)で、柔軟なカスタマイズが困難
  • 可読性や最小化 (minimality)への配慮が不足
  • APIが複雑、パフォーマンスやメモリ効率も課題
  • 主要Go用diffライブラリの比較表を作成し、各特性を評価

diffアルゴリズムの実装と複雑性

  • Myers法 が主流、最小diffを返すが最悪ケースで O(N²) の計算量
  • 小規模・類似入力には高速だが、大規模・非類似入力では性能低下
  • go-internalpatience diff を採用し、 O(N log N) の計算量で高可読性
  • ヒューリスティック による近似解法で速度改善と最小性のトレードオフ
  • histogram diff など他手法も存在、理解と実装は今後の課題

diffの可読性と最小性

  • 同じ最小diffでも 可読性 に大きな差が生じる
  • 実装や 後処理(post-processing) の工夫で可読性を向上可能
  • Michael Haggertyの diff-slider-tools によるヒューリスティックが有効
  • アルゴリズムだけでなく、 実装・後処理 の質が結果に大きく影響

新しいGo用diffライブラリ znkr.io/diff の設計

  • テキスト・任意スライス 両対応

  • unified形式構造化出力 を両立

  • シンプルなAPI 設計

  • 最小または準最小diff の生成

  • 高パフォーマンス拡張性 を両立

  • 三つの動作モード (Default/Fast/Optimal)で用途に応じた最適化を実現

    • Default:バランス重視
    • Fast:速度重視(最小性一部犠牲)
    • Optimal:最小性重視(速度犠牲)
  • API設計 はユーザー視点でデータ構造から逆算

  • Edits (全編集のフラット列挙)と Hunks (連続変更のネスト表現)を用意

    • Edits:変換の全内容を保持
    • Hunks:ユニットテストや差分可視化に最適

まとめ

  • diffライブラリ には入力汎用性・出力柔軟性・APIの単純さ・可読性・パフォーマンスが求められる
  • 既存ライブラリ は全要件を満たさないため、 znkr.io/diff を開発
  • アルゴリズム選択・実装・後処理 の工夫で、可読性と性能の両立を追求
  • 今後も新たなアルゴリズムやヒューリスティックの導入を予定

Hackerたちの意見

ちょっと関連する話だけど、.gitのdiffを表示するのにお気に入りのツールがあるんだ。diff2htmlっていうCLIで、コマンド一発でブラウザにdiffを開けるよ。https://diff2html.xyz/ -- https://github.com/rtfpessoa/diff2html

私はmeldを使ってるよ。(今はスマホだから、どれが優れてるか比べられないけど)

ソースコードのバージョン管理以外で、diffアルゴリズムの重要な実用例って何かある?

  • バックアップと復元 - セキュリティの観点からの整合性チェック - NLP、テキスト内の同じトークンを見つけることとか

17年くらい前に、IPパケットやTCPセグメント、ネットワークイベントのペイロードをdiffするものに出会ったことがある。その時、RSAセキュリティでネットワークフォレンジックとセキュリティ分析の製品に関わっていて、CとC++のミックスで書かれていた。私が関わったプロジェクトの一つでは、ユーザーがパケットやセグメント、ペイロードをdiffできるようにする必要があったんだ。当時は、製品にサードパーティのライブラリを追加するのにすごく慎重だった。もっとその文化について書いたことがあるよ: https://news.ycombinator.com/item?id=39951673 要するに、保守的な文化のせいで、ほとんどのデータ構造やアルゴリズムは社内で実装されていた。パケットやセグメント、ペイロードのdiffアルゴリズムも社内で書いたもので、私が担当した。実装は、最長共通部分列問題に対するシンプルな動的プログラミングの解法に基づいていた。確か、最悪の場合O(mn)の時間とO(min(m, n))の空間で動いていたと思う。mとnは二つのシーケンスの長さね。もっと効率的なアルゴリズムがあるのは知ってたけど、このコードはパフォーマンスが重要じゃなかったから、誰でも理解できるようにシンプルに保つことにした。そうすれば、すぐに学べて、バグが出た時にも修正できるからね。それは次の7年間、製品が新しいものに置き換わるまでうまく機能してくれた。関連する話だけど、昔のソフトウェア開発スタイルが恋しい時もある。問題領域に深く入り込んで、それをマスターして、自分たちで解決策をデザインしていたから。でも、単純にノスタルジックになってるわけじゃないよ。現代の開発が確立されたライブラリに依存していることで、通常ははるかに大きな生産性と信頼性をもたらすことはよくわかってる。とはいえ、基礎から物を作るゆっくりとした、意図的なアプローチには独特の魅力があったと思う。

私たちは建設スケジュールのdiffを取るよ!これって、通常は巨大なガントチャート(400~700ページが普通)になる。

契約の赤入れに使われる、もっと専門的なものがたくさんあるよ。法的な分野でも、ディスカバリーの整理は非常に面倒くさいことがある。ここにはたくさんのdiffベースやdiffに似たソリューションがあるけど、ほとんどは完全に独自のもので、ドキュメントもないんだ。

DOMの変異操作を最小限に抑えること。たとえばReactでね。でもそれだけじゃないよ。

詳細な変更履歴が含まれていない、クソみたいなチップ会社のPDFデータシート。

他にもいろいろあるけど、Kubernetesみたいなインフラストラクチャー・アズ・コードの文脈ではすごく役立つよ。今日、仕事でdiffを使って、2つの異なる'docker history'の出力を比較して、契約者がベースイメージを強化するために行った変更の全体像を探ってみたんだ。

「承認」/「ゴールデンマスター」/「スナップショット」/「特性」テストは非常に役立つことがあるね。これらはほぼ同じアイデアを指しているようだ。テストが初めて成功すると、自動的に出力をファイルとしてキャプチャする。これが「承認された」出力で、コードと一緒にコミットされるか、使用しているテストシステムに保存される。次回テストが実行されると、新しい出力をキャプチャして、承認された出力と自動的に比較する。もし同じならテストは合格、異なればテストは失敗して人間がdiffを調査する必要がある。この技術は多くのデータタイプに対応している:* プレーンテキスト。* UIコンポーネントやレンダリングされたウェブページの画像。これによって、コードの変更や新しいブラウザのバージョンが予期せぬ外観の変化を引き起こさないか確認できる。* 音声処理コードによって作成された音声ファイル。* 他にテストがないコードからの大きなテキストログ。リファクタリングの際に役立つかもしれないし、偶発的な副作用が予期しないdiffとして現れることを期待できる。参照:* https://approvaltests.com/ * https://cucumber.io/blog/podcast/approval-testing/ * https://en.wikipedia.org/wiki/Characterization_test

diffアルゴリズムは驚くほど広く適用できるね。cursesは、2400ボーのターミナル画面をTUIプログラムのメモリイメージのように更新するというタスクを持っていた。(TUIはvi、nethack、メニューシステム、データベースエントリ画面、ntalkなど、何でもあり。)シンプルな解決策は画面全体を再描画することだけど、80x24は2000バイトで、2400ボーでは8秒かかる。ユーザーは、最新のキー入力から8秒も反応がないプログラムは使わないよね。だからcursesはdiffアルゴリズムとターミナル機能データベースを使って、シリアルラインで送信する最小限の更新セットを見つけるんだ。(一部のターミナルには行や文字を挿入または削除するためのエスケープシーケンスがあって、データ送信を大幅に節約できるけど、他のターミナルにはないものもある。)Reactの仮想DOMも同じアイデアだよ。du -k > tmp.duを実行すると、後でdu -k | diff -u tmp.du -を使って、どのディレクトリのサイズが変わったかを確認できる。何かが急速にディスクを埋めていて、それを止める必要があるけど、何かわからないときに非常に役立つ。sha1sum $(find -type f) > good.shasを使えば、同じようにdiffを使って、どのファイルが変更されたかを見ることができる。rsyncは、遅いまたは高価な接続の両端にある2台のコンピュータ間でdiffを計算するためにローリングハッシュを使用して、古いファイルを最新の状態に効率的に更新する。これはcursesのようなもので、ファイルに対して行われ、ターミナル機能に制限されない。rdiffはrsyncアルゴリズムを使用して、ファイルの複数のバージョンを効率的に保存する。これは一般的にソースコードを含む必要はない。重複排除ファイルシステムもこのアプローチを使用できるけど、しばしばそうはしていない。でも、Resticのようなデータバックアップシステムでは一般的だね。ユニットテストでは、ある式の結果が大きなデータ構造であるべきだと言われることが多いが、時にはわずかに異なる大きなデータ構造を生成して失敗することがある。diffアルゴリズムは、テストが失敗した理由を理解するのに非常に価値がある。py.testはこれをデフォルトで行っている。ゲノム解析は、2つのゲノムのバージョンのどの部分が同じで、どの部分が異なるかを見つけることで機能する。Gene MyersによるBLASTは、このための一般的なアルゴリズムだ。これは、種の進化の歴史を理解したり、腫瘍からのDNAにおける癌を引き起こす変異を特定したり、ひいおばあちゃんが夫を裏切った相手を発見したりするなど、非常に多くのことに役立つ。すべてのバイオインフォマティクスは、diffアルゴリズムの現実のユースケースだと言ってもいいかもしれない。これらは、転置や長い繰り返しのような難しい病理学的ケースに対処する必要がある。H.264のようなビデオ圧縮は、「モーション推定」によって大きく機能する。圧縮器は、前のフレームのどの部分が現在のフレームに最も似ているかを特定し、違いだけをエンコードする。これは、ピクセル値が画像を横切って移動する際に、明るくなったり、暗くなったり、ぼやけたり、シャープになったり、回転したりする可能性があるため、1次元のソースコードdiff問題のより難しい一般化だ。さらに、これは2次元でもある。これはある意味でcursesの問題に似ていて、画面画像を更新するために必要な帯域幅を最小限に抑えようとするものだ。

Hacker Newsで議論の続きを見る