How would you convert a "while" iterator into simd instructions?

Viewed 223

This is the code I actually had (for a scalar code) which I've replicated (x4) storing data into simd:

waveTable *waveTables[4];
for (int i = 0; i < 4; i++) {
    int waveTableIindex = 0;
    while ((phaseIncrement[i] >= mWaveTables[waveTableIindex].mTopFreq) && (waveTableIindex < kNumWaveTableSlots)) {
        waveTableIindex++;
    }
    waveTables[i] = &mWaveTables[waveTableIindex];
}

Its not "faster" at all, of course. How would you do the same with simd, saving cpu? Any tips/starting point? I'm with SSE2.

Here's the context of the computation. topFreq for each wave table are calculated starting from the max harmonic amounts (x2, due to Nyquist), and multiply for 2 on every wave table (dividing later the number of harmonics available for each table):

double topFreq = 1.0 / (maxHarmonic * 2);
while (maxHarmonic) {
    // fill the table in with the needed harmonics
    // ... makeWaveTable() code
    
    // prepare for next table
    topFreq *= 2;
    maxHarmonic >>= 1;
}

Than, on processing, for each sample, I need to "catch" the correct wave table to use, due to the osc's freq (i.e. phase increment):

freq = clamp(freq, 20.0f, 22050.0f);
phaseIncrement = freq * vSampleTime;

so, for example (having vSampleTime = 1/44100, maxHarmonic = 500), 30hz is wavetable 0, 50hz is wavetable 1, and so on

3 Answers

Assuming your values are FP32, I would do it like this. Untested.

const __m128 phaseIncrements = _mm_loadu_ps( phaseIncrement );
__m128i indices = _mm_setzero_si128();
__m128i activeIndices = _mm_set1_epi32( -1 );

for( size_t idx = 0; idx < kNumWaveTableSlots; idx++ )
{
    // Broadcast the mTopFreq value into FP32 vector. If you build this for AVX1, will become 1 very fast instruction.
    const __m128 topFreq = _mm_set1_ps( mWaveTables[ idx ].mTopFreq );
    // Compare for phaseIncrements >= topFreq
    const __m128 cmp_f32 = _mm_cmpge_ps( phaseIncrements, topFreq );
    // The following line compiles into no instruction, it's only to please the type checker
    __m128i cmp = _mm_castps_si128( cmp_f32 );
    // Bitwise AND with activeIndices
    cmp = _mm_and_si128( cmp, activeIndices );
    // The following line increments the indices vector by 1, only the lanes where cmp was TRUE
    indices = _mm_sub_epi32( indices, cmp );
    // Update the set of active lane indices
    activeIndices = cmp;
    // The vector may become completely zero, meaning all 4 lanes have encountered at least 1 value where topFreq < phaseIncrements
    if( 0 == _mm_movemask_epi8( activeIndices ) )
        break;
}

// Indices vector keeps 4 32-bit integers
// Each lane contains index of the first table entry less than the corresponding lane of phaseIncrements
// Or maybe kNumWaveTableSlots if not found

There is no standard way to write SIMD instructions in C++. A compiler may produce SIMD instructions when appropriate as long as you've configured it to target a CPU that supports such instructions and enabled relevant optimisations. You can use standard algorithms using the std::execution::unsequenced_policy to help compiler understand that SIMD is appropriate.

Related