Ускорение сокращения FFTW, чтобы избежать массивного заполнения нулями

Предположим, что у меня есть последовательность x(n) длиной K * N и что только первые N элемента отличны от нуля. Я предполагаю, что N << K, скажем, например, N = 10 и K = 100000. Я хочу рассчитать FFT с помощью FFTW такой последовательности. Это эквивалентно последовательности длиной N и дополнению нулями до K * N. Поскольку N и K могут быть "большими", у меня есть значительное заполнение нулями. Я изучаю, могу ли я сэкономить время вычислений, избегая явного заполнения нулями.

Дело K = 2

Начнем с рассмотрения случая K = 2. В этом случае ДПФ x(n) можно записать как

введите здесь описание изображения

Если k четное, а именно k = 2 * m, то

введите здесь описание изображения

что означает, что такие значения ДПФ могут быть вычислены через БПФ последовательности длиной N, а не K * N.

Если k нечетное, а именно k = 2 * m + 1, то

введите здесь описание изображения

что означает, что такие значения ДПФ можно снова вычислить через БПФ последовательности длины N, а не K * N.

Итак, в заключение я могу обменять одно БПФ длиной 2 * N на 2 БПФ длиной N.

Случай произвольного K

В этом случае мы имеем

введите здесь описание изображения

При записи k = m * K + t имеем

введите здесь описание изображения

Итак, в заключение я могу обменять одно БПФ длиной K * N на K БПФ длиной N. Поскольку FFTW имеет fftw_plan_many_dft, я могу ожидать некоторого выигрыша по сравнению со случаем одного БПФ.

Чтобы убедиться в этом, я установил следующий код

#include <stdio.h>
#include <stdlib.h>     /* srand, rand */
#include <time.h>       /* time */
#include <math.h>
#include <fstream>

#include <fftw3.h>

#include "TimingCPU.h"

#define PI_d            3.141592653589793

void main() {

    const int N = 10;
    const int K = 100000;

    fftw_plan plan_zp;

    fftw_complex *h_x = (fftw_complex *)malloc(N     * sizeof(fftw_complex));
    fftw_complex *h_xzp = (fftw_complex *)calloc(N * K, sizeof(fftw_complex));
    fftw_complex *h_xpruning = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhatpruning = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhatpruning_temp = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhat = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));

    // --- Random number generation of the data sequence
    srand(time(NULL));
    for (int k = 0; k < N; k++) {
        h_x[k][0] = (double)rand() / (double)RAND_MAX;
        h_x[k][1] = (double)rand() / (double)RAND_MAX;
    }

    memcpy(h_xzp, h_x, N * sizeof(fftw_complex));

    plan_zp = fftw_plan_dft_1d(N * K, h_xzp, h_xhat, FFTW_FORWARD, FFTW_ESTIMATE);
    fftw_plan plan_pruning = fftw_plan_many_dft(1, &N, K, h_xpruning, NULL, 1, N, h_xhatpruning_temp, NULL, 1, N, FFTW_FORWARD, FFTW_ESTIMATE);

    TimingCPU timerCPU;
    timerCPU.StartCounter();
    fftw_execute(plan_zp);
    printf("Stadard %f\n", timerCPU.GetCounter());

    timerCPU.StartCounter();
    double factor = -2. * PI_d / (K * N);
    for (int k = 0; k < K; k++) {
        double arg1 = factor * k;
        for (int n = 0; n < N; n++) {
            double arg = arg1 * n;
            double cosarg = cos(arg);
            double sinarg = sin(arg);
            h_xpruning[k * N + n][0] = h_x[n][0] * cosarg - h_x[n][1] * sinarg;
            h_xpruning[k * N + n][1] = h_x[n][0] * sinarg + h_x[n][1] * cosarg;
        }
    }
    printf("Optimized first step %f\n", timerCPU.GetCounter());

    timerCPU.StartCounter();
    fftw_execute(plan_pruning);
    printf("Optimized second step %f\n", timerCPU.GetCounter());
    timerCPU.StartCounter();
    for (int k = 0; k < K; k++) {
        for (int p = 0; p < N; p++) {
            h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
            h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
        }
    }
    printf("Optimized third step %f\n", timerCPU.GetCounter());

    double rmserror = 0., norm = 0.;
    for (int n = 0; n < N; n++) {
        rmserror = rmserror + (h_xhatpruning[n][0] - h_xhat[n][0]) * (h_xhatpruning[n][0] - h_xhat[n][0]) + (h_xhatpruning[n][1] - h_xhat[n][1]) * (h_xhatpruning[n][1] - h_xhat[n][1]);
        norm = norm + h_xhat[n][0] * h_xhat[n][0] + h_xhat[n][1] * h_xhat[n][1];
    }
    printf("rmserror %f\n", 100. * sqrt(rmserror / norm));

    fftw_destroy_plan(plan_zp);

}

