OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
SIMD.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2//
3// Copyright 2026 Caleb Buahin
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
22
55
56#ifndef OPENSWMM_ENGINE_SIMD_HPP
57#define OPENSWMM_ENGINE_SIMD_HPP
58
59#ifndef OPENSWMM_RESTRICT
60#if defined(_MSC_VER)
61#define OPENSWMM_RESTRICT __restrict
62#else
63#define OPENSWMM_RESTRICT __restrict__
64#endif
65#endif
66
67// ============================================================================
68// Cross-compiler vectorisation hint
69// ============================================================================
70
71#ifndef OPENSWMM_IVDEP
72#if defined(_MSC_VER)
73#define OPENSWMM_IVDEP __pragma(loop(ivdep))
74#elif defined(__GNUC__) || defined(__clang__)
75#define OPENSWMM_IVDEP _Pragma("GCC ivdep")
76#else
77#define OPENSWMM_IVDEP
78#endif
79#endif
80
81#include <cstddef>
82#include <cmath>
83#include <algorithm>
84#include <cassert>
85
86// ============================================================================
87// Platform detection
88// ============================================================================
89
90#if !defined(OPENSWMM_SIMD_SCALAR)
91#if defined(__AVX2__)
92#include <immintrin.h>
93#define OPENSWMM_SIMD_AVX2 1
94#define OPENSWMM_SIMD_WIDTH 4
95#elif defined(__ARM_NEON) || defined(__aarch64__)
96#include <arm_neon.h>
97#define OPENSWMM_SIMD_NEON 1
98#define OPENSWMM_SIMD_WIDTH 2
99#else
100#define OPENSWMM_SIMD_SCALAR 1
101#define OPENSWMM_SIMD_WIDTH 1
102#endif
103#else
104#if !defined(OPENSWMM_SIMD_WIDTH)
105#define OPENSWMM_SIMD_WIDTH 1
106#endif
107#endif
108
109// Exactly one SIMD backend must be active
110static_assert((0
111#if defined(OPENSWMM_SIMD_AVX2)
112 + 1
113#endif
114#if defined(OPENSWMM_SIMD_NEON)
115 + 1
116#endif
117#if defined(OPENSWMM_SIMD_SCALAR)
118 + 1
119#endif
120 ) == 1,
121 "Exactly one SIMD backend must be active (AVX2, NEON, or SCALAR)");
122
123namespace openswmm::simd {
124
125// ============================================================================
126// Element-wise operations (arrays of doubles)
127// ============================================================================
128
139inline void add(const double* OPENSWMM_RESTRICT a, const double* OPENSWMM_RESTRICT b,
140 double* OPENSWMM_RESTRICT dst, std::size_t n) noexcept {
141#if defined(OPENSWMM_SIMD_AVX2)
142 const std::size_t nv = n / 4;
143 const std::size_t nr = n % 4;
144 for (std::size_t i = 0; i < nv; ++i) {
145 __m256d va = _mm256_loadu_pd(a + i * 4);
146 __m256d vb = _mm256_loadu_pd(b + i * 4);
147 _mm256_storeu_pd(dst + i * 4, _mm256_add_pd(va, vb));
148 }
149 for (std::size_t i = nv * 4; i < n; ++i)
150 dst[i] = a[i] + b[i];
151 (void)nr;
152#elif defined(OPENSWMM_SIMD_NEON)
153 const std::size_t nv = n / 2;
154 for (std::size_t i = 0; i < nv; ++i) {
155 float64x2_t va = vld1q_f64(a + i * 2);
156 float64x2_t vb = vld1q_f64(b + i * 2);
157 vst1q_f64(dst + i * 2, vaddq_f64(va, vb));
158 }
159 for (std::size_t i = nv * 2; i < n; ++i)
160 dst[i] = a[i] + b[i];
161#else
162 for (std::size_t i = 0; i < n; ++i)
163 dst[i] = a[i] + b[i];
164#endif
165}
166
177inline void multiply(const double* OPENSWMM_RESTRICT a, const double* OPENSWMM_RESTRICT b,
178 double* OPENSWMM_RESTRICT dst, std::size_t n) noexcept {
179#if defined(OPENSWMM_SIMD_AVX2)
180 const std::size_t nv = n / 4;
181 for (std::size_t i = 0; i < nv; ++i) {
182 __m256d va = _mm256_loadu_pd(a + i * 4);
183 __m256d vb = _mm256_loadu_pd(b + i * 4);
184 _mm256_storeu_pd(dst + i * 4, _mm256_mul_pd(va, vb));
185 }
186 for (std::size_t i = nv * 4; i < n; ++i)
187 dst[i] = a[i] * b[i];
188#elif defined(OPENSWMM_SIMD_NEON)
189 const std::size_t nv = n / 2;
190 for (std::size_t i = 0; i < nv; ++i) {
191 float64x2_t va = vld1q_f64(a + i * 2);
192 float64x2_t vb = vld1q_f64(b + i * 2);
193 vst1q_f64(dst + i * 2, vmulq_f64(va, vb));
194 }
195 for (std::size_t i = nv * 2; i < n; ++i)
196 dst[i] = a[i] * b[i];
197#else
198 for (std::size_t i = 0; i < n; ++i)
199 dst[i] = a[i] * b[i];
200#endif
201}
202
211inline double min(const double* a, std::size_t n) noexcept {
212 assert(n >= 1);
213 double result = a[0];
214#if defined(OPENSWMM_SIMD_AVX2)
215 const std::size_t nv = n / 4;
216 if (nv > 0) {
217 __m256d vmin = _mm256_loadu_pd(a);
218 for (std::size_t i = 1; i < nv; ++i) {
219 vmin = _mm256_min_pd(vmin, _mm256_loadu_pd(a + i * 4));
220 }
221 // Horizontal reduction
222 __m128d hi = _mm256_extractf128_pd(vmin, 1);
223 __m128d lo = _mm256_castpd256_pd128(vmin);
224 __m128d m2 = _mm_min_pd(lo, hi);
225 __m128d m1 = _mm_min_pd(m2, _mm_shuffle_pd(m2, m2, 1));
226 result = _mm_cvtsd_f64(m1);
227 }
228 for (std::size_t i = nv * 4; i < n; ++i)
229 result = std::min(result, a[i]);
230#elif defined(OPENSWMM_SIMD_NEON)
231 const std::size_t nv = n / 2;
232 if (nv > 0) {
233 float64x2_t vmin = vld1q_f64(a);
234 for (std::size_t i = 1; i < nv; ++i) {
235 vmin = vminq_f64(vmin, vld1q_f64(a + i * 2));
236 }
237 // Horizontal reduction: min of 2 lanes
238 result = std::min(vgetq_lane_f64(vmin, 0), vgetq_lane_f64(vmin, 1));
239 }
240 for (std::size_t i = nv * 2; i < n; ++i)
241 result = std::min(result, a[i]);
242#else
243 for (std::size_t i = 1; i < n; ++i)
244 result = std::min(result, a[i]);
245#endif
246 return result;
247}
248
257inline double max(const double* a, std::size_t n) noexcept {
258 assert(n >= 1);
259 double result = a[0];
260#if defined(OPENSWMM_SIMD_AVX2)
261 const std::size_t nv = n / 4;
262 if (nv > 0) {
263 __m256d vmax = _mm256_loadu_pd(a);
264 for (std::size_t i = 1; i < nv; ++i) {
265 vmax = _mm256_max_pd(vmax, _mm256_loadu_pd(a + i * 4));
266 }
267 // Horizontal reduction
268 __m128d hi = _mm256_extractf128_pd(vmax, 1);
269 __m128d lo = _mm256_castpd256_pd128(vmax);
270 __m128d m2 = _mm_max_pd(lo, hi);
271 __m128d m1 = _mm_max_pd(m2, _mm_shuffle_pd(m2, m2, 1));
272 result = _mm_cvtsd_f64(m1);
273 }
274 for (std::size_t i = nv * 4; i < n; ++i)
275 result = std::max(result, a[i]);
276#elif defined(OPENSWMM_SIMD_NEON)
277 const std::size_t nv = n / 2;
278 if (nv > 0) {
279 float64x2_t vmax = vld1q_f64(a);
280 for (std::size_t i = 1; i < nv; ++i) {
281 vmax = vmaxq_f64(vmax, vld1q_f64(a + i * 2));
282 }
283 result = std::max(vgetq_lane_f64(vmax, 0), vgetq_lane_f64(vmax, 1));
284 }
285 for (std::size_t i = nv * 2; i < n; ++i)
286 result = std::max(result, a[i]);
287#else
288 for (std::size_t i = 1; i < n; ++i)
289 result = std::max(result, a[i]);
290#endif
291 return result;
292}
293
303inline void clamp(double* a, double lo, double hi, std::size_t n) noexcept {
304#if defined(OPENSWMM_SIMD_AVX2)
305 __m256d vlo = _mm256_set1_pd(lo);
306 __m256d vhi = _mm256_set1_pd(hi);
307 const std::size_t nv = n / 4;
308 for (std::size_t i = 0; i < nv; ++i) {
309 __m256d v = _mm256_loadu_pd(a + i * 4);
310 v = _mm256_max_pd(vlo, _mm256_min_pd(vhi, v));
311 _mm256_storeu_pd(a + i * 4, v);
312 }
313 for (std::size_t i = nv * 4; i < n; ++i) {
314 a[i] = std::max(lo, std::min(hi, a[i]));
315 }
316#elif defined(OPENSWMM_SIMD_NEON)
317 float64x2_t vlo = vdupq_n_f64(lo);
318 float64x2_t vhi = vdupq_n_f64(hi);
319 const std::size_t nv = n / 2;
320 for (std::size_t i = 0; i < nv; ++i) {
321 float64x2_t v = vld1q_f64(a + i * 2);
322 v = vmaxq_f64(vlo, vminq_f64(vhi, v));
323 vst1q_f64(a + i * 2, v);
324 }
325 for (std::size_t i = nv * 2; i < n; ++i) {
326 a[i] = std::max(lo, std::min(hi, a[i]));
327 }
328#else
329 for (std::size_t i = 0; i < n; ++i) {
330 a[i] = std::max(lo, std::min(hi, a[i]));
331 }
332#endif
333}
334
346inline double dot(const double* OPENSWMM_RESTRICT a, const double* OPENSWMM_RESTRICT b,
347 std::size_t n) noexcept {
348 double result = 0.0;
349#if defined(OPENSWMM_SIMD_AVX2)
350 const std::size_t nv = n / 4;
351 if (nv > 0) {
352 __m256d vsum = _mm256_setzero_pd();
353 for (std::size_t i = 0; i < nv; ++i) {
354 __m256d va = _mm256_loadu_pd(a + i * 4);
355 __m256d vb = _mm256_loadu_pd(b + i * 4);
356 vsum = _mm256_fmadd_pd(va, vb, vsum); // FMA: vsum += va * vb
357 }
358 // Horizontal sum
359 __m128d hi = _mm256_extractf128_pd(vsum, 1);
360 __m128d lo = _mm256_castpd256_pd128(vsum);
361 __m128d s2 = _mm_add_pd(lo, hi);
362 __m128d s1 = _mm_add_pd(s2, _mm_shuffle_pd(s2, s2, 1));
363 result = _mm_cvtsd_f64(s1);
364 }
365 for (std::size_t i = nv * 4; i < n; ++i)
366 result += a[i] * b[i];
367#elif defined(OPENSWMM_SIMD_NEON)
368 const std::size_t nv = n / 2;
369 if (nv > 0) {
370 float64x2_t vsum = vdupq_n_f64(0.0);
371 for (std::size_t i = 0; i < nv; ++i) {
372 float64x2_t va = vld1q_f64(a + i * 2);
373 float64x2_t vb = vld1q_f64(b + i * 2);
374 vsum = vfmaq_f64(vsum, va, vb); // FMA: vsum += va * vb
375 }
376 // Horizontal sum: add both lanes
377 result = vgetq_lane_f64(vsum, 0) + vgetq_lane_f64(vsum, 1);
378 }
379 for (std::size_t i = nv * 2; i < n; ++i)
380 result += a[i] * b[i];
381#else
382 for (std::size_t i = 0; i < n; ++i)
383 result += a[i] * b[i];
384#endif
385 return result;
386}
387
392constexpr std::size_t lane_width() noexcept {
393 return OPENSWMM_SIMD_WIDTH;
394}
395
405inline void sqrt_array(const double* OPENSWMM_RESTRICT a, double* OPENSWMM_RESTRICT dst,
406 std::size_t n) noexcept {
408 for (std::size_t i = 0; i < n; ++i)
409 dst[i] = std::sqrt(a[i]);
410}
411
421inline void fabs_array(const double* OPENSWMM_RESTRICT a, double* OPENSWMM_RESTRICT dst,
422 std::size_t n) noexcept {
424 for (std::size_t i = 0; i < n; ++i)
425 dst[i] = std::fabs(a[i]);
426}
427
439inline void fma_array(const double* OPENSWMM_RESTRICT a, const double* OPENSWMM_RESTRICT b,
440 const double* OPENSWMM_RESTRICT c, double* OPENSWMM_RESTRICT dst,
441 std::size_t n) noexcept {
443 for (std::size_t i = 0; i < n; ++i)
444 dst[i] = a[i] * b[i] + c[i];
445}
446
447} /* namespace openswmm::simd */
448
449// ============================================================================
450// Fast math: fixed-exponent pow() replacements using std::sqrt / std::cbrt
451//
452// std::pow(x, y) dispatches through exp(y*log(x)) — ~60-80 cycles on ARM.
453// std::sqrt / std::cbrt are ~10-15 cycle intrinsics. For exponents that
454// factor through half- and third-powers these closed forms are dramatically
455// cheaper in the hot loops (Manning friction, weir/orifice equations,
456// submergence corrections).
457// ============================================================================
458
460
467inline double pow3_2(double x) noexcept {
468 return x * std::sqrt(x);
469}
470
477inline double pow5_2(double x) noexcept {
478 return x * x * std::sqrt(x);
479}
480
487inline double pow5_3(double x) noexcept {
488 return x * std::cbrt(x * x);
489}
490
497inline double pow4_3(double x) noexcept {
498 return x * std::cbrt(x);
499}
500
507inline double pow2_3(double x) noexcept {
508 return std::cbrt(x * x);
509}
510
511#if defined(OPENSWMM_SIMD_NEON)
512// ============================================================================
513// NEON 2-wide vectorised cube root via Newton-Raphson refinement.
514//
515// Seeding strategy: cast to float32x2, use single-precision cbrt via
516// arithmetic refinement, then widen back to float64x2 for three
517// double-precision refinement passes. This implementation has no formal
518// error bound; callers must qualify its accuracy over their input range.
519//
520// Used by batch_pow4_3 and batch_pow2_3 to compute Manning exponents on
521// pairs of hydraulic-radius values without calling std::cbrt twice.
522// ============================================================================
523
532inline float64x2_t vcbrtq_f64(float64x2_t x) noexcept {
533 // --- Step 1: float32 seed ---
534 // Narrow to float32, compute a rough cbrt via bit-manipulation seed
535 // (the classic Kahan integer cbrt trick generalised to float):
536 // ieee754(float) cbrt seed: reinterpret as int32, scale exponent by 1/3.
537 float32x2_t xf = vcvt_f32_f64(x); // narrow to float32
538 int32x2_t xi = vreinterpret_s32_f32(xf);
539 // Magic constant for float cbrt seed: (1/3) * (2^23) * (2 - 127/3 + bias)
540 // = 0x2A2A2A2B (approx). Gives ~5-bit accurate seed.
541 xi = vadd_s32(vshr_n_s32(vsub_s32(xi, vdup_n_s32(0x3F800000)), 2),
542 vdup_n_s32(0x3F800000 + (0x3F800000 / 3)));
543 float32x2_t cr = vreinterpret_f32_s32(xi);
544
545 // --- Step 2: two float32 NR steps: cr = cr - (cr - x/cr²) / 3 ---
546 float32x2_t cr2 = vmul_f32(cr, cr);
547 cr = vadd_f32(cr, vmul_f32(vdup_n_f32(0.333333333f), vsub_f32(vdiv_f32(xf, cr2), cr)));
548 cr2 = vmul_f32(cr, cr);
549 cr = vadd_f32(cr, vmul_f32(vdup_n_f32(0.333333333f), vsub_f32(vdiv_f32(xf, cr2), cr)));
550
551 // --- Step 3: widen seed to double and run three double-precision NR ---
552 float64x2_t c = vcvt_f64_f32(cr); // widen to float64
553
554 // NR: c_next = c - (c³ - x) / (3 c²)
555 // = (2c³ + x) / (3c²) [Horner-friendly form]
556 float64x2_t three = vdupq_n_f64(3.0);
557 float64x2_t onethird = vdupq_n_f64(0.333333333333333333);
558 for (int i = 0; i < 3; ++i) {
559 float64x2_t c2 = vmulq_f64(c, c);
560 float64x2_t c3 = vmulq_f64(c2, c);
561 // c = c - (c³ - x) / (3 * c²)
562 c = vsubq_f64(c, vmulq_f64(onethird, vdivq_f64(vsubq_f64(c3, x), vmulq_f64(three, c2))));
563 }
564 return c;
565}
566
574inline float64x2_t batch_pow4_3(float64x2_t x) noexcept {
575 return vmulq_f64(x, vcbrtq_f64(x));
576}
577
585inline float64x2_t batch_pow2_3(float64x2_t x) noexcept {
586 return vcbrtq_f64(vmulq_f64(x, x));
587}
588
589#endif /* OPENSWMM_SIMD_NEON */
590
591} /* namespace openswmm::fastmath */
592
593#endif /* OPENSWMM_ENGINE_SIMD_HPP */
#define OPENSWMM_RESTRICT
Definition KernelPointers.hpp:9
#define OPENSWMM_IVDEP
Definition SIMD.hpp:77
#define OPENSWMM_SIMD_WIDTH
Scalar fallback.
Definition SIMD.hpp:101
Definition SIMD.hpp:459
double pow4_3(double x) noexcept
Compute the real 4/3 power using square or cube roots.
Definition SIMD.hpp:497
double pow3_2(double x) noexcept
Compute the real 3/2 power using square or cube roots.
Definition SIMD.hpp:467
double pow2_3(double x) noexcept
Compute the real 2/3 power using square or cube roots.
Definition SIMD.hpp:507
double pow5_2(double x) noexcept
Compute the real 5/2 power using square or cube roots.
Definition SIMD.hpp:477
double pow5_3(double x) noexcept
Compute the real 5/3 power using square or cube roots.
Definition SIMD.hpp:487
Definition SIMD.hpp:123
constexpr std::size_t lane_width() noexcept
Report the compile-time number of doubles per SIMD register.
Definition SIMD.hpp:392
void multiply(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Compute dst[i] = a[i] * b[i] over n doubles.
Definition SIMD.hpp:177
double min(const double *a, std::size_t n) noexcept
Reduce an array to its minimum value.
Definition SIMD.hpp:211
double max(const double *a, std::size_t n) noexcept
Reduce an array to its maximum value.
Definition SIMD.hpp:257
void sqrt_array(const double *OPENSWMM_RESTRICT a, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Compute dst[i] = sqrt(a[i]) over n doubles.
Definition SIMD.hpp:405
void fabs_array(const double *OPENSWMM_RESTRICT a, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Compute dst[i] = fabs(a[i]) over n doubles.
Definition SIMD.hpp:421
void add(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Compute dst[i] = a[i] + b[i] over n doubles.
Definition SIMD.hpp:139
double dot(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, std::size_t n) noexcept
Sum corresponding products from two arrays.
Definition SIMD.hpp:346
void fma_array(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, const double *OPENSWMM_RESTRICT c, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Compute dst[i] = a[i] * b[i] + c[i] over n doubles.
Definition SIMD.hpp:439
void clamp(double *a, double lo, double hi, std::size_t n) noexcept
Clamp each array element in place to an inclusive interval.
Definition SIMD.hpp:303