Изучение БПФ - Почему это не быстро?

Я не уверен, если это больше математика или больше вопросов программирования. Если это математика, пожалуйста, скажи мне.

Я знаю, что есть много готовых к использованию бесплатных проектов FFT. Но я пытаюсь понять метод БПФ. Просто для удовольствия и для изучения. Поэтому я сделал оба алгоритма - DFT и FFT, чтобы сравнить их.

Но у меня проблема с моим БПФ. Кажется, нет большой разницы в эффективности. Мой FFT только немного быстрее, чем DFT (в некоторых случаях это в два раза быстрее, но это максимальное ускорение)

В большинстве статей о БПФ есть кое-что о инверсии битов. Но я не вижу причины использовать бит-реверс. Вероятно, это так. Я не понимаю это Пожалуйста, помогите мне. Что я делаю не так?

Это мой код (вы можете скопировать его здесь и посмотреть, как он работает - онлайн-компилятор):

#include <complex>
#include <iostream>
#include <math.h>
#include <cmath>
#include <vector>
#include <chrono>
#include <ctime>

float _Pi = 3.14159265;
float sampleRate = 44100;
float resolution = 4;
float _SRrange = sampleRate / resolution; // I devide Sample Rate to make the loop smaller,
                                          //just to perform tests faster
float bufferSize = 512;

// Clock class is for measure time to execute whole loop:
class Clock
{
    public:
        Clock() { start = std::chrono::high_resolution_clock::now(); }
        ~Clock() {}

        float secondsElapsed()
        {
            auto stop = std::chrono::high_resolution_clock::now();
            return std::chrono::duration_cast<std::chrono::microseconds>(stop - start).count();
        }
        void reset() { start = std::chrono::high_resolution_clock::now(); }

    private: 
        std::chrono::time_point<std::chrono::high_resolution_clock> start;
};


// Function to calculate magnitude of complex number:
float _mag_Hf(std::complex<float> sf);

// Function to calculate exp(-j*2*PI*n*k / sampleRate) - where "j" is imaginary number:
std::complex<float> _Wnk_Nc(float n, float k);

// Function to calculate exp(-j*2*PI*k / sampleRate):
std::complex<float> _Wk_Nc(float k);



int main() {
  float scaleFFT = 512; // devide and conquere - if it's "1" then whole algorhitm is just simply DFT
            // I wonder what is the maximum of that value. I alvays thought it should be equal to
            // buffer size (number o samples) but above some value it start to work slower then DFT

  std::vector<float> inputSignal; // array of input signal
  inputSignal.resize(bufferSize); // how many sample we will use to calculate Fourier Transform

  std::vector<std::complex<float>> _Sf; // array to store Fourier Transform value for each measured frequency bin
  _Sf.resize(scaleFFT); // resize it to size which we need.

  std::vector<std::complex<float>> _Hf_Db_vect; //array to store magnitude (in logarythmic dB scale)            
                                                //for each measured frequency bin
  _Hf_Db_vect.resize(_SRrange); //resize it to make it able to store value for each measured freq value

  std::complex<float> _Sf_I_half; // complex to calculate first half of freq range
                                  // from 1 to Nyquist  (sampleRate/2)

  std::complex<float> _Sf_II_half; // complex to calculate second half of freq range
                                   //from Nyquist to sampleRate



        for(int i=0; i<(int)_Sf.size(); i++)
            inputSignal[i]  = cosf((float)i/_Pi); // fill the input signal with some data, no matter


  Clock _time; // Start measure time

for(int freqBinK=0; freqBinK < _SRrange/2; freqBinK++) // start calculate all freq (devide by 2 for two halves)
    {
        for(int i=0; i<(int)_Sf.size(); i++) _Sf[i]  = 0.0f; // clean all values, for next loop we need all values to be zero

        for (int n=0; n<bufferSize/_Sf.size(); ++n) // Here I take all samples in buffer
        {
            std::complex<float> _W = _Wnk_Nc(_Sf.size()*(float)n, freqBinK);

            for(int i=0; i<(int)_Sf.size(); i++) // Finally here is my devide and conquer
                _Sf[i]  += inputSignal[_Sf.size()*n  +i] * _W; // And I see no reason to use any bit reversal, how it shoul be????
        }

        std::complex<float> _Wk = _Wk_Nc(freqBinK);

        _Sf_I_half = 0.0f;
        _Sf_II_half = 0.0f;

        for(int z=0; z<(int)_Sf.size()/2; z++) // here I calculate Fourier transform for each freq
        {
            _Sf_I_half += _Wk_Nc(2.0f * (float)z * freqBinK) * (_Sf[2*z]  + _Wk * _Sf[2*z+1]); // First half - to Nyquist
            _Sf_II_half += _Wk_Nc(2.0f * (float)z *freqBinK) * (_Sf[2*z]  - _Wk * _Sf[2*z+1]); // Second half - to SampleRate
            // also don't see need to use reversal bit, where it shoul be??? :)
        }

        // Calculate magnitude in dB scale
        _Hf_Db_vect[freqBinK] = _mag_Hf(_Sf_I_half); // First half
        _Hf_Db_vect[freqBinK + _SRrange/2] = _mag_Hf(_Sf_II_half); // Second half
    }
  std::cout << _time.secondsElapsed() << std::endl; // time measuer after execution of whole loop
}