Разработанный мной подход состоит из трех шагов:

  1. Умножение входной последовательности на комплексные экспоненты «скручивания»;
  2. Выполнение fftw_many;
  3. Реорганизация результатов.

fftw_many быстрее, чем одиночный FFTW на K * N входных точках. Однако шаги №1 и №3 полностью разрушают такой выигрыш. Я ожидаю, что шаги № 1 и № 3 будут намного легче в вычислительном отношении, чем шаг № 2.

Мои вопросы:

  1. Как возможно, что шаги № 1 и № 3 более требовательны к вычислительным ресурсам, чем шаг № 2?
  2. Как я могу улучшить шаги № 1 и № 3, чтобы получить чистую прибыль по сравнению со «стандартным» подходом?

Большое спасибо за любую подсказку.

ИЗМЕНИТЬ

Я работаю с Visual Studio 2013 и компилирую в режиме Release.


person Vitality    schedule 16.11.2016    source источник
comment
Вы скомпилировали свой тестовый код с включенной оптимизацией, например. gcc -O3 ... ?   -  person Paul R    schedule 16.11.2016
comment
@PaulR Я компилирую с помощью Visual Studio 2013 в режиме выпуска. Я отредактировал сообщение соответствующим образом.   -  person Vitality    schedule 16.11.2016
comment
@JackOLantern Вы можете сами выполнять вычисления БПФ в многопоточном режиме. См. fftw.org/doc/. предназначены для. Их использование не является мошенничеством.   -  person Andrew Henle    schedule 16.11.2016


Ответы (3)


Для третьего шага вы можете попробовать изменить порядок циклов:

for (int p = 0; p < N; p++) {
    for (int k = 0; k < K; k++) {
        h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
        h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
    }
}

поскольку, как правило, более выгодно, чтобы адреса хранилища были непрерывными, чем адреса загрузки.

В любом случае у вас есть недружественный к кешу шаблон доступа. Вы можете попробовать работать с блоками, чтобы улучшить это, например. предполагая, что N кратно 4:

for (int p = 0; p < N; p += 4) {
    for (int k = 0; k < K; k++) {
        for (int p0 = 0; p0 < 4; p0++) {
            h_xhatpruning[(p + p0) * K + k][0] = h_xhatpruning_temp[(p + p0) + k * N][0];
            h_xhatpruning[(p + p0) * K + k][1] = h_xhatpruning_temp[(p + p0) + k * N][1];
        }
    }
}

Это должно помочь несколько уменьшить отток строк кэша. Если это так, то, возможно, также поэкспериментируйте с размерами блоков, отличными от 4, чтобы увидеть, есть ли «золотое пятно».

person Paul R    schedule 17.11.2016
comment
Ваше первое предложение значительно улучшает время третьего шага, см. РЕДАКТИРОВАТЬ № 3 к моему сообщению. Развертывание цикла не способствует дальнейшему улучшению результата. Большое спасибо за ваш ответ. - person Vitality; 17.11.2016
comment
Для успешного обмена циклами for, который вы предложили, многопоточность только ухудшает результаты. - person Vitality; 17.11.2016
comment
Наконец, мне удалось улучшить свой код, также следуя вашим предложениям. Я дал полный ответ ниже. Спасибо за Ваш интерес. - person Vitality; 18.11.2016
comment
@JackOLantern: отлично - спасибо, что проследили за этим и написали свои выводы - может быть очень полезно другим читателям в будущем! - person Paul R; 18.11.2016

