Skip to content

Experimental optional vectorization for SphMap, PolyMap and MatrixMap - #51

Open
embray wants to merge 7 commits into
Starlink:masterfrom
embray:simd-experiment
Open

Experimental optional vectorization for SphMap, PolyMap and MatrixMap#51
embray wants to merge 7 commits into
Starlink:masterfrom
embray:simd-experiment

Conversation

@embray

@embray embray commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Background

A couple weeks ago I was doing some benchmarking of AST (wrapped in my libasdf-gwcs library) against the Python GWCS package. I found, to no surprise, that for many cases AST far outperformed Python (on a single thread). However, at about N=100000 points the Python code started to outperform slightly.

I concluded that this was due to NumPy's built-in SIMD support for many of its ufuncs (and potentially BLAS as well, though I don't think that was a major contributor since there are only some small matrix multiplications performed). On my machine N=100000 happened to be where the Python overhead is amortized by the SIMD enhancements.

I found that the major contributors in my case were PolyMap, MatrixMap, and SphMap, with the biggest potential win coming from SphMap. So I set about adding some bare minimum compiler (GCC) generated vectorization in targeted areas to see if we could get any quick wins, starting with SphMap, and indeed it did make a big difference.

I've added a little benchmark program adapted from my libasdf-gwcs benchmark. I then went on to see if I could get any easy wins with MatrixMap and PolyMap. These were a little trickier to get right, especially PolyMap. The key observation in all these cases is that the main outer loop of each Transform method is over the input points. In a way this makes sense for cache efficiency, but ignoring that for the first pass I trudged ahead, and did close the gap with Python up to a point--eventually at even larger N the performance was being dominated by DRAM moves.

At least for MatrixMap and PolyMap I added a utility function to return the system's L2 cache size (should work for any current x86-64, YMMV on other platforms), and add a separate outer loop to chunk the points into sets small enough to fit in cache (assuming not huge numbers of polynomial coeffs or matrix elements, other overhead), and this proved a big win--AST at parity with or better than NumPy, with near consistent throughput and memory usage even for large N.

End-to-end impact (libasdf-gwcs vs Python GWCS)

The real motivation, and the most representative measurement, is full Roman WCS evaluation through libasdf-gwcs (which drives AST's astTran2 over a complete WCS pipeline on real Roman L2 calibration files). The plots below show single-thread throughput vs Python GWCS across N, before and after this change.

Before:

throughput

After, with AST compiled with SIMD features:

libasdf-gwcs vs Python GWCS -- after AST SIMD

The above was just from one run of the benchmark but YMMV. In effect this closes the gap with NumPy, and I had some runs depending on the conditions that outperformed it entirely.

The per-transform microbenchmarks further down isolate where the gains come from; this end-to-end view is what actually matters for a WCS pipeline, and is also where the L2 chunking pays off most (each pipeline stage's output stays cache-resident as the next stage's input, rather than being re-streamed from DRAM).

What's in this PR

  • Opt-in build flag. A new AST_ENABLE_SIMD option (CMake and autotools), OFF by default. When enabled, SIMD-friendly compiler flags (-fopenmp-simd -ffast-math -fno-associative-math -march=x86-64-v3) are applied only to sphmap.c, polymap.c and matrixmap.c, so the floating-point semantics of the rest of the library are unchanged. The flags are only accepted on a GCC/glibc toolchain that supports them; otherwise the option is a no-op.

  • TransformLoop vtable slot. Each of SphMap, PolyMap and MatrixMap gains a per-class TransformLoop slot. Transform is now a thin wrapper (validation, invert handling, point extraction) that dispatches to either TransformLoopScalar or TransformLoopSIMD. This keeps the scalar and vectorized inner loops cleanly separated instead of #ifdef-ing through the body of Transform.

  • Runtime UseSIMD attribute. A per-instance integer attribute on each of those classes (default 1 when built with SIMD support, 0 otherwise) lets a caller force the scalar path at runtime -- useful for bit-exact reproducibility or debugging without rebuilding. Setting UseSIMD=1 on a library built without SIMD reports AST__ATSER. The attribute is deliberately not serialized (it's just a runtime tuning knob, not part of the mapping description). I debated making this some kind of global setting but opted to make it specific to the supported transforms for now.

    • Between TransformLoop and UseSIMD there's a bunch of duplicative code. I didn't want to do any more refactoring for now before waiting to see what you have to say about this, but it seems to merit a base class or even just moved into the base Mapping class.
  • astCPUCacheSize(level) in memory.c: returns the L1D/L2/L3 cache size via CPUID (x86-64), with a sysconf fallback and hard-coded defaults. PolyMap and MatrixMap use the L2 size to pick a chunk size.

  • The vectorized kernels themselves (the substance):

    • SphMap -- inline atan2/sqrt (forward) and sin/cos (inverse) under #pragma omp simd, which GCC routes through glibc libmvec; a scalar fixup pass handles poles and bad values.
    • PolyMap -- invert the nested evaluation loops so the innermost loop is over points (terms become independent and vectorize); process points in L2-sized chunks so the per-power work buffers stay cache-resident. Falls back to scalar for small N (where the per-call buffer setup dominates) and for ChebyMap (which overrides PolyPowers). This could easily be extended to ChebyMap too but I skipped it for now since my usecase doesn't use any Chebychev polynomials, at least for now.
    • MatrixMap -- full matrix: point loop innermost, VFMADD via #pragma omp simd, L2 chunking, and a bad-value fixup that runs only when a vectorized scan finds a bad input; diagonal matrix: a branchless compare/blend.
  • perf/bench_simd.c -- a standalone benchmark (details below).

Benchmark

perf/bench_simd runs each transform twice in a single process -- scalar (UseSIMD=0) and SIMD (UseSIMD=1) via the runtime attribute -- over an N sweep, and prints a scalar-vs-SIMD summary (plain text, or --markdown).

A note on methodology, since it matters for reading the small-N numbers: each measurement times a calibrated batch of repeated astTranP calls (the batch grows until it runs for at least 20 ms) and reports the amortized per-call time. At small N a single call is dominated by call overhead and clock resolution rather than throughput, so batching is what makes those rows meaningful -- it removes measurement error, not the per-call cost itself. The input arrays stay hot across a batch, so small/medium N measures compute-bound throughput and large N (which exceeds the last-level cache) measures bandwidth-bound throughput. Reported figures are the median over the reps (-r, default 5).

Hardware/build for the numbers below: Intel i7-7820HQ (Kaby Lake, AVX2), L2 = 1 MiB/core, L3 = 8 MiB; GCC 13.3.0 (i.e. just my laptop); -DCMAKE_BUILD_TYPE=RelWithDebInfo -DAST_ENABLE_SIMD=ON.

Headline

Transform what it exercises peak SIMD speedup large-N (16.7M)
sphmap_fwd (x,y,z)->(lon,lat), atan2/sqrt ~8.6x (N~65K) 8.4x
sphmap_inv (lon,lat)->(x,y,z), sin/cos ~6.9x (N~16K) 6.3x
poly5_fwd degree-5 2-D PolyMap (SIP-like) ~3.1x (N~1K) 2.9x
poly1_fwd degree-1 2-D PolyMap (linear) ~2.7x (N~1K) 2.4x
matfull2_fwd 2x2 full MatrixMap (rotation) ~4.5x (N~4K) 2.8x
matdiag2_fwd 2x2 diagonal MatrixMap (scale) ~1.8x (N~4K) 1.2x

A few things worth calling out:

  • SphMap is the big win, as expected -- almost all of its cost is transcendental math, and libmvec vectorizes that directly.
  • PolyMap sustains ~2.5-3x across the useful range.
  • MatrixMap full went from a modest gain to ~2.8-4.5x after restructuring the kernel to write each output once and skip the bad-value fixup on clean data.
  • MatrixMap diagonal stays modest (~1.2x at large N) because it is a single-pass streaming kernel (one read, one multiply, one write per element, no data reuse). At large N it is simply DRAM-bandwidth-bound, and chunking cannot reduce its mandatory traffic -- this is expected, not a regression.
  • Below N~16-32 the SIMD path is at best a wash; PolyMap explicitly falls back to scalar there, and the others come out ~1.0x.

Full sweep

scalar vs SIMD, Mpx/s, N = 1 ... 16.7M (median of 9 reps)
Transform N Scalar Mpx/s SIMD Mpx/s Speedup
sphmap_fwd 1 1.1 1.0 0.90x
sphmap_fwd 4 3.5 3.4 0.97x
sphmap_fwd 8 6.3 7.5 1.20x
sphmap_fwd 16 9.3 16.5 1.77x
sphmap_fwd 32 11.5 27.0 2.35x
sphmap_fwd 64 14.2 42.8 3.01x
sphmap_fwd 128 15.1 58.9 3.90x
sphmap_fwd 256 14.9 83.3 5.60x
sphmap_fwd 512 13.7 90.7 6.62x
sphmap_fwd 1024 13.3 100.1 7.51x
sphmap_fwd 4096 12.6 103.9 8.26x
sphmap_fwd 16384 13.3 113.5 8.55x
sphmap_fwd 65536 12.8 109.6 8.58x
sphmap_fwd 262144 12.7 109.9 8.64x
sphmap_fwd 1048576 12.9 106.5 8.25x
sphmap_fwd 4194304 12.8 107.2 8.40x
sphmap_fwd 16711744 12.4 104.1 8.41x
sphmap_inv 1 1.1 1.1 1.02x
sphmap_inv 4 4.0 4.1 1.01x
sphmap_inv 8 7.0 8.9 1.28x
sphmap_inv 16 10.5 16.4 1.56x
sphmap_inv 32 14.7 28.5 1.94x
sphmap_inv 64 18.2 43.2 2.38x
sphmap_inv 128 20.8 65.7 3.16x
sphmap_inv 256 21.6 87.2 4.03x
sphmap_inv 512 21.9 101.6 4.63x
sphmap_inv 1024 18.0 107.3 5.95x
sphmap_inv 4096 17.1 114.8 6.70x
sphmap_inv 16384 17.3 119.2 6.89x
sphmap_inv 65536 18.0 119.0 6.62x
sphmap_inv 262144 17.5 111.2 6.36x
sphmap_inv 1048576 17.6 110.9 6.30x
sphmap_inv 4194304 16.4 110.0 6.69x
sphmap_inv 16711744 16.9 106.0 6.26x
poly5_fwd 1 0.9 0.9 0.96x
poly5_fwd 4 3.1 3.3 1.04x
poly5_fwd 8 5.5 5.6 1.02x
poly5_fwd 16 9.0 9.1 1.01x
poly5_fwd 32 12.3 15.5 1.26x
poly5_fwd 64 14.4 26.3 1.83x
poly5_fwd 128 16.6 41.8 2.51x
poly5_fwd 256 19.5 55.5 2.84x
poly5_fwd 512 19.8 61.6 3.11x
poly5_fwd 1024 20.6 64.7 3.14x
poly5_fwd 4096 20.9 62.1 2.97x
poly5_fwd 16384 21.7 62.4 2.88x
poly5_fwd 65536 22.1 62.4 2.82x
poly5_fwd 262144 22.4 60.5 2.70x
poly5_fwd 1048576 20.9 59.8 2.86x
poly5_fwd 4194304 21.3 59.9 2.81x
poly5_fwd 16711744 20.6 59.6 2.89x
poly1_fwd 1 0.9 1.0 1.03x
poly1_fwd 4 3.6 3.5 0.97x
poly1_fwd 8 6.3 6.2 0.98x
poly1_fwd 16 10.9 10.2 0.94x
poly1_fwd 32 15.9 18.8 1.18x
poly1_fwd 64 23.2 33.9 1.46x
poly1_fwd 128 28.0 53.5 1.91x
poly1_fwd 256 31.9 72.7 2.28x
poly1_fwd 512 34.3 88.8 2.59x
poly1_fwd 1024 37.2 101.9 2.74x
poly1_fwd 4096 37.7 99.2 2.63x
poly1_fwd 16384 39.3 97.8 2.49x
poly1_fwd 65536 39.9 99.4 2.49x
poly1_fwd 262144 38.9 95.2 2.45x
poly1_fwd 1048576 38.8 89.9 2.31x
poly1_fwd 4194304 36.3 93.1 2.56x
poly1_fwd 16711744 38.5 93.2 2.42x
matdiag2_fwd 1 1.1 1.2 1.11x
matdiag2_fwd 4 4.2 4.5 1.07x
matdiag2_fwd 8 9.0 8.7 0.97x
matdiag2_fwd 16 15.9 17.0 1.07x
matdiag2_fwd 32 36.2 33.6 0.93x
matdiag2_fwd 64 67.6 69.4 1.03x
matdiag2_fwd 128 125.6 130.2 1.04x
matdiag2_fwd 256 215.3 252.8 1.17x
matdiag2_fwd 512 335.3 412.4 1.23x
matdiag2_fwd 1024 457.4 679.5 1.49x
matdiag2_fwd 4096 622.1 1103.5 1.77x
matdiag2_fwd 16384 694.0 1007.3 1.45x
matdiag2_fwd 65536 660.9 1063.3 1.61x
matdiag2_fwd 262144 587.7 736.7 1.25x
matdiag2_fwd 1048576 441.7 543.6 1.23x
matdiag2_fwd 4194304 437.1 505.2 1.16x
matdiag2_fwd 16711744 438.1 508.4 1.16x
matfull2_fwd 1 1.2 1.0 0.83x
matfull2_fwd 4 4.7 4.4 0.94x
matfull2_fwd 8 8.4 8.4 0.99x
matfull2_fwd 16 14.5 16.9 1.16x
matfull2_fwd 32 28.8 35.1 1.22x
matfull2_fwd 64 46.7 63.8 1.37x
matfull2_fwd 128 64.1 122.7 1.91x
matfull2_fwd 256 83.3 216.8 2.60x
matfull2_fwd 512 100.9 338.4 3.35x
matfull2_fwd 1024 108.7 428.4 3.94x
matfull2_fwd 4096 113.2 509.3 4.50x
matfull2_fwd 16384 117.2 488.2 4.16x
matfull2_fwd 65536 118.7 493.8 4.16x
matfull2_fwd 262144 118.6 407.8 3.44x
matfull2_fwd 1048576 117.9 334.9 2.84x
matfull2_fwd 4194304 116.7 328.2 2.80x
matfull2_fwd 16711744 115.5 323.2 2.80x

Correctness and testing

  • The full CMake ctest suite passes; the only failures are ones that pre-date this branch and are unrelated to it.
  • The SIMD paths are not bit-for-bit identical to the scalar paths (-ffast-math lets GCC call libmvec, whose x86_64 vector math functions are documented to within 4 ULP, so a result can differ from scalar libm by a few ULP). This is normally irrelevant. The one test I ran that was sensitive to that (grid_car3) runs with UseSIMD=0 so it stays bit-exact against its committed reference; the rest of the suite runs with SIMD enabled.
  • Built and run clean under -Wall-style warnings and ASan/UBSan (no new warnings, no sanitizer findings) in addition to the normal RelWithDebInfo build.

Caveats and limitations

  • This is currently wired up and tested only for GCC + glibc on x86-64: the build gates on a GNU compiler and the -march=x86-64-v3 flag, and AST_ENABLE_SIMD is off by default, so on any other toolchain it is simply a no-op. Clang can almost certainly do the same (it also supports vector-ABI / libmvec math), but I haven't tested it with Clang at all yet. glibc also ships a libmvec for aarch64 these days, but this build isn't set up or tested for that either.
  • astCPUCacheSize's CPUID path is x86-64; elsewhere it falls back to sysconf and then to conservative defaults (L1 32 KiB, L2 512 KiB, L3 8 MiB).
  • Single-threaded, every transform is ultimately DRAM-bandwidth-bound at large enough N (the diagonal MatrixMap reaches that point almost immediately). Going beyond that needs either threading (more memory bandwidth) or pipeline fusion so intermediate results are consumed while still cache-hot -- the latter is exactly what the end-to-end libasdf-gwcs path benefits from, and is left for future work.

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.26613% with 83 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.60%. Comparing base (b8fcd43) to head (61e3b6f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/matrixmap.c 83.79% 13 Missing and 16 partials ⚠️
src/polymap.c 84.24% 8 Missing and 18 partials ⚠️
src/sphmap.c 83.87% 6 Missing and 14 partials ⚠️
src/memory.c 66.66% 2 Missing and 4 partials ⚠️
src/mapping.c 75.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #51      +/-   ##
==========================================
+ Coverage   61.49%   61.60%   +0.11%     
==========================================
  Files          83       83              
  Lines       96519    96878     +359     
  Branches    30595    30705     +110     
==========================================
+ Hits        59351    59684     +333     
+ Misses      20983    20974       -9     
- Partials    16185    16220      +35     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@dsberry

dsberry commented Jun 3, 2026

Copy link
Copy Markdown
Member

This is all well outside my comfort zone, so I've no real comments to make other than it looks very promising.

@timj

timj commented Jun 3, 2026

Copy link
Copy Markdown
Member

Very impressive work. I had noodled around with MatrixMap SIMD vectorization but for 2x2 matrices I couldn't get it to make a difference and it wasn't clear we had bigger matrices. This is excellent.

@embray

embray commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

I had noodled around with MatrixMap SIMD vectorization but for 2x2 matrices I couldn't get it to make a difference.

The MatrixMap improvements are mostly marginal in practice I found. SphMap was the biggest one by far--afterwards callgrind was still showing most of the remaining time spent on MatrixMap so I dove into it anyways, having become a little obsessed with chasing the last mile. Now it looks like TransformLoopSIMD in polymap.c is back to being the biggest contributor but that's little surprise.

@embray

embray commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

This is all well outside my comfort zone, so I've no real comments to make other than it looks very promising.

Mine too honestly, I just tried to see what we could get almost "for free" from the compiler in terms of AVX2 instruction output, though it still required refactoring the kernels of each transform to vectorize efficiently over the input coordinate points.

I don't want to make a mess of the code though so if/when you have time I'd value your input on the overall architectural decisions (especially refactoring) and code style. It's not urgent of course.

@timj

timj commented Jun 3, 2026

Copy link
Copy Markdown
Member

I am starting to think that for every AST test frameset we have in ast_tester (I know that directory needs to be classified and categorized into subdirectories but I think #37 will be made more complicated if I do that now) we should have a reference forward transform output (maybe of four corners?) so that we can compare outputs from changes like this to what we had before. We could store pixel_x, pixel_y, transform for 100 points in a CSV without blowing up the repo size. At least then you would know if you have substantially changed a mapping unexpectedly.

@embray

embray commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@timj That would not be a bad idea. For what it's worth, as you can see, this didn't really have a big impact on the existing tests. I don't think this would be a big problem for #37. Even if more tests are added I can either incorporate those or, if we want to just focus on the cmake build that's fine too.

@embray
embray force-pushed the simd-experiment branch from 8faea63 to fb52ff5 Compare June 19, 2026 10:18
@embray

embray commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Hmm, as long as we're being persnicketty about satisfying the codecov report, there are several matrix cases that are uncovered by the tests, so I'll try to improve that too.

@embray

embray commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

As far as I'm concerned, this is mostly ready--makes a nice difference already; also improved the tests, boosting coverage a bit. There are two open questions remaining for me:

  • Right now this enables SIMD automatically at runtime if explicitly enabled at build time via cmake, so users automatically get any benefits from it. Questionable whether it should be enabled by default, or instead kept documented as experimental and "opt-in". On the other hand, it still has to be explicitly opted into at build time so maybe that's good enough.
  • Where and how to mention this in the documentation.

@embray
embray force-pushed the simd-experiment branch 2 times, most recently from c8feb10 to b45e4a5 Compare June 26, 2026 11:32
embray referenced this pull request Jul 3, 2026
Baseline x86-64 has no FMA instruction, so compilers round a*b+c twice;
arm64 (and Haswell-or-later x86 via -march=x86-64-v3) contract to a
fused multiply-add by default.  Building one Ubuntu job at v3 makes
contraction-driven transform-oracle differences observable as
baseline-vs-v3 on identical OS and libm, separating them from the libm
differences that dominate ubuntu-vs-macos comparisons.

c_flags is a real matrix dimension (base value '') rather than only an
include key: an include whose standard keys all match an existing
combination would otherwise merge into that job instead of creating a
new one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
embray added a commit to asdf-format/libasdf-gwcs that referenced this pull request Jul 13, 2026
- Starlink/ast#37
- Starlink/ast#51
- Starlink/ast#66
- Starlink/ast#67

This includes the experimental SIMD support (enabled by default, and
support in AST for libfyaml, allowing us to drop the libyaml
requirement).

Also updates the minimum supported versions of the ASDF tags supported
by AST.
@embray embray mentioned this pull request Sep 1, 2026
3 tasks
@timj

timj commented Sep 1, 2026

Copy link
Copy Markdown
Member

I imagine this hasn't been rebased since we added the transform oracle? That would help to solidify whether the SIMD is working properly. I imagine we would also need a way to ensure that we generate oracles from the non-SIMD path? Should we do it as experimental first with an environment variable gate?

@embray

embray commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

I think I have tested it against that, since I did make a private branch with this merged in. But I'll double-check. Needs rebasing anyways.

This is a squashed commit to clean up several WIP steps taken while
experimenting:

wip: small win for diagonal matrix case; perhaps more significant win
for sphmap but needs more testing

temporary (?) workaround for the grid test failure (only fails for me on
car3) but even though this rounding gets the tests to pass, visual
inspection of the results suggests a slightly lower-quality image in the
SIMD case (more jitter in some of the arcs drawn); needs more
examination

wip: try supporting SIMD options in autotools build

Unfortunately automake does not make it easy to set per-object-file
flags, so we have to build supported compilation units as their own
sub-libraries

wip: revert matrixmap changes for now since it wasn't clear if it was
getting the results I wanted (compiler was not vectorizing).  Needs more
careful analysis and should be come back to independently of the sphmap
changes

wip: performance benchmark script (currently for SphMap but more to be
added)

wip: first attempt at polynomial vectorization

results are mixed -- definite ~4x speedup for smaller N, though at
larger N (>~100000) cache pressure seems to dominate, can actually
result in slight regression in performance, of course also with
the memory tradeoff.  Needs more work I think...

wip: huge improvement in polynomial performance at large N

Instead of processing all points at once we take them in chunks -- an
effort is made to determine the available L2 cache capacity; should be
perfectly reliable on any x86_64 processor; on other platforms YMMV.
Chunk size is determined by this and an additional outer loop processes
each chunk in cache maintaining a high throughput and constant memory
charge for any N.

build: fix autotools build for polymap SIMD optimizations

wip: matrixmap enhancements for SIMD

wip: cleanup, consolidation of SIMD-supported mappings

- add astCPUCacheSize to memory.c for use in different mappings and
  expanded it to support LD1 and L3 cache sizes for possible future use.

- Restructured the supported mappings to add a TransformLoop virtual
  method, allowing cleanly separating out the original implementations
  from the SIMD implementations

- Also add UseSIMD attribute for each transform to allow swapping out
  the implementation at runtime (when supported)

  Some duplication here--if we go further down this route might be worth
  adding these either to the Mapping base or maybe more likely a
  "subclass".

- Added simple benchmarks for matrixmap (again derived from my GWCS
  benchmarks) and a more helpful summary table printout (including
  printing in markdown format for easy pasting into GitHub, etc.)

- Cleaned up the failing grid tests -- now that SIMD can be disabled
  at runtime it's cleaner to allow disabling it on specific tests
  (only one I'm aware of, grid_car3) where minor numerical differences
  caused the test to fail.  Better to make this explicit than the
  previous approach of just trying to smooth over the differences with
  rounding.  In fact that particular plot I think (subjectively?) looks
  worse when produced with UseSIMD=1; maybe worth further investigation.

- Consolidated History entries

wip: improve benchmarking and minor performance fixes

- The benchmarks at small N were not reliable due to clock jitter, etc.
  so make several calls in batches to smooth out the per-time call; this
  removes a lot of noise in repeated benchmark runs.

- Identified obvious performance degradation in PolyMap at small N due
  to the additional mallocs; set a minimal N to switch to SIMD version
  similarly to SphMap

- Tightened up the non-diagonial MatrixMap a bit avoiding initial
  zeroing and vectorizing the scan for AST__BAD in the input.
…MD support

plot: disable SIMD in plotting due to negative impacts on curve
plotting in some cases.  After some investigation found that the tiny
1-4 ULP jitter in the vectorized trig functions result in just enough
jitter that it upsets the (dl^2 * (1 - cos^2(theta))) < Crv_limit,
resulting in curves that are overly sub-divided in some places,
resulting in a "staircase" effect.  I don't feel confident to try to
fiddle too much with the curve drawing parameters that are otherwise
working, so it seems the safest approach to preserve reproducibility is
to disable the SIMD computations entirely just for plotting.

This allows reverting the earlier changes I made to the tests to make
them pass by artificially disabling SIMD just in the test; now plotting
is just not broken in general.
This test runs whether or not compiled with SIMD support; mostly it just
exercises the UseSIMD attribute for the affected mappings, and makes
sure also to expercise the non-SIMD code paths for those mappings,
improving overall test coverage.
Found this out actually by the coverage report in the CI.  Curiously the
`__x86_64__` block was being entered, but not finding anything in cpuid
leaf 4.  Turns out AMD CPUs use an extended leaf 0x8000001D for this,
which otherwise works the same way.  The GitHub CI runs don't tell you
by default what actual hardware it's running on so I'm just guessing
this was the reason, but it seems a reasonable guess, and worth fixing
anyways.  Thanks to
https://github.com/m-j-w/CpuId.jl/blob/master/src/CpuId.jl to helping
figure out how to do this.

Adds LCOV_EXCL block around the sysconf fallbacks, which I think now
should be reasonable on most hardware this runs on...but we'll see...
These additional cases exercise more different matrix shapes and code
paths in matrixmap.c that weren't covered.  Debatably this should go in
a more dedicated test for matrixmap, but just put it for now in the test
program I was already working on.
Including it when it's not covered by the tests warps the patch coverage
reports.
@embray

embray commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

@timj Rebased, and confirmed that the oracle tests are passing. Also confirmed via a coverage build that my new code paths were being exercised in the test.

I also re-ran my benchmarks and they were consistent with previous results, with some apparent slight improvement in the polynomial evaluations, and a slight regression in the non-diagonal matrix benchmarks. I haven't looked into whether there are any code changes that could account for that. But I was running the benchmark just on my laptop with my system under average load and performance settings so it's not a clean benchmark; just a sanity check.

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.

3 participants