最难不过二叉树

fftw非线程安全的一些解决思路

2024-05-08

实际工程中要用到快速傅里叶变换fft,我们一般会使用知名的fft库FFTW,因为FFTW在计算性能和稳定性上都有很大的优势,以下是fft库的benchmark

微信截图_20241127142359.png

从性能测试的结果来看,fftw3这个库性能非常好,遥遥领先其他fft库,所以也是最受欢迎的fft库。但是fftw有个很致命的缺点:非线程安全

fftw为了加速性能,在算法内部大量使用了全局变量来计算,所以能维持最佳的计算性能。但是一旦在多线程环境调用fftw的库函数,那么就会coredump。我最近在我的多线程Server里处理并发fft请求时,程序会异常coredump,最后定位到是fftw这个非线程安全的问题。

针对fftw的非线程安全问题,fftw官网是这么解答的:

https://www.fftw.org/fftw3_doc/Thread-safety.html

总的来说就是:

  • fftw只有fftw_execute是线程安全的,其他的函数比如fftw_malloc、fftw_plan_dft_r2c_1d、fftw_free等函数都是非线程安全。即只有执行paln时是线程安全,至于开辟回收内存的操作都非线程安全。
  • fftw只用于单线程场景,如果万不得已要在多线程场景下使用,那就只能加锁,但是加锁后fftw的性能将会急速下降。
  • fftw有些plugins可以帮忙加锁来保证线程安全,比如调用fftw_make_planner_thread_safe函数,但本质上就是加锁,这也会严重影响计算性能。

方案一:fftw加锁

暴力加锁的方案,性能都很低,原本10ms计算好的fft,可能要去到800ms。如果真的图方便要解决程序在fftw多线程下coredump的问题,而且也不在乎性能的话,可以直接加锁。代码大概是就是在调用FFTW库函数时直接加锁,计算完就释放锁。

m_mutex.lock()

fftw_plan p;
double *in1_c =(double*) fftw_malloc(sizeof(double)* NFFT);
fftw_complex  *out1_c = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)* NFFT);
p = fftw_plan_dft_r2c_1d(NFFT, in1_c, out1_c, FFTW_ESTIMATE);//
···
fftw_execute(p);
···
fftw_free(in1_c); fftw_free(out1_c);
fftw_destroy_plan(p);

m_mutex.unlock()

方案二:C++调用python fft库

因为加锁的方案实在太慢了,那么此时开始寻找其他思路去解决这个问题。一个思路就是,我能不能通过调用python 相关的fft库来解决这个fft计算的问题?python中有很多fft库,这些库底层都是C写的也经过了一系列优化,性能应该不错。这里我使用了scipy.fftpack库做fft,Python调用方式很简单:

import scipy.fftpack as fpk
out1_c = fpk.fft(in1_c)

因为我的程序是C++编写,这里就存在C++调用python的方式,我这边采用pybind来做这个胶水层完成这项任务。

std::vector<double> fft(std::vector<double> input) 
{
    py::gil_scoped_acquire acquire;
    py::list input_pylist = py::cast(input);
    //用import函数导入python模块
    auto module=py::module::import("pycall");
    py::object out = module.attr("fft")(input_pylist);
    auto v =out.cast<py::array_t<double>>();
    auto vec = ndarray_to_vector(v);
    return vec;
}

这个方案测试下来发现,总体性能较方案1提升较大,但也限于Python的GIL,性能也没有达到预期。

方案三:使用其他fft库替换

因为性能的问题我需要寻找更合适的fft方案,因此开始在调研业界还有哪些性能还不错库。fft库的调研结果如下:

算法库计算速度线程安全license支持的精度使用者数量header only
FFTW非常快GPL or $单+双非常多(2.5k star)
ffts很快MIT单+双多(516 star)
kfr非常快GPL or $单+双很多(1.5k star)
pffft很快BSD-like一般(219 star)
muFFT很快MIT少(142 star)
KissFFT一般3-BSD单+双很多(1.3k star)
meow_fft一般0-BSD少(102 star)

这里总结一下选择的思路:

  • 如果你需要线程安全且使用者比较多(相对可靠)的fft库,那么可以在ktr、KissFFT中来选,在此基础上,如果还想追求性能那么kfr优先,但是注意kfr商用是要给钱的,如果能接受KissFFT一般的计算速度,那么就选KissFFT,毕竟是BSD证书。
  • 如果没有线程安全的需求,那么直接优先FFTW。如果要商用又不想给钱,那就上ffts,或者受众更多但更慢的KissFFT。

根据我的项目实际情况,我需要线程安全+相对可靠+免费的FFT库,最后我选择了KissFFT作为我的底层算法库,改造了我一些算法函数。大概的写法如下:

kiss_fft_cpx rin[NFFT];
kiss_fft_cpx sout[NFFT];
kiss_fft_cfg  kiss_fft_state;
kiss_fft_scalar zero;
kiss_fft_state = kiss_fft_alloc(NFFT,0,0,0);

···
kiss_fft(kiss_fft_state,rin,sout);
···
kiss_fft_free(kiss_fft_state);

改造后函数计算速度相对于前两个方案有巨大提升,已经能满足项目的性能要求,因此备选方案pffft和muFFT就没有再尝试了,看测评结果,pffft和muFFT计算速度大概是KissFFT的5倍左右,如果后续有更高的性能要求,可以试试这两个线程安全的FFT库。