Несколько вариантов для ускорения работы:

  1. Запустите многопоточность, если вы используете только однопоточность и у вас есть несколько доступных ядер.

  2. Создайте и сохраните файл мудрости FFTW, особенно если размеры БПФ известны заранее. Используйте FFTW_EXHAUSTIVE и перезагружайте мудрость FFTW вместо того, чтобы каждый раз пересчитывать ее. Это также важно, если вы хотите, чтобы ваши результаты были последовательными. Поскольку FFTW может по-разному вычислять БПФ с разной вычисляемой мудростью, а результаты мудрости не обязательно всегда будут одинаковыми, разные запуски вашего процесса могут давать разные результаты, когда оба получают одинаковые входные данные.

  3. Если вы используете x86, используйте 64-разрядную версию. Алгоритм FFTW чрезвычайно интенсивно использует регистры, и ЦП x86, работающий в 64-битном режиме, имеет гораздо больше доступных регистров общего назначения, чем при работе в 32-битном режиме.

  4. Так как алгоритм FFTW очень интенсивно использует регистры, мне удалось улучшить производительность FFTW, скомпилировав FFTW с параметрами компилятора, которые предотвращают использование предварительной выборки и предотвращают неявное встраивание функций.

person Andrew Henle    schedule 16.11.2016
comment
Большое спасибо за ваш ответ. Однако концептуально меня в первую очередь интересует ускорение двух циклов for, а не fftw. Я отредактировал свой пост, добавив решение OpenMP для двух циклов for, но мне кажется, что я провожу несправедливое сравнение: два FFTW являются последовательными, а циклы for многопоточными. - person Vitality; 16.11.2016
comment
Наконец-то я смог улучшить свой код. Я разместил полный ответ. Спасибо за Ваш интерес. - person Vitality; 18.11.2016

Также следуя комментариям Пола Р., я улучшил свой код. Теперь альтернативный подход быстрее стандартного (с нулями). Ниже приведен полный сценарий C++. Для шагов № 1 и № 3 я прокомментировал другие опробованные решения, которые оказались медленнее или быстрее, чем некомментированное. У меня есть привилегированные невложенные циклы for, также с учетом более простого распараллеливания CUDA в будущем. Я еще не использую многопоточность для FFTW.

#include <stdio.h>
#include <stdlib.h>     /* srand, rand */
#include <time.h>       /* time */
#include <math.h>
#include <fstream>

#include <omp.h>

#include <fftw3.h>

#include "TimingCPU.h"

#define PI_d            3.141592653589793

