git diff 为什么这么快?Myers 算法原理

· 约 3 分钟 🔀 文本对比

git diff 对比一个 10000 行的源文件改动,几毫秒就出结果。背后是 Eugene Myers 1986 年发表的 diff 算法——时间复杂度 O((n+m)·d),d 是差异行数。几乎所有现代 diff 工具(git、VS Code、GitHub、Beyond Compare)底层都是它。

先看朴素思路:LCS

对比两段文本最直接的思路:找最长公共子序列(LCS)。LCS 里的行就是没变的,其余就是 +/- 的差异。

动态规划解 LCS:构建一个 (n+1) × (m+1) 的二维表,时间和空间都是 O(n·m)

问题

  • 两个 10000 行文件:1 亿次运算,内存 800 MB
  • 实际中 绝大多数行都没变(d 很小),这种浪费就不合理了

Myers 的核心思路:在编辑图上找最短路径

把比较两个序列变成图搜索

  1. 构造一个 (n+1) × (m+1) 的网格
  2. 从 (0,0) 走到 (n,m)
  3. 每一步有三种走法:
    • 向右(→):删除 A 的一行
    • 向下(↓):插入 B 的一行
    • 对角线(↘,仅当 A[i]==B[j]):两行相同,“免费”走
  4. 目标:找到走法里 → 和 ↓ 总次数最少的路径,这个次数就是编辑距离 d

关键洞察:如果 d 很小,根本不需要遍历整个网格——从 (0,0) 做广度优先搜索,只需要探索 d 层

复杂度为什么是 O((n+m)·d)

  • 每一层扩展新状态,状态数和 d 成线性
  • 每次扩展沿对角线”滑到不能滑为止”
  • d = 0(完全相同)时,算法瞬间返回
  • d 小(改动少)时,算法几乎不工作

这就是为什么 git diff 大多数时候很快——代码仓库里的改动总是小改动

还有什么在用 Myers

  • git / mercurial / svn 的 diff 引擎
  • GNU diff 命令
  • VS Code 的行级对比
  • Beyond Compare、WinMerge 的底层
  • DiffMatchPatch 库(Google 开源)
  • LLM 的输出 diff(比如代码修改建议的展示)

局限性:行粒度太粗

Myers 算法本身是按行比较的。如果一行改了一个字符,整行被标记”删 + 加”:

- const x = 100;
+ const x = 200;

要在字符级别高亮”100→200”,需要再跑一次 Myers(或 patience diff、histogram diff),对这两行做字符级对比。这就是 GitHub 上”行内 highlight”背后的技术。

改进版本:Patience diff

2008 年提出,git 的 --patience 选项。适用于 Myers 结果”不符合直觉”的情况,比如大段相似代码里的小改动:

  • Myers 可能把一对公共空行匹配成”锚点”
  • 导致 diff 输出里两块真正相关的代码被拆散到不相邻位置

Patience 先找出唯一共同行作为锚点,在锚点之间递归应用 Myers。生成的 diff 往往更接近程序员的直觉。

实际对比测试

  • 相同文件:Myers < 1ms
  • 每行都改了(最坏情况 d = n+m):退化到 O((n+m)²),10000 行需要 1 秒
  • 改动 100 行:几毫秒,和文件大小几乎无关

所以对比历史版本(改动少)很快,对比无关的两个文件反而慢——这是算法本身的特性决定的。

实时对比

粘贴两段文本,逐行 / 逐字符差异高亮、同步滚动、可选忽略空白/大小写——底层就是 Myers 算法的高效实现。

❓ 常见问题

git diff 对比大文件为什么这么快?

因为 Myers 算法复杂度是 `O((n+m)·d)`,d 是差异行数。git 仓库里绝大多数提交只改几十行,d 很小,所以 10 万行文件的 diff 依然毫秒出结果。只有"完全不同的两个文件"才会退化到 O(n²),这在代码仓库里极少发生。

行级 diff 和字符级 diff 有什么区别?

行级 diff(git diff 默认)把改动的一行标记为"删一行 + 加一行",速度快;字符级 diff 在这两行内部再跑一次 Myers,高亮出哪些字符改了。GitHub、VS Code 显示的行内高亮就是字符级 diff,更精确但计算量大几倍。

为什么有时候 diff 结果让人难以理解?

Myers 算法优化的是"编辑操作最少",不是"可读性最好"。当代码里有相同空行公共的括号行时,算法可能把它们选作锚点,导致 diff 把不相关的代码拆散。解决:用 `git diff --patience` 或 `--histogram`,它们会优先选"独一无二的行"作为锚点,输出更接近人眼直觉。

diff 能处理中文吗?

行级 diff 完全没问题——它按行比较,不管行内是什么字符。字符级 diff 也能处理,但要注意按 Unicode 字符拆分,不能按字节——UTF-8 里一个汉字占 3 字节,按字节拆会得到乱码。现代 diff 库(diff.js、google-diff-match-patch)都正确处理 Unicode。

🔀 打开 文本对比 逐行/逐词差异 · 分栏高亮