Skip to content

boxblur: replace the per-pixel division with an exact reciprocal - #179

Merged
georgmartius merged 1 commit into
masterfrom
perf/boxblur-reciprocal
Aug 9, 2026
Merged

boxblur: replace the per-pixel division with an exact reciprocal#179
georgmartius merged 1 commit into
masterfrom
perf/boxblur-reciprocal

Conversation

@georgmartius

Copy link
Copy Markdown
Owner

Both boxblur passes end in acc/size. size is a runtime value, so that compiled to a real integer division — ~20-30 cycles inside boxblur_hori's serial running sum, and in boxblur_vert it kept the output loop from vectorizing at all, since no SIMD unit has an integer divide.

This was listed under "Known limits" in docs/simd.md, with the claim that a magic-number reciprocal is "provably exact for size <= 4096 with a 32-bit multiply". That bound is wrong. acc <= 255*size, so the product only fits in 32 bits if shift <= 24; exactness then needs 255*size*(size-1) < 2^24, i.e. it holds for odd sizes up to 265 and fails first at 267.

So the constants are checked rather than assumed. vs_reciprocal() searches downward from shift=31 for a shift satisfying both the overflow and the exactness condition, and returns valid=0 if none exists — in which case both passes keep the division. boxblurPlanar only ever passes stepSize-derived sizes, so the fast path is always taken in practice.

Applied to boxblur_hori too, where the division sat in the serial chain; that turned out to be the larger absolute win.

Correctness

Output is bit-identical.

  • Exhaustive off-line check: every size in 1..8192 × every reachable acc, (acc*mul)>>shift == acc/size. No mismatch.
  • boxblur.c's vertical output loop now reports loop vectorized using 16 byte vectors under -fopt-info-vec; it did not before.
  • tests/test_boxblur.c now also checks boxblur_hori_C against a verbatim copy of the original — it was previously untested — and the size list straddles 265/267 so both the reciprocal and the division fallback are exercised. 500 → ~1200 checks.
  • Full suite: 38/38 units pass. Mutation-checked that the new assertions do fail on a deliberately wrong magic number.

Measurements

Ryzen 9 9900X, 1080p, size=15, best of three (bench/bench_boxblur.c):

1 thread 24 threads
boxblur_hori 2.23 → 1.15 ms/frame 0.22 → 0.11 ms/frame
boxblur_vert 2.35 → 0.44 ms/frame 0.88 → 0.27 ms/frame

2.9x on the blur in both configurations.

🤖 Generated with Claude Code

Both passes end in `acc/size`. `size` is a runtime value, so that compiled to
a real integer division: ~20-30 cycles inside `boxblur_hori`'s serial running
sum, and in `boxblur_vert` it kept the output loop from vectorizing at all,
since no SIMD unit has an integer divide.

Replaced by `(acc*mul) >> shift` with `mul = ceil(2^shift/size)`. The
constants are checked rather than assumed: `acc <= 255*size` forces the 32-bit
product to cap `shift` at 24, and exactness then needs
`255*size*(size-1) < 2^24` -- true for every odd size up to 265, first failing
at 267. So `vs_reciprocal()` searches for a shift satisfying both the overflow
and the exactness condition and reports failure if there is none, in which
case both passes keep the division. (`boxblurPlanar` only ever passes
stepSize-derived sizes, far inside the range.)

Output is bit-identical. `test_boxblur` now also checks `boxblur_hori_C`
against a verbatim copy of the original, which was untested, and the size list
straddles 265/267 so the reciprocal and the division fallback are both
exercised -- 500 -> ~1200 checks.

Ryzen 9 9900X, 1080p, size=15, best of three, ms/frame:

  boxblur_hori   2.23 -> 1.15 (1 thread)   0.22 -> 0.11 (24 threads)
  boxblur_vert   2.35 -> 0.44 (1 thread)   0.88 -> 0.27 (24 threads)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@georgmartius
georgmartius merged commit 947b817 into master Aug 9, 2026
4 checks passed
@georgmartius
georgmartius deleted the perf/boxblur-reciprocal branch August 9, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant