Skip to content

perf(simd.h): fill in missing NEON paths in simd.h - #5412

Open
lgritz wants to merge 2 commits into
AcademySoftwareFoundation:mainfrom
lgritz:lg-neon
Open

perf(simd.h): fill in missing NEON paths in simd.h#5412
lgritz wants to merge 2 commits into
AcademySoftwareFoundation:mainfrom
lgritz:lg-neon

Conversation

@lgritz

@lgritz lgritz commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Audit of simd.h for places where the SSE branch had an obvious aarch64 counterpart but NEON fell through to the scalar path.

The biggest find is exp() and log(): both were gated on OIIO_SIMD_SSE even though their bodies are written entirely in terms of our own vector ops and contain no raw _mm_ intrinsics at all, so NEON was evaluating them one lane at a time. Widening the gate makes them 2.5x faster.

Also newly vectorized on NEON:

  • vint4/vfloat4 loads from short, unsigned short, char, unsigned char -- these were SSE4-or-scalar, now vmovl/vcvt (1.6x-2.5x, and it carries up to the 8- and 16-wide types, which decompose into vfloat4).
  • ceil, floor and ifloor for vfloat4 and vfloat3 (vrndpq/vrndmq/vcvtmq). round already had a NEON branch; these were simply missed.
  • round for vfloat3, and vreduce_add for vfloat3.
  • transpose and AxBxCxDx for vfloat4 and vint4 (vtrnq + vcombine).
  • matrix44 * vfloat4, where vpaddq_f32 is the exact equivalent of the _mm_hadd_ps the SSE3 path uses.
  • msub, nmadd and nmsub. madd already had a NEON branch; the other three did not, and nmadd was compiling to a separate fmul and fsub rather than a single fused instruction. Switched madd from vmlaq to vfmaq while here so all four match the fused semantics of the _mm_fmadd_ps path (clang was already lowering vmlaq to fmla, so codegen is unchanged).
  • vfloat4::load_pairs, and vfloat4::load(const half*), which was widening to 32 bits only to immediately narrow back to 16.

fast_rint in fmath.h had the same shape of gap: it was gated on SSE4 for the single-instruction path, but aarch64 lowers std::rint to frintx just as well.

cpu_has_neon() is new, so that the hw:simd attribute reports "neon" on ARM rather than an empty string -- cpuid() returns all zeros off x86, so every existing query answered false there.

Two things this deliberately does not do. A NEON path for the general shuffle<i0,i1,i2,i3> using vqtbl1q_u8 was tried and dropped: clang already pattern-matches the scalar form into rev64/ext/zip, sometimes more cheaply than a table lookup, and the benchmark showed no difference. And nothing was added for the 8- and 16-wide gather/scatter fallbacks, since aarch64 has no gather instruction.

simd_test on an M4: 428 of 649 benchmarks improved by more than 10%, none regressed by more than 10%.

Assisted-by: Claude Code / claude-opus-5

Audit of simd.h for places where the SSE branch had an obvious aarch64
counterpart but NEON fell through to the scalar path.

The biggest find is exp() and log(): both were gated on OIIO_SIMD_SSE even
though their bodies are written entirely in terms of our own vector ops and
contain no raw _mm_ intrinsics at all, so NEON was evaluating them one lane
at a time. Widening the gate makes them 2.5x faster.

Also newly vectorized on NEON:

  * vint4/vfloat4 loads from short, unsigned short, char, unsigned char --
    these were SSE4-or-scalar, now vmovl/vcvt (1.6x-2.5x, and it carries up
    to the 8- and 16-wide types, which decompose into vfloat4).
  * ceil, floor and ifloor for vfloat4 and vfloat3 (vrndpq/vrndmq/vcvtmq).
    round already had a NEON branch; these were simply missed.
  * round for vfloat3, and vreduce_add for vfloat3.
  * transpose and AxBxCxDx for vfloat4 and vint4 (vtrnq + vcombine).
  * matrix44 * vfloat4, where vpaddq_f32 is the exact equivalent of the
    _mm_hadd_ps the SSE3 path uses.
  * msub, nmadd and nmsub. madd already had a NEON branch; the other three
    did not, and nmadd was compiling to a separate fmul and fsub rather than
    a single fused instruction. Switched madd from vmlaq to vfmaq while here
    so all four match the fused semantics of the _mm_fmadd_ps path (clang
    was already lowering vmlaq to fmla, so codegen is unchanged).
  * vfloat4::load_pairs, and vfloat4::load(const half*), which was widening
    to 32 bits only to immediately narrow back to 16.

fast_rint in fmath.h had the same shape of gap: it was gated on SSE4 for the
single-instruction path, but aarch64 lowers std::rint to frintx just as well.

cpu_has_neon() is new, so that the hw:simd attribute reports "neon" on ARM
rather than an empty string -- cpuid() returns all zeros off x86, so every
existing query answered false there.

Two things this deliberately does not do. A NEON path for the general
shuffle<i0,i1,i2,i3> using vqtbl1q_u8 was tried and dropped: clang already
pattern-matches the scalar form into rev64/ext/zip, sometimes more cheaply
than a table lookup, and the benchmark showed no difference. And nothing was
added for the 8- and 16-wide gather/scatter fallbacks, since aarch64 has no
gather instruction.

simd_test on an M4: 428 of 649 benchmarks improved by more than 10%, none
regressed by more than 10%.

Assisted-by: Claude Code / claude-opus-5

