Bisection Debugging
18 min
When a bug was introduced somewhere in a long history of commits (or a
long sequence of steps, or a huge input), checking every single one
one-by-one is slow. Bisection debugging — the same idea as git bisect — applies binary search (from the Algorithms course) directly to
the debugging process itself: check the MIDDLE candidate, and the result
tells you which half to search next.
def find_first_failing_index(results):
lo, hi = 0, len(results)
while lo < hi:
mid = (lo + hi) // 2
if results[mid]:
lo = mid + 1 # this one passed -- bug is later
else:
hi = mid # this one failed -- bug is here or earlier
return lo if lo < len(results) else -1
This precondition -- once it starts failing, it KEEPS failing -- is essential. Bisection gives a wrong answer on a 'flaky' bug that fails intermittently rather than consistently after some point.
This is precisely the O(log n) idea from the Complexity Analysis
course, applied to debugging: 1,000 commits takes at most ~10
comparisons to bisect, not 1,000. git bisect automates exactly this
loop — you tell it "good" or "bad" for whatever commit it checks out,
and it narrows the search the same way.
def bisect_call_count(results):
lo, hi = 0, len(results)
count = 0
while lo < hi:
mid = (lo + hi) // 2
count += 1
if results[mid]:
lo = mid + 1
else:
hi = mid
return count
Write `find_first_failing_index(results)`: `results` is a list of booleans (True = passed) where, once a version starts failing, every later version also fails. Using BINARY SEARCH (not a linear scan), return the index of the first failing version, or -1 if none fail.
Write `bisect_call_count(results)`: same bisection logic, but return the NUMBER of comparisons (midpoint checks) it actually took.
Why is bisecting through a sequence of commits (like `git bisect`) to find which one introduced a bug more efficient than checking each commit one by one?
You can apply binary search to find where a bug was introduced across a long sequence, instead of checking every candidate one by one.