Git の diff の中身:Myers アルゴリズム入門

約8分

git diffdiff コマンドが返す「2 つのファイルの差分」は、見た目はシンプルですが、内部では「最も少ない編集操作で A から B にする手順」を計算する非自明なアルゴリズムが動いています。本記事では事実上の標準である Myers アルゴリズム(1986)と、その派生について整理します。

問題設定:最短編集距離

2 つの文字列(または行の列)A と B が与えられたとき:

  • A から B に変換するために必要な「挿入」と「削除」の合計回数の最小値を求めたい
  • その最小操作の具体的な内容(どの行を消し、どの行を足すか)も知りたい

これは 最短編集スクリプト(SES:Shortest Edit Script) と呼ばれ、長さ N の文字列 A、長さ M の文字列 B に対して D = N + M − 2L(L は最長共通部分列の長さ)の操作で達成できます。

つまり「最短 SES を求めること」と「最長共通部分列(LCS)を求めること」は等価な問題です。

ナイーブ解:DP で O(NM)

最も素直な解は動的計画法。LCS のサイズを 2 次元 DP テーブルで埋める方式で、時間・空間ともに O(NM)。

       B[0] B[1] B[2] ...
A[0]    .    .    .
A[1]    .    .    .
A[2]    .    .    .

各セル (i, j) に「A[0..i] と B[0..j] の LCS の長さ」を入れていく。

問題点:

  • N = M = 1000 なら 100 万セル → 余裕
  • N = M = 100,000(大きいファイル)なら 10^10 → メモリ・時間ともに非現実的

実用ファイルサイズで動かすには、より効率的なアルゴリズムが必要でした。

Myers アルゴリズム:O((N+M)D)

Eugene Myers が 1986 年の論文「An O(ND) Difference Algorithm and Its Variations」で発表した方式で、差分の大きさ D に依存する計算量 O((N+M)D) が革新的でした。実用ではほとんどのファイル比較で D が小さいので、結果として非常に高速です。

編集グラフによる定式化

A と B を 2 次元グリッド上に展開して考えます:

  • 横軸:A の文字(左から右に進む = A から削除)
  • 縦軸:B の文字(上から下に進む = B から挿入)
  • 対角線:A[i] == B[j] のとき斜めに進める(一致=コピー)

目標:左上 (0, 0) から右下 (N, M) に、斜め移動はゼロコスト、横・縦移動はコスト 1 として、最小コストで到達する経路を求める。

        A: A B C
B:      .─.─.─.
A   .   │ ↘     ←  右下に進むほど A を消費
B   .     ↘
C   .       ↘   ←  斜めは「一致してコピー」

これは 0-1 BFS(または bidirectional BFS) で解けます。Myers の論文ではさらに「中点を見つけて再帰分割する」最適化を加え、空間計算量を O(N+M) に抑えています。

D-path:D 回の編集で到達できる地点群

Myers の核心アイデアは「D 回の編集(横+縦移動)で到達できる地点群」を D = 0, 1, 2, … と順に広げていくこと。

  • D = 0:対角線だけで到達できる範囲(A と B が完全一致なら一発で右下)
  • D = 1:1 回の挿入または削除で到達できる範囲
  • D = 2:2 回の編集で到達できる範囲
  • … 最終的に右下 (N, M) を含む D が見つかれば終了

D は SES の長さに直結するので、差分が小さければ早期に終了する性質を持ちます。

なぜ Git は素の Myers を使わないのか

Git の diff は実は素の Myers ではなく、派生アルゴリズムを選択肢として持っています。

アルゴリズム特徴
myers(デフォルト)標準 Myers。最短 SES を保証
minimalMyers + より最小化を試す。やや遅い
patience「ユニークな行をアンカーにする」発想。リファクタリングで読みやすい diff
histogrampatience 改良版。Git 2.7 以降で利用可能。重複ヒストグラムでアンカーを見つける

最短 SES と「人間にとって読みやすい diff」は必ずしも一致しません。例えば関数の入れ替えが起きた場合、Myers は「全体を消して挿入し直す」と等価な手順を返すことがあります。これに対し patience は「ユニークな共通行(関数定義など)をアンカーにする」ため、より構造的な diff を返します。

git config diff.algorithm patience で切り替えられます。リファクタリング多めのプロジェクトでは patience または histogram にすると体感が良くなります。

ハンク表示:差分の文脈表示

diff -u で得られる「ユニファイド形式」は、Myers が出した編集スクリプトを ハンク(hunk) にまとめて表示します。

@@ -10,7 +10,9 @@
 unchanged line
 unchanged line
-removed line
+added line A
+added line B
 unchanged line

@@ -10,7 +10,9 @@ の意味:A の 10 行目から 7 行、B の 10 行目から 9 行を表示。前後 3 行(デフォルト)の変更されない行も含めて表示することで、文脈が分かるようになっています。

ハンクは「変更行の連続」を 1 つの単位にまとめたもので、変更が離れていれば複数のハンクに分かれます。git diff -U10 のように -U オプションで前後の文脈行数を増やせます。

行単位 vs 文字単位

git diff はデフォルトで行単位の diff です。文字単位の差分が見たいときは:

  • git diff --word-diff — 単語単位(空白区切り)
  • git diff --color-words — 単語単位 + 色付き
  • git diff --word-diff-regex='.' — 文字単位

文字単位は計算量が増えるので、長いファイルでは遅くなります。実用的には行単位 + 単語単位の併用がバランスが良いです。

実装で遭遇する罠

1. 改行コードの扱い

CRLF と LF が混在すると、すべての行が「変更された」と認識されます。.gitattributes で改行コードを正規化するか、ローカルの core.autocrlf を設定して防ぎます。

2. 末尾改行の有無

ファイル末尾に改行がない場合、diff は \ No newline at end of file というメタ情報を出力します。エディタや CI で意図せず末尾改行が消えると、無関係な diff が大量に発生します。

3. インデント変更だけの巨大 diff

タブ → スペース変換のように見た目が変わらない変更が、Myers では「全行削除+全行挿入」と判定されてしまいます。git diff -w(空白を無視)で前後比較すれば、本質的な変更だけ確認できます。

まとめ

  • diff の本質は「最短編集スクリプトを求める = 最長共通部分列を求める」問題
  • 1986 年の Myers アルゴリズムは差分サイズ D に依存する O((N+M)D) で実用化を達成
  • 「読みやすさ」を狙った派生として patience / histogram があり、Git は切り替え可能
  • ユニファイド形式のハンクは Myers の出力を文脈付きで人間向けに整形したもの

実際に 2 つのテキストの差分を視覚的に確認したい場面では、本サイトの diff チェッカーで貼り付けるのが一番早いです。Myers アルゴリズム相当の挙動でハイライト表示します。