Repository navigation
Conversation
getindex/setindex! with N Int indices compute l = 1 + (i1-1) + s1*((i2-1) + s2*((i3-1) + ...)), as StrideArraysCore.jl does for dense arrays, instead of Base's forward sum with stride products, then index the parent with the linear index as before. Assisted-by: Claude Code (Opus 5.5)
Assisted-by: Claude Code (Opus 5.5)
4013c6f to
042d8b3
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #206 +/- ##
==========================================
- Coverage 93.50% 93.46% -0.04%
==========================================
Files 6 6
Lines 231 245 +14
==========================================
+ Hits 216 229 +13
- Misses 15 16 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Is this worth a FileCheck test? 👀 |
If it makes things slower, then yeah, a version guard would be better. And FileCheck tests to make sure the important properties are preserved 😃 |
|
should base switch also? |
|
Ah its not a clear win. It only really seems to win when only the last dimension is dynamic and the rest static. |
|
Replaced by #217 |
- 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>
* Compute the linear index of Cartesian indexing in Horner form `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 * Add tests and a CHANGELOG entry for the Horner-form linear index - 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> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
getindex/setindex!withNIntindices now compute the linear index in Horner form,1 + (i1 - 1) + s1 * ((i2 - 1) + s2 * ((i3 - 1) + ...)), as StrideArraysCore.jl does for dense arrays. Previously they used Base's sum of stride products,1 + (i1 - 1) + (i2 - 1) * s1 + (i3 - 1) * (s1 * s2) + .... Element access is unchanged (parent(A)[l]aftercheckbounds), so bounds checking,@inbounds, effects and non-CPU parents behave as before. Calls with fewer or more indices thanNstill use the genericAbstractArraymethods.Why the order matters
Both forms give the same value. With the Horner form, all elements that differ only in the first index share the nested part
s1 * (...)of the offset. A kernel that readsu[v, i, j, k, e]forv in 1:5therefore needs one base address plus constant offsets. With the forward sum, especially after a size check such asaxes(u) == ax || throw(...)has made the dimensions compile-time constants, LLVM reassociates the sum differently for each element and computes a separate offset (add+shl+gep) for each of them. That costs instructions and registers. With this PR the optimized IR of the Trixi.jl kernel below has the same shape as withStrideArraysCore.PtrArray:add/shlgo from 276/117 to 152/57.Performance
MWE from #201 (
kernel!with a size check and 5-variable accesses), FixedSizeArray backed by aVector, 1 thread, instructions per call (perf counters) and minimum time:Trixi.jl (trixi-framework/Trixi.jl#3357, which uses
FixedSizeArrays for the solution arrays; TreeMesh3D Taylor-Green vortex, polydeg 3, 1 thread), Julia v1.13.1:rhs!: 24.73 M → 24.41 M instructions, 2387 → 2315 μsOn nightly the volume integral improves by about 0.5%. On 1.12.7 it needs 1.4% more instructions (21.21 → 21.49 M) at the same time.
Julia v1.10: with LLVM 15 the Horner form is slower in the MWE (+12% instructions, +17% time, consistent over repeated runs). I have not found out why yet. If that matters, the methods could be defined only for
VERSION >= v"1.11"orv"1.13". I left them ungated on purpose and am happy to add a gate if you prefer.General microbenchmarks:
sum, iteration, broadcast,copyto!, views andCartesianIndicesloops are unchanged. A 2-d stencil needs 3% (1.13) to 19% (nightly) fewer instructions, and a 2-d matvec 10% fewer on nightly.Follows #201/#202 (eager
size), which made the dimensions available to LLVM after a size check in the first place.Tests
The new test set "Cartesian indexing with
Ints" comparesa[I...]anda[I...] = xagainstLinearIndicesfor N = 1–5 with odd sizes, including size-1 dimensions. It covers Memory- and Vector-backed arrays andFloat64/Int16elements, plus out-of-bounds errors in each dimension and trailing indices. The full test suite passes on 1.10.12, 1.12.7, 1.13.1 and 1.14.0-DEV.3384. The 2 broken tests on 1.13/nightly are already broken onmain. Enzyme gradients through Cartesiangetindex/setindex!were checked on 1.12 (forward and reverse, Memory- and Vector-backed).This PR was prepared with assistance from Claude Code (Anthropic's AI coding assistant). I reviewed the changes.
🤖 Generated with Claude Code