为啥 FFT 的计算时间比 intel MKL 中的元素到元素乘法要短?

Posted

技术标签:

【中文标题】为啥 FFT 的计算时间比 intel MKL 中的元素到元素乘法要短?【英文标题】:Why calculating time of FFT is shorter than element-to-element multiplication in intel MKL?为什么 FFT 的计算时间比 intel MKL 中的元素到元素乘法要短? 【发布时间】:2019-03-19 02:51:14 【问题描述】:

我有 1024*4608 个元素的向量(Original_signal),它存储在一维数组中。

我通过将每 1024 个元素复制 32 次到 1024*32*4608 来将 Original_signal 扩大到 Expand_signal

然后我使用 1024*32 的 Com_arrayExpand_signal 进行元素到元素的乘法,并在乘法后的数组中进行 1024FFT。

核心代码如下:

//initialize Original_signal
MKL_Complex8 *Original_signal = new MKL_Complex8[1024*4608];
for (int i=0; i<4608; i++)

  for (int j=0; j<1024; j++)
    
      Original_signal[j+i*1024].real=rand();
      Original_signal[j+i*1024].imag=rand();
    
 
//Com_array
MKL_Complex8 *Com_array= new MKL_Complex8[32*1024];
for (int i=0; i<32; i++)
  
    for (int j=0; j<1024; j++)
      
        Com_array[j+i*1024].real=cosf(2*pi*(i-16.0)/10.0*j^2);
        Com_array[j+i*1024].imag=sinf(2*pi*(i-16.0)/10.0*j^2);
      
  


//element-to-element multiplication
MKL_Complex8 *Temp_signal= new MKL_Complex8[1024*32];
MKL_Complex8 *Expand_signal= new MKL_Complex8[1024*32*4608];

gettimeofday(&Bgn_Time, 0);

for (int i=0; i<4608; i++)
  
    for (int j=0; j<32; j++)
      
        memcpy(Temp_signal+j*1024, Original_signal+i*1024, 1024*sizeof(MKL_Complex8));
      
      vmcMul(1024*32, Temp_signal, Com_array, Expand_signal+i*1024*32);
  

gettimeofday(&End_Time, 0);
double time_used = (double)(End_Time.tv_sec-Bgn_Time.tv_sec)*1000000+(double)(End_Time.tv_usec-Bgn_Time.tv_usec);
printf("element-to-element multiplication use time %fus\n, time_used ");


//FFT
DFTI_DESCRIPTOR_HANDLE h_FFT = 0;
DftiCreateDescriptor(&h_FFT, DFTI_SINGLE, DFTI_COMPLEX, 1, 1024);
DftiSetValue(h_FFT, DFTI_NUMBER_OF_TRANSFORMS, 32*4608);
DftiSetValue(h_FFT, DFTI_INPUT_DISTANCE, 1024);
DftiCommitDescriptor(h_FFT);


gettimeofday(&Bgn_Time, 0);

DftiComputeForward(h_FFT,Expand_signal);

gettimeofday(&End_Time, 0);
double time_used = (double)(End_Time.tv_sec-Bgn_Time.tv_sec)*1000000+(double)(End_Time.tv_usec-Bgn_Time.tv_usec);
printf("FFT use time %fus\n, time_used ");

元素到元素相乘的时间是700ms(去掉memcpy成本后),FFT的时间是500ms。

FFT的复数乘法计算为N/2log2N,元素到元素的乘法为N。

在这个项目中 N=1024。理论上,FFT 比元素到元素的乘法慢 5 倍。为什么实际上更快。

有什么方法可以加快项目速度?

(注意 Com_array 是对称的)

【问题讨论】:

您的时序可能包括大量的 I/O。对于元素乘法,您有 2N 次读取。对于 FFT,它是 N 个读数。在 FFT 情况下,函数调用开销也更少。您可能还需要检查 CPU/核心调度计划,以查看多个 FFT 是否并行完成,以及 vcMul 是否也是这种情况。 一般说一种算法的时间复杂度是N,另一种算法的时间复杂度是N log N,并不是说这些数字是可比的。在这两种情况下,都有一个常数因子(C1 * N vs C2 * N * log(N))在每种情况下都不同。插入排序是 O(N*N) 而快速排序是 O(N log N) - 但是对于短列表,插入排序通常更快,因为(隐含的)常数更小。 欢迎来到 ***!您是否对 mode of multiplication 使用特定值?其中一些模式执行错误检查或提高准确性。这些特征可能会导致计算速度变慢。长度为 N 的 dft 的 flop 计数约为 5Nlog_2(N) (//Cooley Tukey 算法)。 fftw.org/fftw-paper.pdf 对于 N=1024,每个值大约对应 50 次翻牌。对于实际信号,它可以除以 2。它确实比 1 乘法大得多! @francis 我在 vmcMul 中专门使用了 VML_EP 模式,它可以加速乘法并降低精度。但时间成本仍然大于 FFT。 @SleuthEye 我将此线程绑定在 CPU 的一个核心中。所以我认为他们在同一个环境中。 【参考方案1】:

在这个项目中 N=1024。理论上,FFT 比元素到元素的乘法慢 5 倍。为什么实际更快?

正如 cmets 中所指出的,FFT 的时间复杂度为您提供了各种 FFT 长度的相对度量,直至某个常数因子。当试图与其他计算进行比较时,这个因素变得很重要。此外,您的分析假设性能受到浮点运算的限制,而实际上实际性能似乎受到其他因素的限制,例如特殊情况处理(例如NaNInf)、内存和缓存访问。

有什么方法可以加快项目速度?

由于您的性能瓶颈在于复杂的逐元素向量乘法运算,以下将重点关注提高该运算的性能。

我没有 MKL 来执行实际的基准测试,但可以公平地假设 vmcMul 实现对于 NaNInf 等特殊情况都相当稳健,并且在这种情况下得到了相当优化。

如果您不需要针对特殊情况的鲁棒性,在 SSE3 处理器上运行,可以保证您的数组大小是 2 的倍数,并且它们是 16 字节对齐的,那么您可以通过使用获得一些性能提升一个简化的实现,如下所示(基于Sebastien's answer to another post):

#include <pmmintrin.h>
#include <xmmintrin.h>

// Computes and element-by-element multiplication of complex vectors "a" and "b" and
// stores the results in "c".
// Vectors "a", "b" and "c" must be:
//   - vectors of even length N
//   - 16-bytes aligned
// Special cases such as NaN and Inf are not handled.
//
// based on https://***.com/questions/3211346/complex-mul-and-div-using-sse-instructions#4884057
void packed_vec_mult(int N, MKL_Complex8* a, MKL_Complex8* b, MKL_Complex8* c)

  int M = N/2;

  __m128* aptr = reinterpret_cast<__m128*>(a);
  __m128* bptr = reinterpret_cast<__m128*>(b);
  __m128* cptr = reinterpret_cast<__m128*>(c);
  for (int i = 0; i < M; i++)
  
    __m128 t0 = _mm_moveldup_ps(*aptr);
    __m128 t1 = *bptr;
    __m128 t2 = _mm_mul_ps(t0, t1);
    __m128 t3 = _mm_shuffle_ps(t1, t1, 0xb1);
    __m128 t4 = _mm_movehdup_ps(*aptr);
    __m128 t5 = _mm_mul_ps(t4, t3);
    *cptr = _mm_addsub_ps(t2, t5);

    ++aptr;
    ++bptr;
    ++cptr;
  

一旦优化了乘法,您的实现仍然可以通过直接将Orignal_signalCom_array 的不同部分多次相乘来消除Temp_signalmemcpy 的额外副本来改进您的实现,如下所示:

MKL_Complex8* outptr = Expand_signal;
for (int i=0; i<4608; i++)

  for (int j=0; j<32; j++)
  
    packed_vec_mult(1024, Original_signal+i*1024, Com_array+j*1024, outptr);
    outptr += 1024;
  

与将vmcMul 替换为packed_vec_mult 的实现相比,最后一步将使您的性能再提高约20%。

最后,由于循环在独立的块上执行操作,您可以通过在多个线程上启动并行计算来获得显着更高的吞吐量(但相似的延迟),以便 CPU 始终保持忙碌而不是等待传输中的数据到/从内存。我的测试表明大约有 2 倍的改进,但结果可能会因您的特定机器而异。

【讨论】:

以上是关于为啥 FFT 的计算时间比 intel MKL 中的元素到元素乘法要短?的主要内容,如果未能解决你的问题,请参考以下文章

转载:Intel MKL 稀疏矩阵求解PARDISO 函数

英特尔至强融核上的 MKL 3D 双精度复数 FFT

如何在 tensorflow 中使用 intel-mkl

mkl调用,编译

检测是不是在 Visual Studio 项目的属性中启用了 Intel MKL

macos安装pytorch出现Intel MKL 问题