ref:783dab7b2eadd5f40234c35613e5c5ad4dd5b838

perf(diff): remove redundant V-table probes from the Myers bisect loop

Profiling the fangorn/hephaestus#74 pull-request diff (fangorn/anvil#367) put ~77% of diff CPU in the Myers middle-snake search, with two constants larger than they need to be: * `vget`/`vput` were not inlined. On that diff `vget` is called 3.6M times; eprof attributed 23.08% of total runtime to it against 12.26% for the `:atomics.get/2` it wraps. `@compile {:inline, ...}` folds the wrapper away. * `forward_sweep`/`reverse_sweep` read both neighbouring diagonals in the `cond` guard and then re-read the winner in the selected branch — three or four V probes where two suffice. Binding them once removes roughly one probe in three from the hottest loop in the module. Neither touches the algorithm. The output is unchanged and the O(ND) bound (Myers 1986 §4b) is unchanged; this is strictly less work per diagonal step. Measured over every text file in the hephaestus#74 three-dot diff (118 files, median of 5 runs): before 66.8 ms slowest single file 48.2 ms after 60.3 ms slowest single file 40.8 ms ~10% overall, ~15% on the hot file. Smaller than eprof suggested, because eprof's per-call instrumentation inflates the apparent cost of small functions — the honest number is the wall-clock one. Also adds MyersOptimalityTest: 900 randomized cases checked against a dynamic-programming LCS reference for both soundness (the script rebuilds both inputs) and optimality (the script is genuinely shortest). The existing tests are hand-written cases and would not catch a bisect that still produces a valid but non-minimal script, which is exactly the failure mode an optimization here can introduce. Verified to pass before and after. Refs fangorn/anvil#367
SHA: 783dab7b2eadd5f40234c35613e5c5ad4dd5b838
Author: CI <ci@fangorn.io>
Date: 2026-07-29 16:28
Parents: 228bb1e
2 files changed +176 -8
Type
lib/ex_git_objectstore/diff/myers.ex +19 −8
@@ -39,6 +39,14 @@
(https://github.com/git/git/blob/master/xdiff/xdiffi.c).
"""
# `vget`/`vput` are one-liners over `:atomics`, called ~3.6M and ~1.1M times
# respectively on a single real pull-request diff. Unlinlined they cost more
# in call overhead than in the atomic access itself: profiling the
# fangorn/hephaestus#74 diff showed `vget` at 23.08% of total runtime against
# 12.26% for the `:atomics.get/2` underneath it. Inlining folds the wrapper
# away (fangorn/anvil#367).
@compile {:inline, vget: 2, vput: 3}
@type edit :: {:eq, term()} | {:ins, term()} | {:del, term()}
@doc """
@@ -268,6 +276,10 @@
defp forward_sweep(a, b, a_lo, b_lo, n, m, v_off, delta, front?, v1, v2, d, k1, k1s, k1e) do
k1_off = v_off + k1
# The general case needs both neighbouring diagonals. Reading them once
# and comparing the bound values costs two V probes; letting the `cond`
# test them and then re-read the winner costs three or four. Same result,
# ~1 of every 3 probes removed from the hottest loop in the module.
x1 =
cond do
k1 == -d ->
@@ -276,11 +288,10 @@
k1 == d ->
vget(v1, k1_off - 1) + 1
vget(v1, k1_off - 1) < vget(v1, k1_off + 1) ->
vget(v1, k1_off + 1)
true ->
below = vget(v1, k1_off - 1)
above = vget(v1, k1_off + 1)
if below < above, do: above, else: below + 1
vget(v1, k1_off - 1) + 1
end
y1 = x1 - k1
@@ -388,6 +399,7 @@
defp reverse_sweep(a, b, a_lo, b_lo, n, m, v_off, delta, front?, v1, v2, d, k2, k2s, k2e) do
k2_off = v_off + k2
# Same two-probe form as forward_sweep — see the note there.
x2 =
cond do
k2 == -d ->
@@ -396,11 +408,10 @@
k2 == d ->
vget(v2, k2_off - 1) + 1
vget(v2, k2_off - 1) < vget(v2, k2_off + 1) ->
vget(v2, k2_off + 1)
true ->
vget(v2, k2_off - 1) + 1
below = vget(v2, k2_off - 1)
above = vget(v2, k2_off + 1)
if below < above, do: above, else: below + 1
end
y2 = x2 - k2
test/ex_git_objectstore/diff/myers_optimality_test.exs +157 −0
@@ -1,0 +1,157 @@
defmodule ExGitObjectstore.Diff.MyersOptimalityTest do
@moduledoc """
Ground-truth tests for `Diff.Myers` against an independent reference.
The existing Myers tests check hand-written cases. These check the two
properties that actually define a correct shortest-edit-script, on
randomized input, against a dynamic-programming LCS computed here:
1. **Soundness** — applying the edit script to `a` reproduces `b`, and
the `:eq`/`:del` entries in order reproduce `a`.
2. **Optimality** — the number of non-`:eq` edits equals `2 * (n + m) / 2`
minus twice the LCS length, i.e. the script is genuinely shortest.
An algorithm can be sound while producing a needlessly long script;
only this property catches that.
These exist so the bisect loop can be optimized without silently changing
what it produces (fangorn/anvil#367). Anything that perturbs the middle
snake search — a mis-cached V-table read, an off-by-one in a sweep bound —
breaks optimality long before it breaks soundness.
"""
use ExUnit.Case, async: true
alias ExGitObjectstore.Diff.Myers
# Length of the longest common subsequence, by the textbook O(n*m) DP.
# Deliberately naive: it is the reference, so it must be obviously right.
defp lcs_length(a, b) do
a = List.to_tuple(a)
b = List.to_tuple(b)
n = tuple_size(a)
m = tuple_size(b)
Enum.reduce(0..(n - 1)//1, List.duplicate(0, m + 1), fn i, prev ->
Enum.reduce(0..(m - 1)//1, {[0], prev}, fn j, {cur, prev} ->
val =
if elem(a, i) == elem(b, j) do
Enum.at(prev, j) + 1
else
max(Enum.at(prev, j + 1), hd(cur))
end
{[val | cur], prev}
end)
|> then(fn {cur, _} -> Enum.reverse(cur) end)
end)
|> List.last()
end
defp apply_script(edits) do
from = for {t, x} <- edits, t in [:eq, :del], do: x
to = for {t, x} <- edits, t in [:eq, :ins], do: x
{from, to}
end
defp check(a, b) do
edits = Myers.diff(a, b)
{from, to} = apply_script(edits)
assert from == a,
"script's :eq/:del entries must reproduce a\n a=#{inspect(a)}\n got=#{inspect(from)}"
assert to == b,
"script's :eq/:ins entries must reproduce b\n b=#{inspect(b)}\n got=#{inspect(to)}"
eq_count = Enum.count(edits, &match?({:eq, _}, &1))
changes = length(edits) - eq_count
expected_lcs = if a == [] or b == [], do: 0, else: lcs_length(a, b)
expected_changes = length(a) + length(b) - 2 * expected_lcs
assert changes == expected_changes,
"""
edit script is not minimal
a = #{inspect(a)}
b = #{inspect(b)}
changes = #{changes}, optimal = #{expected_changes} (lcs = #{expected_lcs})
script = #{inspect(edits)}
"""
# An optimal script also can't claim more equalities than the LCS allows.
assert eq_count == expected_lcs
end
describe "soundness and optimality on randomized input" do
test "small alphabet, high collision rate — the case that stresses the snakes" do
seeds = Enum.to_list(1..300)
for seed <- seeds do
:rand.seed(:exsss, {seed, seed * 7, seed * 13})
a = for _ <- 1..:rand.uniform(12), do: Enum.random(["a", "b", "c"])
b = for _ <- 1..:rand.uniform(12), do: Enum.random(["a", "b", "c"])
check(a, b)
end
end
test "large alphabet, mostly-unique lines — the typical source-code case" do
for seed <- 1..200 do
:rand.seed(:exsss, {seed, seed * 3, seed * 11})
a = for _ <- 1..:rand.uniform(20), do: Enum.random(1..40)
b = for _ <- 1..:rand.uniform(20), do: Enum.random(1..40)
check(a, b)
end
end
test "b is a mutated copy of a — the realistic diff shape" do
for seed <- 1..200 do
:rand.seed(:exsss, {seed, seed * 5, seed * 17})
a = for _ <- 1..:rand.uniform(18), do: Enum.random(1..25)
b =
Enum.flat_map(a, fn x ->
case :rand.uniform(4) do
1 -> []
2 -> [x, Enum.random(1..25)]
_ -> [x]
end
end)
check(a, b)
end
end
test "degenerate shapes" do
check([], [])
check([], [1, 2, 3])
check([1, 2, 3], [])
check([1], [1])
check([1], [2])
check([1, 2, 3], [1, 2, 3])
check(Enum.to_list(1..50), Enum.to_list(1..50))
# pure append — no deletions at all (the hephaestus#74 hot shape)
check(Enum.to_list(1..30), Enum.to_list(1..30) ++ Enum.to_list(100..160))
# pure prepend
check(Enum.to_list(1..30), Enum.to_list(100..160) ++ Enum.to_list(1..30))
# complete replacement — nothing in common
check(Enum.to_list(1..25), Enum.to_list(100..125))
end
end
describe "diff_lines" do
test "round-trips real multi-line text" do
a = Enum.map_join(1..60, "\n", &"line #{&1}")
b =
Enum.map_join(1..60, "\n", fn i ->
if rem(i, 7) == 0, do: "CHANGED #{i}", else: "line #{i}"
end)
edits = Myers.diff_lines(a, b)
{from, to} = apply_script(edits)
assert Enum.join(from, "\n") == a
assert Enum.join(to, "\n") == b
end
end
end