/******************/
/* STEP #1 ON CPU */
/******************/
void step1CPU(fftw_complex * __restrict h_xpruning, const fftw_complex * __restrict h_x, const int N, const int K) {

//  double factor = -2. * PI_d / (K * N);
//  int n;
//  omp_set_nested(1);
//#pragma omp parallel for private(n) num_threads(4)
//  for (int k = 0; k < K; k++) {
//      double arg1 = factor * k;
//#pragma omp parallel for num_threads(4)
//      for (n = 0; n < N; n++) {
//          double arg = arg1 * n;
//          double cosarg = cos(arg);
//          double sinarg = sin(arg);
//          h_xpruning[k * N + n][0] = h_x[n][0] * cosarg - h_x[n][1] * sinarg;
//          h_xpruning[k * N + n][1] = h_x[n][0] * sinarg + h_x[n][1] * cosarg;
//      }
//  }

    //double factor = -2. * PI_d / (K * N);
    //int k;
    //omp_set_nested(1);
    //#pragma omp parallel for private(k) num_threads(4)
    //for (int n = 0; n < N; n++) {
    //  double arg1 = factor * n;
    //  #pragma omp parallel for num_threads(4)
    //  for (k = 0; k < K; k++) {
    //      double arg = arg1 * k;
    //      double cosarg = cos(arg);
    //      double sinarg = sin(arg);
    //      h_xpruning[k * N + n][0] = h_x[n][0] * cosarg - h_x[n][1] * sinarg;
    //      h_xpruning[k * N + n][1] = h_x[n][0] * sinarg + h_x[n][1] * cosarg;
    //  }
    //}

    //double factor = -2. * PI_d / (K * N);
    //for (int k = 0; k < K; k++) {
    //  double arg1 = factor * k;
    //  for (int n = 0; n < N; n++) {
    //      double arg = arg1 * n;
    //      double cosarg = cos(arg);
    //      double sinarg = sin(arg);
    //      h_xpruning[k * N + n][0] = h_x[n][0] * cosarg - h_x[n][1] * sinarg;
    //      h_xpruning[k * N + n][1] = h_x[n][0] * sinarg + h_x[n][1] * cosarg;
    //  }
    //}

    //double factor = -2. * PI_d / (K * N);
    //for (int n = 0; n < N; n++) {
    //  double arg1 = factor * n;
    //  for (int k = 0; k < K; k++) {
    //      double arg = arg1 * k;
    //      double cosarg = cos(arg);
    //      double sinarg = sin(arg);
    //      h_xpruning[k * N + n][0] = h_x[n][0] * cosarg - h_x[n][1] * sinarg;
    //      h_xpruning[k * N + n][1] = h_x[n][0] * sinarg + h_x[n][1] * cosarg;
    //  }
    //}

    double factor = -2. * PI_d / (K * N);
    #pragma omp parallel for num_threads(8)
    for (int n = 0; n < K * N; n++) {
        int row = n / N;
        int col = n % N;
        double arg = factor * row * col;
        double cosarg = cos(arg);
        double sinarg = sin(arg);
        h_xpruning[n][0] = h_x[col][0] * cosarg - h_x[col][1] * sinarg;
        h_xpruning[n][1] = h_x[col][0] * sinarg + h_x[col][1] * cosarg;
    }
}

/******************/
/* STEP #3 ON CPU */
/******************/
void step3CPU(fftw_complex * __restrict h_xhatpruning, const fftw_complex * __restrict h_xhatpruning_temp, const int N, const int K) {

    //int k;
    //omp_set_nested(1);
    //#pragma omp parallel for private(k) num_threads(4)
    //for (int p = 0; p < N; p++) {
    //  #pragma omp parallel for num_threads(4)
    //  for (k = 0; k < K; k++) {
    //      h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
    //      h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
    //  }
    //} 

    //int p;
    //omp_set_nested(1);
    //#pragma omp parallel for private(p) num_threads(4)
    //for (int k = 0; k < K; k++) {
    //  #pragma omp parallel for num_threads(4)
    //  for (p = 0; p < N; p++) {
    //      h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
    //      h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
    //  }
    //}

    //for (int p = 0; p < N; p++) {
    //  for (int k = 0; k < K; k++) {
    //      h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
    //      h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
    //  }
    //}

    //for (int k = 0; k < K; k++) {
    //  for (int p = 0; p < N; p++) {
    //      h_xhatpruning[p * K + k][0] = h_xhatpruning_temp[p + k * N][0];
    //      h_xhatpruning[p * K + k][1] = h_xhatpruning_temp[p + k * N][1];
    //  }
    //}

    #pragma omp parallel for num_threads(8)
    for (int p = 0; p < K * N; p++) {
        int col = p % N;
        int row = p / K;
        h_xhatpruning[col * K + row][0] = h_xhatpruning_temp[col + row * N][0];
        h_xhatpruning[col * K + row][1] = h_xhatpruning_temp[col + row * N][1];
    }

    //for (int p = 0; p < N; p += 2) {
    //  for (int k = 0; k < K; k++) {
    //      for (int p0 = 0; p0 < 2; p0++) {
    //          h_xhatpruning[(p + p0) * K + k][0] = h_xhatpruning_temp[(p + p0) + k * N][0];
    //          h_xhatpruning[(p + p0) * K + k][1] = h_xhatpruning_temp[(p + p0) + k * N][1];
    //      }
    //  }
    //}

}

