Putting this through the built-in profiler reveals some hot spots. Perhaps surprisingly: ReverseBits
. It's not the biggest thing in the list, but it is significant while it shouldn't be.
You could use one of the many alternate ways to implement ReverseBits
, or the sequence of bit-reversed indexes (which does not require reversing all the indexes), or the overall bit-reversal permutation (which does not require bit reversals).
For example here is a way to compute the sequence of bit-reversed indexes without explicitly reversing any index:
for (size_t n = 0, rev = 0; n < N; ++n)
{
X[n] = x[rev];
size_t change = n ^ (n + 1);
rev ^= change << (__lzcnt64(change) - (64 - stages));
}
On my PC, that reduces the time from around 2.8 million microseconds to 2.3 million microseconds.
This trick works by using that the XOR between adjacent indexes is a mask of ones up to and including the least significant zero (the +1 borrows through the least significant set bits and into that least significant zero), which has a form that can be reversed by just shifting it. The reversed mask is then the XOR between adjacent reversed indexes, so applying it to the current reversed index with XOR increments it.
__lzcnt64
is the MSVC intrinsic, you could use some preprocessor tricks to find the right intrinsic for the current compiler.
Secondly, there is a repeated expression: W[n * W_offset] * X[k + n + N_stage / 2]
. The compiler is often relied on to remove such duplication, but here it didn't happen. Factoring that out reduced the time to under 2 million microseconds.