fft_cc.cpp#
The file fft_cc.cpp
shows complex time, complex frequency FFTs performed
both in-place and also out-of-place.
Scratch memory is demonstrated using allocateScratch()
, allocateMemory()
,
and by manually allocating a fixed area on the stack.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Copyright (C) 2023--2024, High Performance Kernels LLC *
* *
* This software and the related documents are provided as is, WITHOUT ANY *
* WARRANTY, without even the implied warranty of MERCHANTABILITY or FITNESS *
* FOR A PARTICULAR PURPOSE. *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <cassert>
#include <complex>
#include <iostream>
#include <new> // placement new in examples 4 and 5
#include <string>
#include <vector>
#include <hpk/fft/makeFactory.hpp>
// Prints data (having elements of type 'T', which may be complex) formatted
// into rows and columns, with line continuations if necessary.
template<class T>
void printData(std::string label, const T* data, int rows, int cols) {
std::cout << label << ":\n";
for (int i = 0; i < rows; ++i) {
std::cout << " ";
for (int j = 0; j < cols; ++j) {
if (j % 8 == 0 && j > 0) std::cout << " \\\n ";
std::cout << data[cols * i + j] << " ";
}
std::cout << '\n';
}
std::cout << std::endl;
}
// Prints data formatted into rows and columns, with line continuations if
// necessary. It assumes that real and imaginary elements (of type 'fp_t')
// are interleaved in memory.
template<typename fp_t>
void printComplexData(std::string label, const fp_t* data, int rows, int cols) {
std::cout << label << ":\n";
for (int i = 0; i < rows; ++i) {
std::cout << " ";
for (int j = 0; j < cols; ++j) {
if (j % 8 == 0 && j > 0) std::cout << " \\\n ";
std::cout << data[(2 * cols * i) + (2 * j) + 0] << std::showpos
<< data[(2 * cols * i) + (2 * j) + 1] << "i "
<< std::noshowpos;
}
std::cout << '\n';
}
std::cout << std::endl;
}
int main() {
// Simple introductory example of a one-dimensional two-point FFT
std::cout << "Example #0: Simple two-point example.\n"
<< "~~~~~~~~~~ \n";
float twoPoints[4] = {5.0f, 6.0f, 1.0f, 2.0f};
printComplexData("input", twoPoints, 1, 2);
// Watch me do the FFT on one line:
hpk::fft::makeFactory<float>()->makeInplace({2})->forward(twoPoints);
printComplexData("forward", twoPoints, 1, 2);
// Let's make factories (for both single precision and double precision)
// for FFTs with complex time and complex frequency domains.
std::cout << "Setup: Making factories.\n"
<< "~~~~~ \n";
hpk::Configuration cfg{{hpk::Parameter::threads, 1}};
// Below, auto is std::unique_ptr<hpk::fft::FactoryCC<float>>.
auto factory_s = hpk::fft::makeFactory<float>(cfg);
if (factory_s) {
std::cout << "Using " << *factory_s << " for single precision.\n";
} else {
std::cout << "Error: makeFactory<float>() failed" << std::endl;
return -1;
}
// Below, auto is std::unique_ptr<hpk::fft::FactoryCC<double>>.
auto factory_d = hpk::fft::makeFactory<double>(cfg);
if (factory_d) {
std::cout << "Using " << *factory_d << " for double precision.\n";
} else {
std::cout << "Error: makeFactory<double>() failed" << std::endl;
return -1;
}
std::cout << '\n';
// Simple example of a one-dimensional four-point FFT
std::cout << "Example #1: Simple four-point example.\n"
<< "~~~~~~~~~~ \n";
std::vector<std::complex<double>> fourPoints = {1.0, 0.0, 0.0, 0.0};
printData("input", fourPoints.data(), 1, 4);
// In C++20, avoid the cast below by using std::ssize(fourPoints).
long ssize = static_cast<long>(std::size(fourPoints));
factory_d->makeInplace({ssize})->forward(fourPoints.data());
printData("forward", fourPoints.data(), 1, 4);
// One-dimensional eight-point FFT in single precision.
std::cout << "Example #2: One-dimensional eight-point FFT (batch=1).\n"
<< "~~~~~~~~~~ \n";
// The points are contiguous in memory, so strides are omitted.
hpk::fft::InplaceDim layout[1] = {8};
hpk::fft::InplaceDim batch = 1;
// Below, auto is std::unique_ptr<hpk::fft::InplaceCC<float>>.
// And, it's a good idea to state the number of dimensions explicitly,
// both for readability and so the compiler can check it.
auto sfft_8 = factory_s->makeInplace<1>(layout, batch);
assert(sfft_8 && "Error: makeInplace() failed.");
// No spooky action at a distance; changes to layout and/or batch from
// now on will not affect sfft_8.
layout[0].n = 4; // Perhaps the example after this will be four points.
std::cout << *sfft_8 << '\n';
float data[16] = {1.0f, 2.0f};
printComplexData("input", data, 1, 8);
// Scratch memory (if needed) will be automatically allocated and deleted.
sfft_8->forward(data);
printComplexData("forward", data, 1, 8);
// One-dimensional four-point FFT in double precision.
std::cout << "Example #3: One-dimensional four-point FFT (batch=1).\n"
<< "~~~~~~~~~~ \n";
// Recall that, in the previous example, we've already initialized layout.
std::cout << "Note: layout has " << std::size(layout)
<< " dimension, which is " << layout[0] << '\n';
// Note that, below, the batch argument is omitted so it defaults to one.
std::unique_ptr dfft_4 = factory_d->makeInplace<1>(layout);
assert(dfft_4 && "Error: makeInplace() failed.");
{
std::cout << *dfft_4 << '\n';
double inout[8] = {1.0, 2.0};
printComplexData("input", inout, 1, 4);
// It's more efficient to make a scratch area once and reuse it.
// The function allocateScratch() allocates memory and returns a
// std::unique_ptr, so the allocated memory (if any) is deallocated
// when the smart pointer goes out of scope.
// Recall that dfft_4 is itself a (smart) pointer and so must be
// dereferenced to be passed as an argument to allocateScratch().
// By default, memory is allocated with an hpk::AlignedAllocator,
// in this case hpk::AlignedAllocator<double>.
// Below, auto is std::unique_ptr<double, Deleter>, and Deleter is
// hpk::Deleter<hpk::AlignedAllocator<double>>.
// The function template name ::hpk::fft::allocateScratch is found
// by ADL (argument-dependent lookup) and the math type (double) is
// deduced.
auto scratch = allocateScratch(*dfft_4);
dfft_4->forward(inout, scratch);
printComplexData("forward", inout, 1, 4);
dfft_4->backward(inout, scratch);
printComplexData("backward", inout, 1, 4);
} // The memory owned by the std::unique_ptr scratch is deleted here.
// In general, we don't know how much scratch space will be needed, and
// this may change from one library release to another.
// However, we can allocate a fixed amount of scratch space on the stack
// and use it (when it's enough) to avoid dynamic memory allocation.
// Note that leaving this block scope automatically deallocates stackArea.
constexpr std::size_t kStackAreaBytes = 8192;
alignas(64) std::byte stackArea[kStackAreaBytes];
// Two-dimensional FFT with five rows and eight columns
std::cout << "Example #4: Two-dimensional 5x8 FFT (batch=1).\n"
<< "~~~~~~~~~~ \n";
auto sfft_5x8 = factory_s->makeOoplace<2>({5, 8});
assert(sfft_5x8 && "Error: makeOoplace() failed.");
std::string fftString = sfft_5x8->toString();
std::cout << fftString << '\n';
alignas(64) float input[80] = {1.0f, 2.0f};
alignas(64) float output[80] = {0.0f, 0.0f};
printComplexData("input", input, 5, 8);
std::size_t scratchSize = sfft_5x8->scratchSize();
// Note that scratchSize() is measured in real (not complex) elements.
if (scratchSize * sizeof(float) <= kStackAreaBytes) {
// Below we use a non-allocating placement new.
float* scratch = new (stackArea) float[scratchSize];
sfft_5x8->forwardCopy(input, output, scratch);
} else {
// There is a function template hpk::allocateMemory(), which is
// not specific to FFTs, that has a std::size_t parameter for the
// number of real elements to allocate. This argument may be 0,
// in which case an empty std::unique_ptr is returned.
// Since the function's arguments are C++ basic types, there are
// no ADL-associated namespaces, and so one needs the "hpk::".
auto scratch = hpk::allocateMemory<float>(scratchSize);
sfft_5x8->forwardCopy(input, output, scratch);
}
printComplexData("output", output, 5, 8);
// One-dimensional 60-point FFT in double precision.
std::cout << "Example #5: One-dimensional 60-point FFT (batch=1).\n"
<< "~~~~~~~~~~ \n";
// Initialize a shared_ptr if one desires to make copies later.
std::shared_ptr dfft_60 = factory_d->makeInplace<1>({60});
assert(dfft_60 && "Error: makeInplace() failed.");
std::cout << *dfft_60 << '\n';
// It's a good idea to use a vector having cacheline aligned data.
std::vector<double, hpk::AlignedAllocator<double>> inout(120, 0.0f);
inout[0] = 1.0f;
inout[1] = 2.0f;
printComplexData("input", inout.data(), 1, 60);
// Below, scratchSize() returns the number of doubles needed.
scratchSize = dfft_60->scratchSize();
// Since we already have scratchSize, it would be faster simply to
// multiply it by sizeof(double), but for pedagogical reasons:
if (dfft_60->scratchSizeBytes() <= kStackAreaBytes) {
// This placement new ends the lifetime of the previous objects
// in stackArea. No destructor is called, which is OK for floats.
double* scratch = new (stackArea) double[scratchSize];
dfft_60->forward(inout.data(), scratch);
} else {
// Scratch memory (with 128B alignment) by specifying an Allocator
// for the template type parameter of the forward() function.
// If not specified, hpk::AlignedAllocator<double> would be used.
// Note that the default alignment for hpk::AlignedAllocator is
// 64, and this default also applies to allocateScratch().
dfft_60->forward<hpk::AlignedAllocator<double, 128>>(inout.data());
}
printComplexData("forward", inout.data(), 1, 60);
}