Signed-off-by: Larry Gritz <lg@larrygritz.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves OpenImageIO’s SIMD infrastructure on ARM by filling in missing NEON implementations in simd.h, reducing scalar fallbacks (notably for exp()/log()), and enhancing runtime SIMD capability reporting to include NEON where applicable.

Changes:

  • Add multiple NEON-accelerated code paths in simd.h (loads, rounding ops, transpose helpers, FMA-family helpers, matrix multiply, and exp/log gating).
  • Extend fast_rint(float) to use the fast single-instruction path on NEON targets.
  • Introduce cpu_has_neon() and report NEON in the hw:simd capability string.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/include/OpenImageIO/simd.h Adds NEON implementations for several SIMD ops and broadens exp/log gating to include NEON.
src/include/OpenImageIO/fmath.h Enables the fast std::rint-based path for fast_rint(float) on NEON.
src/include/OpenImageIO/platform.h Adds cpu_has_neon() helper used for runtime SIMD reporting.
src/libOpenImageIO/imageio.cpp Includes NEON in the hw:simd capability string when available.
Suppressed comments (6)

src/include/OpenImageIO/simd.h:7718

  • The NEON path uses vfmaq_f32, which is not available on all NEON targets (notably armv7/aarch32). simd.h already uses OIIO_SIMD_NEON && defined(__aarch64__) to guard AArch64-only intrinsics (e.g. vdivq_f32 at simd.h:7131). Please guard vfmaq_f32 similarly and keep a non-AArch64 NEON fallback (vmlaq_f32).
#elif OIIO_SIMD_NEON
    return vfmaq_f32(c.simd(), a.simd(), b.simd());   // c + a*b
#elif OIIO_SIMD_SSE && !defined(_MSC_VER)

src/include/OpenImageIO/simd.h:7738

  • The NEON path uses vfmaq_f32 without an AArch64 guard. If OIIO_SIMD_NEON is enabled for armv7/aarch32, this can be a compile break. Consider using vfmaq_f32 only for AArch64 and falling back to vmlaq_f32 for other NEON targets (matching the existing pattern of guarding AArch64-only intrinsics in simd.h).
#elif OIIO_SIMD_NEON
    return vfmaq_f32(vnegq_f32(c.simd()), a.simd(), b.simd());   // -c + a*b
#elif OIIO_SIMD_SSE && !defined(_MSC_VER)

src/include/OpenImageIO/simd.h:7759

  • The NEON path uses vfmsq_f32, which is AArch64-only on many toolchains. Since OIIO_SIMD_NEON can be enabled on armv7/aarch32, please guard this and provide a non-AArch64 NEON fallback (e.g. vmlsq_f32) to avoid build failures.
#elif OIIO_SIMD_NEON
    return vfmsq_f32(c.simd(), a.simd(), b.simd());   // c - a*b
#elif OIIO_SIMD_SSE && !defined(_MSC_VER)

src/include/OpenImageIO/simd.h:7780

  • The NEON path uses vfmaq_f32 without an AArch64 guard. On armv7/aarch32 NEON builds this may not compile. Consider gating the fused intrinsic to AArch64 and using vmlaq_f32 as the fallback for other NEON targets.
#elif OIIO_SIMD_NEON
    return vnegq_f32(vfmaq_f32(c.simd(), a.simd(), b.simd()));   // -(c + a*b)
#elif OIIO_SIMD_SSE && !defined(_MSC_VER)

src/include/OpenImageIO/simd.h:8028

  • This NEON branch uses vtrn1_f32, which is AArch64-only in many NEON header implementations. Because OIIO_SIMD_NEON can be set for armv7/aarch32, this risks a compile break. Consider using an AArch64-only branch with vtrn1_f32 and a generic NEON fallback using vtrn_f32.
#elif OIIO_SIMD_NEON
    float32x2_t ab = vtrn1_f32 (vget_low_f32(a), vget_low_f32(b)); // a0 b0
    float32x2_t cd = vtrn1_f32 (vget_low_f32(c), vget_low_f32(d)); // c0 d0
    return vcombine_f32 (ab, cd);

src/include/OpenImageIO/simd.h:8045

  • This NEON branch uses vtrn1_s32, which is AArch64-only in many NEON header implementations. Since OIIO_SIMD_NEON may also be enabled for armv7/aarch32, consider guarding this and providing a generic NEON fallback using vtrn_s32.
#elif OIIO_SIMD_NEON
    int32x2_t ab = vtrn1_s32 (vget_low_s32(a), vget_low_s32(b)); // a0 b0
    int32x2_t cd = vtrn1_s32 (vget_low_s32(c), vget_low_s32(d)); // c0 d0
    return vcombine_s32 (ab, cd);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/include/OpenImageIO/simd.h Outdated
Comment on lines +657 to +661
#if defined(__aarch64__) || defined(__aarch64) || defined(_M_ARM64) || defined(_M_ARM64EC)
return true;
#else
return false;
#endif
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Larry Gritz <lg@larrygritz.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/include/OpenImageIO/simd.h:7737

  • The SSE/FMA comment in msub() names the wrong intrinsic: this branch calls _mm_fmsub_ps (not _mm_fnmsub_ps). This is misleading for future maintenance and audits of fused semantics.
#if OIIO_SIMD_SSE && OIIO_FMA_ENABLED
    // If we are sure _mm_fnmsub_ps intrinsic is available, use it.
    return _mm_fmsub_ps (a, b, c);

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.

2 participants