Repository navigation
Compute the linear index of Cartesian indexing in Horner form - #217
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #217 +/- ##
==========================================
+ Coverage 94.40% 94.44% +0.03%
==========================================
Files 8 8
Lines 322 324 +2
==========================================
+ Hits 304 306 +2
Misses 18 18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`static_size_linear_index` now computes `i + n1 * ((j - 1) + n2 * (k - 1))` instead of the sum of strided terms `1 + (i - 1) + n1 * (j - 1) + (n1 * n2) * (k - 1)`, which needs fewer integer operations. In the flux differencing volume integral of Trixi.jl (2D, 4096 elements, 1 thread), this is 3.5% faster with a `Vector` backend (2553 to 2464 us) and 2.5% faster with a `MemoryRef` backend (#212). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5z6TVTAFfLGSFafUoLzuo
37049a4 to
d769e36
Compare
- Test Cartesian `getindex`/`setindex!` against `LinearIndices` for 1-5 dimensions with odd sizes, size-1 dimensions, `Memory`/`Vector` backends, a static first dimension, and out-of-bounds indices (ported from #206). - FileCheck test: all elements `u[v, i, j, k]` along the first dimension share one offset, with no multiplication after the first load. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
giordano
left a comment
There was a problem hiding this comment.
The bot is happy about performance
PR #217 doesn't change performance for fully dynamic or fully static arrays. With mixed static and dynamic dimensions it's neutral or slightly better, and one stencil case is 10% faster. I found no meaningful slowdown.
Setup: Julia 1.13.2,
-O3, Zen 4 (Threadripper 7995WX), pinned to one core.Float64arrays of size 32×32×32,@inbounds. I tested all 8 dynamic/static patterns:dis anIntdimension,sis aVal(32)dimension, anddddis a plainFixedSizeArray.LLVM IR (instruction count, main → PR)
kernel ddd sdd / sds dsd / dss ssd sss scalar getindex/setindex!18 → 18 (same) 15 → 15 15 → 15 12 → 13 12 → 13 loop with iinnermost252 → 252 315 → 246 (integer ops 87 → 18) same 258 → 255 252 → 249 loop with jinnermostsame 198 → 190 same 261 → 302 255 → 296 stencil along j,k≈ same 331 → 298 (integer ops 130 → 92) 338 → 332 274 → 255 267 → 248 random gather same same same 182 → 118 182 → 118
- Fully dynamic: for scalar indexing, LLVM already turns main's sum of strided terms into the Horner form (
%reass.mul), so both versions give the same IR.- Fully static: main uses 3
shlinstructions folded into the address; the PR adds the shifts together first. In native code that's 10 vs 11 instructions, with one extramov.- Mixed patterns: this is where the counts differ, sometimes a lot, but the cycle count barely moves (see below).
Timing (PR/main)
Separate processes gave noisy results: the stencil times varied by up to 2× between runs of the same configuration. So I compared both formulas in one process, swapping the method back and forth 4 times. Bold cells moved in the same direction in every repetition.
kernel ddd sdd dsd dds ssd sds dss sss loop with iinnermost1.00 1.03 0.97 0.97 1.00 1.00 1.00 1.00 loop with kinnermost1.01 1.00 1.00 0.99 0.98 0.99 1.00 0.99 stencil 0.96 1.02 1.00 1.00 1.00 0.90 1.00 1.01 loop with jinnermost*1.00 0.99 1.02 1.00 1.00 0.99 1.00 0.97 random gather* 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 * These two kernels were timed in separate processes (3 per version), where they were stable to within about 2%.
- Fully dynamic and fully static: within ±1%. The +1% on
dddwithkinnermost is consistent but tiny.- Mixed: the stencil on
sdsis 10% faster in every repetition.ssdwithkinnermost is 2% faster.sddwithiinnermost might be 1–3% slower, but that signal is weak. The PR description reported up to 2.7% slowdown withkinnermost for mixed patterns; I didn't reproduce it.- Random gather: the index can't be turned into pointer increments here, and the 35% fewer instructions for
ssd/sssmade no difference in time. The loads dominate.The IR-count changes are big in places, but the measured effect is ±1–3%, apart from a 10% gain for the stencil on
sds.
Follow-up to #213: compute the linear index of Cartesian indexing in Horner form.
Before:
static_size_linear_indexcomputes the column-major index as a sum of strided terms,1 + (i - 1) + n1 * (j - 1) + (n1 * n2) * (k - 1).After: Horner form,
i + n1 * ((j - 1) + n2 * (k - 1)). Static dimensions still give constant factors. This affects allFixedSizeArrays, not only those with static dimensions.History
The first
StaticSizeArrayversion in #210 used Horner form. When any dimension could be static, it switched to the strided sum, with this rationale: "Unlike the Horner form, the terms of indices that don't change in a loop can be hoisted out of it if the dimensions are dynamic." A review commit later dropped that comment. The measurements below show the concern is small.Trixi.jl
This is the volume integral of Trixi.jl's 2D
elixir_euler_ec.jl, with the flux-differencing kernel inlined. Setup: 4096 elements, polydeg 3, Julia 1.12, 1 thread pinned to one core,Vectorbackend.rhs_hyperbolic!That is about −3.9% and −3.3%. Each row covers 3 processes run with
JULIA_LLVM_ARGS="-align-all-nofallthru-blocks=6". Without forced alignment, the mean gain over 6 processes is 1.5% (2566 → 2527 µs), because the timing of this kernel swings by up to about 6% from one process to the next, depending on where the code is placed. In the kernel IR, the integer shifts go from 21 to 15.Microbenchmarks: static and dynamic dimensions
I compared both formulas in one process on the same
Memory, computingdst = 2 * srcand similar operations on a 32×32×32 array (8×64×64 for the pairs kernel). I tested all 8 static/dynamic patterns of the 3 dimensions (d= dynamic,s= static). The table shows Horner time / strided time, minimum of 200 calls:iinnermost (column-major)kinnermost (row-major)jinnermostj,ki,iiddd, i.e. every plainFixedSizeArray) show no difference.Tests: the full suite, including the FileCheck tests for indexing, passes on Julia 1.10 and 1.12.
🤖 Generated with Claude Code
https://claude.ai/code/session_01D5z6TVTAFfLGSFafUoLzuo