/********/
/* MAIN */
/********/
void main() {

    int N = 10;
    int K = 100000;

    // --- CPU memory allocations
    fftw_complex *h_x = (fftw_complex *)malloc(N     * sizeof(fftw_complex));
    fftw_complex *h_xzp = (fftw_complex *)calloc(N * K, sizeof(fftw_complex));
    fftw_complex *h_xpruning = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhatpruning = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhatpruning_temp = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    fftw_complex *h_xhat = (fftw_complex *)malloc(N * K * sizeof(fftw_complex));
    //double2        *h_xhatGPU = (double2 *)malloc(N * K * sizeof(double2));


    // --- Random number generation of the data sequence on the CPU - moving the data from CPU to GPU
    srand(time(NULL));
    for (int k = 0; k < N; k++) {
        h_x[k][0] = (double)rand() / (double)RAND_MAX;
        h_x[k][1] = (double)rand() / (double)RAND_MAX;
    }
    //gpuErrchk(cudaMemcpy(d_x, h_x, N * sizeof(double2), cudaMemcpyHostToDevice));

    memcpy(h_xzp, h_x, N * sizeof(fftw_complex));

    // --- FFTW and cuFFT plans
    fftw_plan h_plan_zp      = fftw_plan_dft_1d(N * K, h_xzp, h_xhat, FFTW_FORWARD, FFTW_ESTIMATE);
    fftw_plan h_plan_pruning = fftw_plan_many_dft(1, &N, K, h_xpruning, NULL, 1, N, h_xhatpruning_temp, NULL, 1, N, FFTW_FORWARD, FFTW_ESTIMATE);

    double totalTimeCPU = 0., totalTimeGPU = 0.;
    double partialTimeCPU, partialTimeGPU;

    /****************************/
    /* STANDARD APPROACH ON CPU */
    /****************************/
    printf("Number of processors available = %i\n", omp_get_num_procs());
    printf("Number of threads              = %i\n", omp_get_max_threads());

    TimingCPU timerCPU;
    timerCPU.StartCounter();
    fftw_execute(h_plan_zp);
    printf("\nStadard on CPU: \t \t %f\n", timerCPU.GetCounter());

    /******************/
    /* STEP #1 ON CPU */
    /******************/
    timerCPU.StartCounter();
    step1CPU(h_xpruning, h_x, N, K);
    partialTimeCPU = timerCPU.GetCounter();
    totalTimeCPU = totalTimeCPU + partialTimeCPU;
    printf("\nOptimized first step CPU: \t %f\n", totalTimeCPU);

    /******************/
    /* STEP #2 ON CPU */
    /******************/
    timerCPU.StartCounter();
    fftw_execute(h_plan_pruning);
    partialTimeCPU = timerCPU.GetCounter();
    totalTimeCPU = totalTimeCPU + partialTimeCPU;
    printf("Optimized second step CPU: \t %f\n", timerCPU.GetCounter());

    /******************/
    /* STEP #3 ON CPU */
    /******************/
    timerCPU.StartCounter();
    step3CPU(h_xhatpruning, h_xhatpruning_temp, N, K);
    partialTimeCPU = timerCPU.GetCounter();
    totalTimeCPU = totalTimeCPU + partialTimeCPU;
    printf("Optimized third step CPU: \t %f\n", partialTimeCPU);

    printf("Total time CPU: \t \t %f\n", totalTimeCPU);

    double rmserror = 0., norm = 0.;
    for (int n = 0; n < N; n++) {
        rmserror = rmserror + (h_xhatpruning[n][0] - h_xhat[n][0]) * (h_xhatpruning[n][0] - h_xhat[n][0]) + (h_xhatpruning[n][1] - h_xhat[n][1]) * (h_xhatpruning[n][1] - h_xhat[n][1]);
        norm = norm + h_xhat[n][0] * h_xhat[n][0] + h_xhat[n][1] * h_xhat[n][1];
    }
    printf("\nrmserror %f\n", 100. * sqrt(rmserror / norm));

    fftw_destroy_plan(h_plan_zp);

}

Для случая

N = 10
K = 100000

мое время следующее

Stadard on CPU:                  23.895417

Optimized first step CPU:        4.472087
Optimized second step CPU:       4.926603
Optimized third step CPU:        2.394958
Total time CPU:                  11.793648
person Vitality    schedule 18.11.2016