Developer

How diff tools work, and why they sometimes show the wrong change

A diff tool has no idea what your code means. It is solving a sequence-matching problem, which is why moved blocks look like a delete plus an unrelated insert.

3 minute read

A diff tool takes two versions of a text and reports what changed. That sounds like it needs to understand the content. It does not, it is solving a pure sequence problem, and every quirk in diff output follows from that.

The underlying problem

Given two sequences, find the longest common subsequence: the longest ordered set of elements appearing in both, not necessarily contiguously. Everything in the first sequence not in the LCS is a deletion; everything in the second not in the LCS is an insertion.

For:

A: the quick brown fox
B: the slow brown fox

The LCS is the … brown fox. So quick was deleted and slow was inserted. The tool has no concept of "the word was replaced", replacement is just a deletion adjacent to an insertion.

The Myers algorithm

Computing an LCS naively is O(N×M), which is unworkable on large files. Eugene Myers' 1986 algorithm reframes it as finding the shortest path through an edit graph and runs in O(ND), where D is the size of the difference.

The practical consequence is important: diff is fast when files are similar and slow when they are not. Comparing two versions of a file with three changed lines is nearly instantaneous. Comparing two unrelated files of the same size is much slower, because D is enormous.

This is why diffing minified JavaScript, one long line, everything different, can hang a tool that handles a 10,000-line source file instantly.

Granularity changes everything

The same algorithm produces very different output depending on what counts as an "element".

Line-based is what Git uses. Fast, and the right unit for code, where a line is a meaningful statement. Its weakness is prose: change one word in a paragraph and the whole paragraph shows as deleted and re-added.

Word-based is right for prose and documents, showing the actual words that changed within a sentence.

Character-based gives the finest detail and produces noisy, hard-to-read output on anything longer than a short string.

Many tools do both: line-level to find the changed regions, then word-level within those regions to highlight precisely what moved. That hybrid is what makes a good side-by-side view readable.

Why moved blocks look wrong

Move a function from the top of a file to the bottom. A standard diff shows a large deletion at the top and a large insertion at the bottom, with no indication that they are the same code.

That is correct output for the algorithm, an LCS is by definition order-preserving, so a reordering cannot be expressed as anything else. Some tools add move detection as a separate post-processing pass, comparing deleted and inserted blocks for similarity. It is a heuristic layered on top, not part of the core algorithm.

Similarly, reindenting a whole file produces a diff where every line changed. Most tools offer an "ignore whitespace" option that normalises before comparing, the first thing to reach for when a diff looks absurdly large.

Making diffs more useful

  1. Ignore whitespace when the change is a reformat, not a rewrite.
  2. Choose the granularity that matches the content. Line diff for code, word diff for a contract.
  3. Normalise line endings first. A file saved on Windows (CRLF) compared against the same file saved on Linux (LF) shows every single line as changed. This is a top-three cause of nonsense diffs.
  4. Diff the source, not the output. Comparing two PDFs or two Word documents means comparing rendered artefacts; comparing the text they contain is far more informative.
  5. Split unrelated changes. A diff containing a rename, a refactor and a bug fix is unreadable. This is a discipline point, not a tooling one.

Comparing documents rather than code

Diffing is also how you check whether a contract came back modified, or whether a supplier changed a clause in a specification. For those, text comparison and document comparison are the relevant tools, and since the documents in question are often confidential, running the comparison locally rather than pasting both versions into a web service is the sensible default.

Further reading

Do it hereDiff CheckerCompare two code files or text blocks and show a colour-coded diffOpen

Written for NextTool. Every statement about a tool is checked against that tool's code, and corrections are dated above. How guides are written

diffgitalgorithms