I've been writing up code to test an alternate way of calculating the Binomial probability mass function. The Binomial random variable is the number of heads out of N coin tosses with coins that have probability p of being heads.
The algorithm is pretty simple, we loop through the N coin tosses and update the probability that there are k successes as:
pmf[k] = pmf[k-1] * p + pmf[k] * (1 - p)
Since we know pmf[k] = 0
for k > n
(there can't be more successes than coins tossed so far), we evaluate this line N(N+1)/2 times.
The algorithm works well, is exact, and avoids a number of common pitfalls when computing the Binomial distribution. This is the best implementation I've managed so far, which takes ~190 ms to run on my Dell XPS-15 for N = 15000
and long double p = 0.5
.
template <typename T>
std::vector<T> ComputePmf(T p, unsigned int N) {
std::vector<T> pmf(N + 1, 0.0);
pmf[0] = 1.0;
auto k = 0;
for (auto n = 0; n < N; n++) {
for (k = n + 1; k > 0; --k) {
pmf[k] += p * (pmf[k - 1] - pmf[k]);
}
pmf[0] *= (1 - p);
}
return pmf;
}
Ordinarily I'd hang up my hat and move on, but when I implemented the same algorithm in Julia it ran in 150 ms! Since then I've been stuck trying to figure out how to make my c++ version faster - I've tried:
- using different data types (
long double
works best, maybe due to SIMD alignment?), std::array
versusstd::vector
(no difference),- other ways of factoring the inner loop (this is the best I've come up with),
- clang and gcc compilers (gcc does slightly better),
It seems like the only real information that I'm not exploiting is the very sequential nature of the data access in this algorithm. Any suggestions?
long double
was best, it prevents SIMD from being used (Clang doesn't seem too keen on auto-vectorizing this anyway though) \$\endgroup\$double
takes ~7 times longer (~1.4 seconds). Maybe this hints at the problem? \$\endgroup\$float
is even slower. Most of these values are very close to zero, some eventually round to zero even with long double values. \$\endgroup\$