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
49
50#ifndef OPENSWMM_ENGINE_SIMD_HPP
51#define OPENSWMM_ENGINE_SIMD_HPP
52
53#ifndef OPENSWMM_RESTRICT
54# if defined(_MSC_VER)
55# define OPENSWMM_RESTRICT __restrict
56# else
57# define OPENSWMM_RESTRICT __restrict__
58# endif
59#endif
60
61// ============================================================================
62// Cross-compiler vectorisation hint
63// ============================================================================
64
65#ifndef OPENSWMM_IVDEP
66# if defined(_MSC_VER)
67# define OPENSWMM_IVDEP __pragma(loop(ivdep))
68# elif defined(__GNUC__) || defined(__clang__)
69# define OPENSWMM_IVDEP _Pragma("GCC ivdep")
70# else
71# define OPENSWMM_IVDEP
72# endif
73#endif
74
75#include <cstddef>
76#include <cmath>
77#include <algorithm>
78#include <cassert>
79
80// ============================================================================
81// Platform detection
82// ============================================================================
83
84#if !defined(OPENSWMM_SIMD_SCALAR)
85# if defined(__AVX2__)
86# include <immintrin.h>
87# define OPENSWMM_SIMD_AVX2 1
88# define OPENSWMM_SIMD_WIDTH 4
89# elif defined(__ARM_NEON) || defined(__aarch64__)
90# include <arm_neon.h>
91# define OPENSWMM_SIMD_NEON 1
92# define OPENSWMM_SIMD_WIDTH 2
93# else
94# define OPENSWMM_SIMD_SCALAR 1
95# define OPENSWMM_SIMD_WIDTH 1
96# endif
97#else
98# if !defined(OPENSWMM_SIMD_WIDTH)
99# define OPENSWMM_SIMD_WIDTH 1
100# endif
101#endif
102
103// Exactly one SIMD backend must be active
104static_assert(
105 (0
106#if defined(OPENSWMM_SIMD_AVX2)
107 + 1
108#endif
109#if defined(OPENSWMM_SIMD_NEON)
110 + 1
111#endif
112#if defined(OPENSWMM_SIMD_SCALAR)
113 + 1
114#endif
115 ) == 1,
116 "Exactly one SIMD backend must be active (AVX2, NEON, or SCALAR)"
117);
118
119namespace openswmm::simd {
120
121// ============================================================================
122// Element-wise operations (arrays of doubles)
123// ============================================================================
124
135inline void add(
136 const double* OPENSWMM_RESTRICT a,
137 const double* OPENSWMM_RESTRICT b,
138 double* OPENSWMM_RESTRICT dst,
139 std::size_t n
140) 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) dst[i] = a[i] + b[i];
150 (void)nr;
151#elif defined(OPENSWMM_SIMD_NEON)
152 const std::size_t nv = n / 2;
153 for (std::size_t i = 0; i < nv; ++i) {
154 float64x2_t va = vld1q_f64(a + i * 2);
155 float64x2_t vb = vld1q_f64(b + i * 2);
156 vst1q_f64(dst + i * 2, vaddq_f64(va, vb));
157 }
158 for (std::size_t i = nv * 2; i < n; ++i) dst[i] = a[i] + b[i];
159#else
160 for (std::size_t i = 0; i < n; ++i) dst[i] = a[i] + b[i];
161#endif
162}
163
167inline void multiply(
168 const double* OPENSWMM_RESTRICT a,
169 const double* OPENSWMM_RESTRICT b,
170 double* OPENSWMM_RESTRICT dst,
171 std::size_t n
172) noexcept {
173#if defined(OPENSWMM_SIMD_AVX2)
174 const std::size_t nv = n / 4;
175 for (std::size_t i = 0; i < nv; ++i) {
176 __m256d va = _mm256_loadu_pd(a + i * 4);
177 __m256d vb = _mm256_loadu_pd(b + i * 4);
178 _mm256_storeu_pd(dst + i * 4, _mm256_mul_pd(va, vb));
179 }
180 for (std::size_t i = nv * 4; i < n; ++i) dst[i] = a[i] * b[i];
181#elif defined(OPENSWMM_SIMD_NEON)
182 const std::size_t nv = n / 2;
183 for (std::size_t i = 0; i < nv; ++i) {
184 float64x2_t va = vld1q_f64(a + i * 2);
185 float64x2_t vb = vld1q_f64(b + i * 2);
186 vst1q_f64(dst + i * 2, vmulq_f64(va, vb));
187 }
188 for (std::size_t i = nv * 2; i < n; ++i) dst[i] = a[i] * b[i];
189#else
190 for (std::size_t i = 0; i < n; ++i) dst[i] = a[i] * b[i];
191#endif
192}
193
201inline double min(const double* a, std::size_t n) noexcept {
202 assert(n >= 1);
203 double result = a[0];
204#if defined(OPENSWMM_SIMD_AVX2)
205 const std::size_t nv = n / 4;
206 if (nv > 0) {
207 __m256d vmin = _mm256_loadu_pd(a);
208 for (std::size_t i = 1; i < nv; ++i) {
209 vmin = _mm256_min_pd(vmin, _mm256_loadu_pd(a + i * 4));
210 }
211 // Horizontal reduction
212 __m128d hi = _mm256_extractf128_pd(vmin, 1);
213 __m128d lo = _mm256_castpd256_pd128(vmin);
214 __m128d m2 = _mm_min_pd(lo, hi);
215 __m128d m1 = _mm_min_pd(m2, _mm_shuffle_pd(m2, m2, 1));
216 result = _mm_cvtsd_f64(m1);
217 }
218 for (std::size_t i = nv * 4; i < n; ++i) result = std::min(result, a[i]);
219#elif defined(OPENSWMM_SIMD_NEON)
220 const std::size_t nv = n / 2;
221 if (nv > 0) {
222 float64x2_t vmin = vld1q_f64(a);
223 for (std::size_t i = 1; i < nv; ++i) {
224 vmin = vminq_f64(vmin, vld1q_f64(a + i * 2));
225 }
226 // Horizontal reduction: min of 2 lanes
227 result = std::min(vgetq_lane_f64(vmin, 0), vgetq_lane_f64(vmin, 1));
228 }
229 for (std::size_t i = nv * 2; i < n; ++i) result = std::min(result, a[i]);
230#else
231 for (std::size_t i = 1; i < n; ++i) result = std::min(result, a[i]);
232#endif
233 return result;
234}
235
239inline double max(const double* a, std::size_t n) noexcept {
240 assert(n >= 1);
241 double result = a[0];
242#if defined(OPENSWMM_SIMD_AVX2)
243 const std::size_t nv = n / 4;
244 if (nv > 0) {
245 __m256d vmax = _mm256_loadu_pd(a);
246 for (std::size_t i = 1; i < nv; ++i) {
247 vmax = _mm256_max_pd(vmax, _mm256_loadu_pd(a + i * 4));
248 }
249 // Horizontal reduction
250 __m128d hi = _mm256_extractf128_pd(vmax, 1);
251 __m128d lo = _mm256_castpd256_pd128(vmax);
252 __m128d m2 = _mm_max_pd(lo, hi);
253 __m128d m1 = _mm_max_pd(m2, _mm_shuffle_pd(m2, m2, 1));
254 result = _mm_cvtsd_f64(m1);
255 }
256 for (std::size_t i = nv * 4; i < n; ++i) result = std::max(result, a[i]);
257#elif defined(OPENSWMM_SIMD_NEON)
258 const std::size_t nv = n / 2;
259 if (nv > 0) {
260 float64x2_t vmax = vld1q_f64(a);
261 for (std::size_t i = 1; i < nv; ++i) {
262 vmax = vmaxq_f64(vmax, vld1q_f64(a + i * 2));
263 }
264 result = std::max(vgetq_lane_f64(vmax, 0), vgetq_lane_f64(vmax, 1));
265 }
266 for (std::size_t i = nv * 2; i < n; ++i) result = std::max(result, a[i]);
267#else
268 for (std::size_t i = 1; i < n; ++i) result = std::max(result, a[i]);
269#endif
270 return result;
271}
272
281inline void clamp(double* a, double lo, double hi, std::size_t n) noexcept {
282#if defined(OPENSWMM_SIMD_AVX2)
283 __m256d vlo = _mm256_set1_pd(lo);
284 __m256d vhi = _mm256_set1_pd(hi);
285 const std::size_t nv = n / 4;
286 for (std::size_t i = 0; i < nv; ++i) {
287 __m256d v = _mm256_loadu_pd(a + i * 4);
288 v = _mm256_max_pd(vlo, _mm256_min_pd(vhi, v));
289 _mm256_storeu_pd(a + i * 4, v);
290 }
291 for (std::size_t i = nv * 4; i < n; ++i) {
292 a[i] = std::max(lo, std::min(hi, a[i]));
293 }
294#elif defined(OPENSWMM_SIMD_NEON)
295 float64x2_t vlo = vdupq_n_f64(lo);
296 float64x2_t vhi = vdupq_n_f64(hi);
297 const std::size_t nv = n / 2;
298 for (std::size_t i = 0; i < nv; ++i) {
299 float64x2_t v = vld1q_f64(a + i * 2);
300 v = vmaxq_f64(vlo, vminq_f64(vhi, v));
301 vst1q_f64(a + i * 2, v);
302 }
303 for (std::size_t i = nv * 2; i < n; ++i) {
304 a[i] = std::max(lo, std::min(hi, a[i]));
305 }
306#else
307 for (std::size_t i = 0; i < n; ++i) {
308 a[i] = std::max(lo, std::min(hi, a[i]));
309 }
310#endif
311}
312
324inline double dot(
325 const double* OPENSWMM_RESTRICT a,
326 const double* OPENSWMM_RESTRICT b,
327 std::size_t n
328) noexcept {
329 double result = 0.0;
330#if defined(OPENSWMM_SIMD_AVX2)
331 const std::size_t nv = n / 4;
332 if (nv > 0) {
333 __m256d vsum = _mm256_setzero_pd();
334 for (std::size_t i = 0; i < nv; ++i) {
335 __m256d va = _mm256_loadu_pd(a + i * 4);
336 __m256d vb = _mm256_loadu_pd(b + i * 4);
337 vsum = _mm256_fmadd_pd(va, vb, vsum); // FMA: vsum += va * vb
338 }
339 // Horizontal sum
340 __m128d hi = _mm256_extractf128_pd(vsum, 1);
341 __m128d lo = _mm256_castpd256_pd128(vsum);
342 __m128d s2 = _mm_add_pd(lo, hi);
343 __m128d s1 = _mm_add_pd(s2, _mm_shuffle_pd(s2, s2, 1));
344 result = _mm_cvtsd_f64(s1);
345 }
346 for (std::size_t i = nv * 4; i < n; ++i) result += a[i] * b[i];
347#elif defined(OPENSWMM_SIMD_NEON)
348 const std::size_t nv = n / 2;
349 if (nv > 0) {
350 float64x2_t vsum = vdupq_n_f64(0.0);
351 for (std::size_t i = 0; i < nv; ++i) {
352 float64x2_t va = vld1q_f64(a + i * 2);
353 float64x2_t vb = vld1q_f64(b + i * 2);
354 vsum = vfmaq_f64(vsum, va, vb); // FMA: vsum += va * vb
355 }
356 // Horizontal sum: add both lanes
357 result = vgetq_lane_f64(vsum, 0) + vgetq_lane_f64(vsum, 1);
358 }
359 for (std::size_t i = nv * 2; i < n; ++i) result += a[i] * b[i];
360#else
361 for (std::size_t i = 0; i < n; ++i) result += a[i] * b[i];
362#endif
363 return result;
364}
365
369constexpr std::size_t lane_width() noexcept {
370 return OPENSWMM_SIMD_WIDTH;
371}
372
378inline void sqrt_array(
379 const double* OPENSWMM_RESTRICT a,
380 double* OPENSWMM_RESTRICT dst,
381 std::size_t n
382) noexcept {
384 for (std::size_t i = 0; i < n; ++i) dst[i] = std::sqrt(a[i]);
385}
386
390inline void fabs_array(
391 const double* OPENSWMM_RESTRICT a,
392 double* OPENSWMM_RESTRICT dst,
393 std::size_t n
394) noexcept {
396 for (std::size_t i = 0; i < n; ++i) dst[i] = std::fabs(a[i]);
397}
398
402inline void fma_array(
403 const double* OPENSWMM_RESTRICT a,
404 const double* OPENSWMM_RESTRICT b,
405 const double* OPENSWMM_RESTRICT c,
406 double* OPENSWMM_RESTRICT dst,
407 std::size_t n
408) noexcept {
410 for (std::size_t i = 0; i < n; ++i) dst[i] = a[i] * b[i] + c[i];
411}
412
413} /* namespace openswmm::simd */
414
415// ============================================================================
416// Fast math: fixed-exponent pow() replacements using std::sqrt / std::cbrt
417//
418// std::pow(x, y) dispatches through exp(y*log(x)) — ~60-80 cycles on ARM.
419// std::sqrt / std::cbrt are ~10-15 cycle intrinsics. For exponents that
420// factor through half- and third-powers these closed forms are dramatically
421// cheaper in the hot loops (Manning friction, weir/orifice equations,
422// submergence corrections).
423// ============================================================================
424
426
428inline double pow3_2(double x) noexcept {
429 return x * std::sqrt(x);
430}
431
433inline double pow5_2(double x) noexcept {
434 return x * x * std::sqrt(x);
435}
436
438inline double pow5_3(double x) noexcept {
439 return x * std::cbrt(x * x);
440}
441
443inline double pow4_3(double x) noexcept {
444 return x * std::cbrt(x);
445}
446
448inline double pow2_3(double x) noexcept {
449 return std::cbrt(x * x);
450}
451
452#if defined(OPENSWMM_SIMD_NEON)
453// ============================================================================
454// NEON 2-wide vectorised cube root via Newton-Raphson refinement.
455//
456// Seeding strategy: cast to float32x2, use single-precision cbrt via
457// vrecpe/NR, widen back to float64x2 for three double-precision NR
458// corrections. Convergence: each NR step cubes the relative error, so
459// after 3 double steps the residual is < 1 ULP for normalised doubles.
460//
461// Used by batch_pow4_3 and batch_pow2_3 to compute Manning exponents on
462// pairs of hydraulic-radius values without calling std::cbrt twice.
463// ============================================================================
464
469inline float64x2_t vcbrtq_f64(float64x2_t x) noexcept {
470 // --- Step 1: float32 seed ---
471 // Narrow to float32, compute a rough cbrt via bit-manipulation seed
472 // (the classic Kahan integer cbrt trick generalised to float):
473 // ieee754(float) cbrt seed: reinterpret as int32, scale exponent by 1/3.
474 float32x2_t xf = vcvt_f32_f64(x); // narrow to float32
475 int32x2_t xi = vreinterpret_s32_f32(xf);
476 // Magic constant for float cbrt seed: (1/3) * (2^23) * (2 - 127/3 + bias)
477 // = 0x2A2A2A2B (approx). Gives ~5-bit accurate seed.
478 xi = vadd_s32(vshr_n_s32(vsub_s32(xi, vdup_n_s32(0x3F800000)), 2),
479 vdup_n_s32(0x3F800000 + (0x3F800000 / 3)));
480 float32x2_t cr = vreinterpret_f32_s32(xi);
481
482 // --- Step 2: two float32 NR steps: cr = cr - (cr - x/cr²) / 3 ---
483 float32x2_t cr2 = vmul_f32(cr, cr);
484 cr = vadd_f32(cr, vmul_f32(vdup_n_f32(0.333333333f),
485 vsub_f32(vdiv_f32(xf, cr2), cr)));
486 cr2 = vmul_f32(cr, cr);
487 cr = vadd_f32(cr, vmul_f32(vdup_n_f32(0.333333333f),
488 vsub_f32(vdiv_f32(xf, cr2), cr)));
489
490 // --- Step 3: widen seed to double and run three double-precision NR ---
491 float64x2_t c = vcvt_f64_f32(cr); // widen to float64
492
493 // NR: c_next = c - (c³ - x) / (3 c²)
494 // = (2c³ + x) / (3c²) [Horner-friendly form]
495 float64x2_t three = vdupq_n_f64(3.0);
496 float64x2_t onethird = vdupq_n_f64(0.333333333333333333);
497 for (int i = 0; i < 3; ++i) {
498 float64x2_t c2 = vmulq_f64(c, c);
499 float64x2_t c3 = vmulq_f64(c2, c);
500 // c = c - (c³ - x) / (3 * c²)
501 c = vsubq_f64(c, vmulq_f64(onethird,
502 vdivq_f64(vsubq_f64(c3, x),
503 vmulq_f64(three, c2))));
504 }
505 return c;
506}
507
512inline float64x2_t batch_pow4_3(float64x2_t x) noexcept {
513 return vmulq_f64(x, vcbrtq_f64(x));
514}
515
520inline float64x2_t batch_pow2_3(float64x2_t x) noexcept {
521 return vcbrtq_f64(vmulq_f64(x, x));
522}
523
524#endif /* OPENSWMM_SIMD_NEON */
525
526} /* namespace openswmm::fastmath */
527
528#endif /* OPENSWMM_ENGINE_SIMD_HPP */
#define OPENSWMM_IVDEP
Definition SIMD.hpp:71
#define OPENSWMM_SIMD_WIDTH
Scalar fallback.
Definition SIMD.hpp:95
#define OPENSWMM_RESTRICT
Definition XSectBatch.hpp:57
Definition SIMD.hpp:425
double pow4_3(double x) noexcept
pow(x, 4/3) = x * cbrt(x). Manning friction.
Definition SIMD.hpp:443
double pow3_2(double x) noexcept
pow(x, 3/2) = x * sqrt(x). Weir TRANSVERSE / TRAPEZOIDAL.
Definition SIMD.hpp:428
double pow2_3(double x) noexcept
pow(x, 2/3) = cbrt(x²). Manning normal-flow.
Definition SIMD.hpp:448
double pow5_2(double x) noexcept
pow(x, 5/2) = x² * sqrt(x). Weir V-NOTCH.
Definition SIMD.hpp:433
double pow5_3(double x) noexcept
pow(x, 5/3) = x * cbrt(x²). Weir SIDEFLOW (legacy 1.67 exponent).
Definition SIMD.hpp:438
Definition SIMD.hpp:119
constexpr std::size_t lane_width() noexcept
Returns the SIMD lane width (doubles per register on this platform).
Definition SIMD.hpp:369
void multiply(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Element-wise multiplication: dst[i] = a[i] * b[i].
Definition SIMD.hpp:167
double min(const double *a, std::size_t n) noexcept
Find the minimum value in an array.
Definition SIMD.hpp:201
double max(const double *a, std::size_t n) noexcept
Find the maximum value in an array.
Definition SIMD.hpp:239
void sqrt_array(const double *OPENSWMM_RESTRICT a, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Element-wise sqrt: dst[i] = sqrt(a[i]). Written as a simple loop; compilers auto-vectorize to platfor...
Definition SIMD.hpp:378
void fabs_array(const double *OPENSWMM_RESTRICT a, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Element-wise fabs: dst[i] = fabs(a[i]).
Definition SIMD.hpp:390
void add(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, double *OPENSWMM_RESTRICT dst, std::size_t n) noexcept
Element-wise addition: dst[i] = a[i] + b[i].
Definition SIMD.hpp:135
double dot(const double *OPENSWMM_RESTRICT a, const double *OPENSWMM_RESTRICT b, std::size_t n) noexcept
Dot product of two arrays: sum(a[i] * b[i]).
Definition SIMD.hpp:324
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
Element-wise fused multiply-add: dst[i] = a[i] * b[i] + c[i].
Definition SIMD.hpp:402
void clamp(double *a, double lo, double hi, std::size_t n) noexcept
Clamp all elements of an array to [lo, hi].
Definition SIMD.hpp:281