float _mag_Hf(std::complex<float> sf)
{
float _Re_2;
float _Im_2;
    _Re_2 = sf.real() * sf.real();
    _Im_2 = sf.imag() * sf.imag();
    return 20*log10(pow(_Re_2 + _Im_2, 0.5f)); //transform magnitude to logarhytmic dB scale
}

std::complex<float> _Wnk_Nc(float n, float k)
{
    std::complex<float> _Wnk_Ncomp;
    _Wnk_Ncomp.real(cosf(-2.0f * _Pi * (float)n * k / sampleRate));
    _Wnk_Ncomp.imag(sinf(-2.0f * _Pi * (float)n * k / sampleRate));
    return _Wnk_Ncomp;
}

std::complex<float> _Wk_Nc(float k)
{
    std::complex<float> _Wk_Ncomp;
    _Wk_Ncomp.real(cosf(-2.0f * _Pi * k / sampleRate));
    _Wk_Ncomp.imag(sinf(-2.0f * _Pi * k / sampleRate));
    return _Wk_Ncomp;
}

3 ответа

Одна огромная ошибка, которую вы делаете, это вычисление веса бабочки (которое включает в себя sin а также cos) на лету (в _Wnk_Nc()). sin а также cos как правило, стоимость от 10 до 100 с тактов, в то время как другие операции типа "бабочка" - просто "множитель" и "сложение", которые занимают всего несколько циклов, отсюда и необходимость их учитывать. Все быстрые реализации FFT делают это как часть шага инициализации (обычно называемого "созданием плана" или подобным). Смотрите, например, FFTW и KissFFT.

Кроме вышеупомянутой оптимизации "предварительного расчета весов бабочек", большинство реализаций FFT также используют SIMD инструкции по векторизации кода.

// также не вижу необходимости использовать бит разворота, где он должен быть?

Самая первая петля бабочки должна быть проиндексирована обратным битом. Эти индексы обычно рассчитываются внутри рекурсии, но для расчета по циклу эти индексы также являются дорогостоящими, поэтому их лучше предварительно рассчитать в плане.

Сочетание этих подходов к оптимизации приводит к ускорению примерно в 100 раз

В большинстве быстрых реализаций FFT либо используется таблица поиска предварительно вычисленных коэффициентов твида, либо простая рекурсия для вращения коэффициентов твида на лету вместо вызова тригонометрических функций библиотеки математики внутри внутреннего цикла FFT.

Для больших БПФ использование рекурсивной формулы триггера с меньшей вероятностью разрушает кэш данных на современных процессорах.

Другие вопросы по тегам