\documentclass[11pt]{article} % !Rnw weave = knitr %% \VignetteIndexEntry{DFT benchmarks: fft vs FFT} %% \VignetteEngine{knitr::knitr} \usepackage[T1]{fontenc} \usepackage[utf8]{inputenc} \usepackage{fancyvrb} \usepackage[pdfborder={0 0 0}]{hyperref} \usepackage{url} \usepackage{upquote} \usepackage{graphicx} \usepackage{grffile} \usepackage{amsmath} \usepackage{amssymb} \usepackage{float} \usepackage{natbib} \usepackage{geometry} \geometry{verbose,tmargin=3cm,bmargin=5cm,lmargin=2.5cm,rmargin=2.5cm} \usepackage[font=sf, labelfont={sf,bf}, margin=2cm]{caption} \usepackage{color} \usepackage{txfonts} %% \input{supp_mathsyms} %% \newcommand{\SC}[1]{\textsc{#1}} \newcommand{\SCY}[0]{\SC{Yes}} \newcommand{\SCN}[0]{\SC{No}} \newcommand{\Rcmd}[1]{\texttt{#1}} \newcommand{\psd}[0]{\href{https://github.com/abarbour/psd/}{\color{blue}\Rcmd{psd}}} \newcommand{\naive}[0]{na\"{\i}ve} \newcommand{\bidx}[1]{\index{#1}{\textbf{#1}}} \newcommand{\idx}[1]{\index{#1}{#1}} % \title{Benchmarks for Discrete Fourier Transform (DFT) calculations in R} \author{Andrew J. Barbour} % \begin{document} \maketitle % \begin{abstract} % \begin{quote} The DFT calculator in R, \Rcmd{stats::fft}, uses the Mixed-Radix algorithm of \citet{singleton1969}. In this vignette we show how this calculator compares to \Rcmd{FFT} in the \Rcmd{fftw} package \citep{fftw}, which uses the FFTW algorithm of \citet{frigo2005}. For univariate DFT computations, the methods are nearly equivalent with two exceptions which are not mutually exclusive: (1) the series to be transformed is very long ($10^6$ terms), and \emph{especially} (2) when the series length is not highly composite. In both exceptions the algorithm \Rcmd{FFT} outperforms \Rcmd{fft}. \end{quote} % \begin{quote} \textit{\textbf{Update:} I have decided that (for now) \psd{} will not use \Rcmd{fftw::FFT}, despite its advantage over \Rcmd{stats::fft} for large-$n$ `NHC' series, simply because the binaries on CRAN have not been reliably built for some time now. If they do become reliable, I may consider using \Rcmd{fftw::FFT} instead. } \end{quote} % \end{abstract} \tableofcontents \clearpage \section{Benchmarking function} We use both functions in their default state, and ask them to transform the same univariate random series. Benchmark information comes from the \Rcmd{rbenchmark} program, and the versatile \Rcmd{plyr} and \Rcmd{reshape2} packages are used to manipulate the information for this presentation; \Rcmd{ggplot2} is used for plotting. First we load the libraries needed: <>= rm(list=ls()) library(fftw) library(rbenchmark) library(plyr) library(reshape2) library(ggplot2) @ and create a benchmark function: <>= reps <- 10 dftbm <- function(nd, repls=reps){ set.seed(1234) x <- rnorm(nd, mean=0, sd=1) bmd <- benchmark(replications=repls, fftw::FFT(x), stats::fft(x)) bmd$num_dat <- nd bmd$relative[is.na(bmd$relative)] <- 1 # NA happens. return(bmd) } @ \section{Highly composite (HC) series} It's well known that DFT algorithms are most efficient for ``Highly Composite Numbers"\footnote{ This is the reason for the \Rcmd{stats::nextn} function. }, specifically multiples of (2,3,5). So, we create a vector of series lengths we wish to benchmark <>= (nterms.even <- round(2**seq.int(from=4,to=20,by=1))) @ and use it with \Rcmd{lapply} and the benchmark function previously defined. These data are further distilled into a usable format with \Rcmd{ldply}: <>= bench.even <- function(){ benchdat.e <- plyr::ldply(lapply(X=nterms.even, FUN=dftbm)) } bench.even() @ \section{Non highly composite (NHC) series} DFT algorithms can have drastically reduced performance if the series length is not highly composite (NHC). We now test NHC series by adding one to the HC series-length vector (also restricting the total length for sanity's sake): <>= nterms.odd <- nterms.even + 1 nterms.odd <- nterms.odd[nterms.odd < 50e3] # painfully long otherwise! @ and performing the full set of benchmarks again: <>= bench.odd <- function(){ benchdat.o <- plyr::ldply(lapply(X=nterms.odd, FUN=dftbm)) } bench.odd() # FAIR WARNING: this can take a while!! @ \section{Visualization} In order to plot the results, we need to perform some map/reduce operations on the data \citep{wickham2010}. We intend to show faceted \Rcmd{ggplot2}-based figures with row-wise summary information\footnote{ Based on this post:\\ {\small \url{https://geokook.wordpress.com/2012/12/29/row-wise-summary-curves-in-faceted-ggplot2-figures/} } } so we can easily intercompare the benchmark data. The benchmark data we will show are \Rcmd{user.self}, \Rcmd{sys.self}, \Rcmd{elapsed}, and \Rcmd{relative}. The results are shown in Figure \ref{fig:results}. <>= pltbench <- function(lentyp=c("even","odd")){ benchdat <- switch(match.arg(lentyp), even=benchdat.e, odd=benchdat.o) stopifnot(exists("benchdat")) tests <- unique(benchdat$test) ## subset only information we care about allbench.df.drp <- subset(benchdat, select=c(test, num_dat, user.self, sys.self, elapsed, relative)) ## reduce data.frame with melt allbench.df.mlt <- reshape2::melt(allbench.df.drp, id.vars=c("test","num_dat")) ## calculate the summary information to be plotted: tmpd <- plyr::ddply(allbench.df.mlt, .(variable, num_dat), summarise, summary="medians", value=ggplot2::mean_cl_normal(value)[1,1]) ## create copies for each test and map to data.frame allmeds <<- plyr::ldply(lapply(X=tests, FUN=function(x,df=tmpd){ df$test <- x; return(df) })) ## plot the benchmark data # 1/sqrt(n) standard errors [assumes N(0,1)] g <- ggplot(data=allbench.df.mlt, aes(x=log10(num_dat), y=log2(value), ymin=log2(value*(1-1/sqrt(reps))), ymax=log2(value*(1+1/sqrt(reps))), colour=test, group=test)) + scale_colour_discrete(guide="none") + theme_bw()+ ggtitle(sprintf("DFT benchmarks of %s length series",toupper(lentyp))) + ylim(c(-11,11))+ xlim(c(0.5,6.5)) ## add previous summary curves if exist if (exists("allmeds.prev")){ g <- g + geom_path(size=1.5, colour="dark grey", data=allmeds.prev, aes(group=test)) } ## create a facetted version g2 <- g + facet_grid(variable~test) #, scales="free_y") ## add the summary data as a line g3 <- g2 + geom_path(colour="black", data=allmeds, aes(group=test)) ## and finally the data print(g4 <<- g3 + geom_pointrange()) } @ <>= pltbench("even") allmeds.prev <- allmeds pltbench("odd") @ \begin{figure}[htb!] \begin{center} \includegraphics[width=0.5\textwidth]{fftw_bench_even}% \includegraphics[width=0.5\textwidth]{fftw_bench_odd} \caption{ DFT benchmark results for HC series lengths (left), and NHC series lengths (right) as a function of logarithmic series length. In each figure, the left facet-column is for results from \Rcmd{fftw::FFT} and the right column is for \Rcmd{stats::fft}. We also show the summary curves from the HC results in the NHC frames (thick grey curve) to highlight the drastic degradation in performance.} \label{fig:results} \end{center} \end{figure} \section{Conclusion} Figure \ref{fig:results} compares the DFT calculations for HC and NHC length series. For univariate DFT computations, the methods are nearly equivalent with two exceptions which are not mutually exclusive: (A) the series to be transformed is very long, and especially (B) when the series length is not highly composite. In both exceptions the algorithm \Rcmd{FFT} outperforms \Rcmd{fft}. In the case of exception (B), both methods have drastically increased computation times; hence, zero padding should be done to ensure the length does not adversely affect the efficiency of the DFT calculator. %\pagebreak \section*{Session Info} <>= utils::sessionInfo() @ %% bib and index \bibliographystyle{apalike} \bibliography{REFS} \end{document}