Skip to content

Commit 482fab9

Browse files
authored
Merge pull request #588 from andrewkern/simd_dnorm
SIMD optimization for dnorm()
2 parents d419669 + f16e880 commit 482fab9

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

eidos/eidos_functions_distributions.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "gsl_linalg.h"
2727
#include "gsl_errno.h"
2828
#include "gsl_cdf.h"
29+
#include "eidos_simd.h"
2930

3031

3132
// BCH 20 October 2016: continuing to try to fix problems with gcc 5.4.0 on Linux without breaking other
@@ -484,10 +485,26 @@ EidosValue_SP Eidos_ExecuteFunction_dnorm(const std::vector<EidosValue_SP> &p_ar
484485
EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(num_quantiles);
485486
result_SP = EidosValue_SP(float_result);
486487

488+
#ifdef _OPENMP
487489
EIDOS_THREAD_COUNT(gEidos_OMP_threads_DNORM_1);
488490
#pragma omp parallel for schedule(static) default(none) shared(num_quantiles) firstprivate(float_data, float_result, mu0, sigma0) if(num_quantiles >= EIDOS_OMPMIN_DNORM_1) num_threads(thread_count)
489491
for (int value_index = 0; value_index < num_quantiles; ++value_index)
490492
float_result->set_float_no_check(gsl_ran_gaussian_pdf(float_data[value_index] - mu0, sigma0), value_index);
493+
#else
494+
// SIMD-optimized version: batch the exp() calls
495+
double norm = 1.0 / (std::sqrt(2.0 * M_PI) * sigma0);
496+
double inv_2var = -1.0 / (2.0 * sigma0 * sigma0);
497+
double *result_data = float_result->data_mutable();
498+
499+
for (int value_index = 0; value_index < num_quantiles; ++value_index)
500+
{
501+
double diff = float_data[value_index] - mu0;
502+
result_data[value_index] = diff * diff * inv_2var;
503+
}
504+
Eidos_SIMD::exp_float64(result_data, result_data, num_quantiles);
505+
for (int value_index = 0; value_index < num_quantiles; ++value_index)
506+
result_data[value_index] *= norm;
507+
#endif
491508
}
492509
else
493510
{
@@ -497,6 +514,7 @@ EidosValue_SP Eidos_ExecuteFunction_dnorm(const std::vector<EidosValue_SP> &p_ar
497514

498515
bool saw_error = false;
499516

517+
#ifdef _OPENMP
500518
EIDOS_THREAD_COUNT(gEidos_OMP_threads_DNORM_2);
501519
#pragma omp parallel for schedule(static) default(none) shared(num_quantiles) firstprivate(float_data, float_result, mu_singleton, sigma_singleton, mu0, sigma0, arg_mu, arg_sigma) reduction(||: saw_error) if(num_quantiles >= EIDOS_OMPMIN_DNORM_2) num_threads(thread_count)
502520
for (int value_index = 0; value_index < num_quantiles; ++value_index)
@@ -512,6 +530,35 @@ EidosValue_SP Eidos_ExecuteFunction_dnorm(const std::vector<EidosValue_SP> &p_ar
512530

513531
float_result->set_float_no_check(gsl_ran_gaussian_pdf(float_data[value_index] - mu, sigma), value_index);
514532
}
533+
#else
534+
// SIMD-optimized version: batch the exp() calls
535+
double *result_data = float_result->data_mutable();
536+
std::vector<double> norms(num_quantiles);
537+
538+
// Pass 1: compute exponents and norms, check for errors
539+
for (int value_index = 0; value_index < num_quantiles; ++value_index)
540+
{
541+
double mu = (mu_singleton ? mu0 : arg_mu->NumericAtIndex_NOCAST(value_index, nullptr));
542+
double sigma = (sigma_singleton ? sigma0 : arg_sigma->NumericAtIndex_NOCAST(value_index, nullptr));
543+
544+
if (sigma <= 0.0)
545+
{
546+
saw_error = true;
547+
continue;
548+
}
549+
550+
double diff = float_data[value_index] - mu;
551+
result_data[value_index] = -diff * diff / (2.0 * sigma * sigma);
552+
norms[value_index] = 1.0 / (std::sqrt(2.0 * M_PI) * sigma);
553+
}
554+
555+
// Pass 2: batch exp() - SIMD accelerated
556+
Eidos_SIMD::exp_float64(result_data, result_data, num_quantiles);
557+
558+
// Pass 3: scale by per-element norms
559+
for (int value_index = 0; value_index < num_quantiles; ++value_index)
560+
result_data[value_index] *= norms[value_index];
561+
#endif
515562

516563
if (saw_error)
517564
EIDOS_TERMINATION << "ERROR (Eidos_ExecuteFunction_dnorm): function dnorm() requires sd > 0.0." << EidosTerminate(nullptr);

simd_benchmarks/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This directory contains benchmark scripts used during the development of SIMD op
1010

1111
- **`slim_benchmark.slim`** - SLiM simulation benchmark (N=5000, 1Mb chromosome, 5000 generations with selection) for measuring overall simulation performance.
1212

13+
- **`dnorm_benchmark.eidos`** - Eidos script that benchmarks the SIMD-optimized `dnorm()` function with singleton and vector mu/sigma parameters.
14+
1315
- **`SIMD_BUILD_FLAGS.md`** - Documentation on how SIMD and SLEEF build flags are set and interact.
1416

1517
For SLEEF header generation scripts and documentation, see `eidos/sleef/`.
@@ -88,4 +90,28 @@ Speedup: .96x
8890
Benchmark complete
8991
============================================
9092
```
91-
so the takeaway is that SIMD provided significant speedups for eidos math functions, while the overall SLiM simulation speedup was minimal in this specific benchmark scenario.
93+
so the takeaway is that SIMD provided significant speedups for eidos math functions, while the overall SLiM simulation speedup was minimal in this specific benchmark scenario.
94+
95+
## dnorm() Benchmark Results
96+
97+
The `dnorm()` function was optimized to batch `exp()` calls using SLEEF SIMD vectorization. Results on x86_64 with AVX2 (N=1,000,000 elements, 10 iterations):
98+
99+
| Case | SIMD (M elem/s) | Scalar (M elem/s) | Speedup |
100+
|------|-----------------|-------------------|---------|
101+
| `dnorm(x, scalar, scalar)` | 119.9 | 43.7 | **2.74x** |
102+
| `dnorm(x, scalar, vector)` | 52.5 | 33.5 | **1.57x** |
103+
| `dnorm(x, vector, scalar)` | 56.8 | 34.3 | **1.66x** |
104+
| `dnorm(x, vector, vector)` | 41.7 | 28.2 | **1.48x** |
105+
106+
The single mu/sigma case shows the best speedup at 2.74x. Variable parameter cases have additional overhead from per-element lookups but still benefit from batched SIMD exp().
107+
108+
To run this benchmark:
109+
```bash
110+
# Build with SIMD
111+
mkdir build && cd build && cmake .. && make eidos
112+
./eidos ../simd_benchmarks/dnorm_benchmark.eidos
113+
114+
# Build without SIMD for comparison
115+
mkdir build_nosimd && cd build_nosimd && cmake .. -DUSE_SIMD=OFF && make eidos
116+
./eidos ../simd_benchmarks/dnorm_benchmark.eidos
117+
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Benchmark dnorm() SIMD optimization
2+
// Tests both singleton and variable mu/sigma cases
3+
4+
defineGlobal("N", 1000000);
5+
defineGlobal("ITERS", 10);
6+
7+
// Generate test data
8+
x = rnorm(N, 0, 1);
9+
10+
catn("dnorm() SIMD Benchmark");
11+
catn(" N = " + N + ", iterations = " + ITERS);
12+
catn("");
13+
14+
// Path 1: singleton mu and sigma (most common case)
15+
start = clock();
16+
for (i in 1:ITERS)
17+
result = dnorm(x, 0.0, 1.0);
18+
elapsed = clock() - start;
19+
catn("Path 1 - dnorm(x, scalar, scalar):");
20+
catn(" Time: " + format("%.3f", elapsed) + "s for " + ITERS + " iterations");
21+
catn(" Throughput: " + format("%.1f", N * ITERS / elapsed / 1e6) + " M elements/sec");
22+
catn("");
23+
24+
// Path 2a: variable sigma only
25+
sigmas = rep(1.0, N) + runif(N, -0.1, 0.1);
26+
start = clock();
27+
for (i in 1:ITERS)
28+
result = dnorm(x, 0.0, sigmas);
29+
elapsed = clock() - start;
30+
catn("Path 2a - dnorm(x, scalar, vector):");
31+
catn(" Time: " + format("%.3f", elapsed) + "s for " + ITERS + " iterations");
32+
catn(" Throughput: " + format("%.1f", N * ITERS / elapsed / 1e6) + " M elements/sec");
33+
catn("");
34+
35+
// Path 2b: variable mu only
36+
mus = runif(N, -0.1, 0.1);
37+
start = clock();
38+
for (i in 1:ITERS)
39+
result = dnorm(x, mus, 1.0);
40+
elapsed = clock() - start;
41+
catn("Path 2b - dnorm(x, vector, scalar):");
42+
catn(" Time: " + format("%.3f", elapsed) + "s for " + ITERS + " iterations");
43+
catn(" Throughput: " + format("%.1f", N * ITERS / elapsed / 1e6) + " M elements/sec");
44+
catn("");
45+
46+
// Path 2c: variable mu and sigma
47+
start = clock();
48+
for (i in 1:ITERS)
49+
result = dnorm(x, mus, sigmas);
50+
elapsed = clock() - start;
51+
catn("Path 2c - dnorm(x, vector, vector):");
52+
catn(" Time: " + format("%.3f", elapsed) + "s for " + ITERS + " iterations");
53+
catn(" Throughput: " + format("%.1f", N * ITERS / elapsed / 1e6) + " M elements/sec");

0 commit comments

Comments
 (0)