Build voxel grids about 1.5x faster, with identical output - #33
Merged
Conversation
The camera runs made the voxel kernel the thing worth looking at: on a
saturated EVK4 it took 211 ms of a 50 ms window and was 80 % of loop time, so
the capture path was never what limited that loop.
Almost all of the cost turned out to be one line. Each event called both
`floor()` and `ceil()` and compared the results, which is two saturating
float-to-int conversions per event where one will do. `age` is filtered to
`[0, window_ms]` immediately above, so `position` lands in `[0, bins - 1]` and
is never negative --- and for a non-negative float `as usize` *is* the floor.
The fraction then carries everything the second call was for: it is zero
exactly when floor and ceil agreed, and when it is not, the upper bin is the
next one up, which is in range because a non-integer position is strictly below
`bins - 1`.
Measured on 2.8 M events over a 1280x720 grid, best of seven, old and new
kernels A/B'd in one process:
bins=1 61.0 ms -> 40.4 ms 1.51x
bins=5 96.8 ms -> 65.3 ms 1.48x
bins=9 109.0 ms -> 71.0 ms 1.54x
Output is bit-identical at every bin count --- the arithmetic is untouched, and
only the way the bin index is extracted from it changed. That was the point of
keeping the change this small: three other rewrites were tried and each of them
moved results in the last bits for less benefit than this one gives for none.
What the same benchmark says not to bother with, since the measurements cost
nothing to record and save someone repeating them:
- Bounds checks are free here. Replacing both indexed writes with
`get_unchecked_mut` came out slower than the safe version, inside noise.
The scatter is waiting on memory, not on checks.
- Accumulating bin-minor and transposing once at the end --- so an event's
two contributions share a cache line instead of sitting `plane_len` apart
--- was 15 % *slower*. The transpose costs more than the locality wins.
- Walking the four columns by index instead of through `stream.iter()` was
slower too; the iterator optimises better than manual indexing does.
- Folding the divide into a constant and filtering ages in integer ticks is
worth ~12 % on its own but nothing once truncation is in, and it does move
the last bits. Not taken.
Rayon over per-thread accumulators reached a further 1.24x on two cores, but it
changes summation order (3.8e-5 on a cell here) and wants sizing on real
hardware, so it is left out of this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vj722xDQa64yshAdG3seYg
A sensor emits many events at one tick --- an EVT3 vector word alone carries up
to twelve --- so the benchmark recording has 26 events per distinct timestamp.
The response `exp(-age/tau)` depends on the timestamp and nothing else, so
remembering the last one turns the most expensive operation in the loop from
once per event into once per tick.
2.8 M events, 1280x720: 66.7 ms -> 50.2 ms 1.33x
Exact, and checked rather than assumed: the output checksum is unchanged
(b903401351dfabf7 before and after). The same input returns the same value, so
there is nothing approximated here --- unlike a faster `exp`, which is the
other way to attack this and moves every cell.
The rest of the representations were measured at the same operating point and
left alone, which is worth writing down so nobody repeats it:
binary 3.2 ms count 6.8 ms polarity 16.3 ms countmask 17.9 ms
tencode 30.8 ms tsurf 32.8 ms voxel 46.8 ms mcts 60.9 ms
`count` and `binary` are already at 400-870 Mev/s and have nothing in them but
a bounds check and an increment. `tsurf` looked like the obvious target ---
it is a time surface, so surely an `exp` per event --- but it already keeps the
latest timestamp per cell and exponentiates once per *cell* at the end, over
only the touched ones. Nothing to take.
`mcts` is the slow one after this and resisted both attempts. Memoising its
five window contributions the same way was worth nothing (57.3 -> 60.2 ms, i.e.
noise), so the five divisions are not what it spends its time on. Accumulating
channel-minor and transposing at the end, so an event's five writes share a
cache line instead of landing in five planes 3.7 MB apart, was far worse:
57.3 -> 106.0 ms. That is the same result the voxel work got from the same idea,
which makes twice that this layout change loses here. What mcts is doing is
waiting on memory for up to five scattered maxima per event across a 37 MB
buffer, and neither arithmetic nor layout moves that.
`reference_time` walking the stream through the `Event` iterator to take a max
is also not worth changing: replacing it with a pass over the timestamp column
alone measured inside noise for every representation that calls it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vj722xDQa64yshAdG3seYg
Tobias-Fischer
force-pushed
the
speed-up-voxel
branch
from
September 3, 2026 03:59
043eecf to
36f479c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The camera runs made the voxel kernel the thing worth looking at: on a
saturated EVK4 it took 211 ms of a 50 ms window and was 80 % of loop time, so
the capture path was never what limited that loop.
Almost all of the cost turned out to be one line. Each event called both
floor()andceil()and compared the results, which is two saturatingfloat-to-int conversions per event where one will do.
ageis filtered to[0, window_ms]immediately above, sopositionlands in[0, bins - 1]andis never negative --- and for a non-negative float
as usizeis the floor.The fraction then carries everything the second call was for: it is zero
exactly when floor and ceil agreed, and when it is not, the upper bin is the
next one up, which is in range because a non-integer position is strictly below
bins - 1.Measured on 2.8 M events over a 1280x720 grid, best of seven, old and new
kernels A/B'd in one process:
Output is bit-identical at every bin count --- the arithmetic is untouched, and
only the way the bin index is extracted from it changed. That was the point of
keeping the change this small: three other rewrites were tried and each of them
moved results in the last bits for less benefit than this one gives for none.
What the same benchmark says not to bother with, since the measurements cost
nothing to record and save someone repeating them:
get_unchecked_mutcame out slower than the safe version, inside noise.The scatter is waiting on memory, not on checks.
two contributions share a cache line instead of sitting
plane_lenapart--- was 15 % slower. The transpose costs more than the locality wins.
stream.iter()wasslower too; the iterator optimises better than manual indexing does.
worth ~12 % on its own but nothing once truncation is in, and it does move
the last bits. Not taken.
Rayon over per-thread accumulators reached a further 1.24x on two cores, but it
changes summation order (3.8e-5 on a cell here) and wants sizing on real
hardware, so it is left out of this change.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Vj722xDQa64yshAdG3seYg