I have the following function in Matlab:
function [RawDataKK]=DataCompressKKTest(Data,RXangle,s)
tSize=size(Data,1);
xSize=size(Data,2);
TXSize=size(Data,3);
RXSize=numel(RXangle);
RawDataKK=zeros(tSize,TXSize,RXSize);
DataTemp=zeros(tSize,xSize);
slope = s*sin(RXangle(:))/2;
nShift = round(slope.*((0:xSize-1) - (xSize-1)*(1-sign(RXangle(:)))/2));
for nR=1:RXSize
% indices = mod((0:tSize-1)' + round(nShift(nR,:)), tSize) + 1;
for nT=1:TXSize
for nx=1:xSize
% DataTemp(indices(:,nx)) = Data(:,nx,nT);
DataTemp(:,nx)=circshift(Data(:,nx,nT),nShift(nR,nx));
% DataTemp(1:nShift(nR,nx),nx) = Data( (tSize-nShift(nR,nx)+1):end,nx,nT);
% DataTemp( (nShift(nR,nx)+1):end,nx) = Data( 1:(tSize-nShift(nR,nx)),nx,nT);
end
RawDataKK(:,nT,nR)=sum(DataTemp,2);
end
end
end
I tried implementing it as a C++ function using the Eigen toolbox and the Matlab Data API for C++. In C++, I'm implementing the commented out version which returns identical results and runs at roughly the same speed in Matlab.
#include <iostream>
#include "mex.hpp"
#include "mexAdapter.hpp"
#include <Eigen/Dense>
#include <MatlabDataArray.hpp>
#include <cmath>
#include <math.h>
#include <omp.h>
class MexFunction : public matlab::mex::Function {
public:
// Factory to create MATLAB data arrays
matlab::data::ArrayFactory factory;
void operator()(matlab::mex::ArgumentList outputs, matlab::mex::ArgumentList inputs) {
checkArguments(outputs, inputs);
// Initialize input parameters
int tSize = inputs[0][0];
int xSize = inputs[0][1];
int TXSize = inputs[0][2];
int RXSize = inputs[0][3];
int RFlen = inputs[0][4];
auto ptr = getDataPtr<std::complex<double>>(inputs[1]);
Eigen::Map< const Eigen::MatrixXcd > Data( ptr, tSize, TXSize*xSize );
double s = inputs[2][0];
auto ptr2 = getDataPtr<double>(inputs[3]);
Eigen::Map< const Eigen::VectorXd > RXangle( ptr2, RXSize );
outputs[0] = factory.createArray<std::complex<double>>({static_cast<size_t>(tSize),static_cast<size_t>(TXSize*RXSize)});
auto ptrRecon = getOutDataPtr<std::complex<double>>(outputs[0]);
Eigen::Map<Eigen::MatrixXcd> RFDataKK(ptrRecon,tSize,TXSize*RXSize);
// Get num threads
int numThreads = omp_get_max_threads();
int nProc = omp_get_num_procs();
omp_set_num_threads(nProc*2);
Eigen::MatrixXi nShift = ( (s*RXangle.array().sin()/2).replicate(1,xSize).array() *
(Eigen::VectorXd::LinSpaced(xSize,0,xSize-1).transpose().replicate(RXSize,1).array() -
((xSize-1)*((1-RXangle.array().sign())/2)).matrix().replicate(1,xSize).array()) ).round().cast<int>();
Eigen::MatrixXi nShift2 = tSize - nShift.array();
#pragma omp parallel for
for (int nR = 0; nR < RXSize; nR++) {
Eigen::MatrixXcd shiftedData = Eigen::MatrixXcd::Zero(tSize,TXSize*xSize);
for (int nx = 0; nx < xSize; nx++) {
shiftedData( Eigen::seq(0,nShift(nR,nx)-1) , Eigen::seq(nx*TXSize,(nx+1)*TXSize-1)) = Data( Eigen::seq(nShift2(nR,nx),tSize-1) , Eigen::seq(nx*TXSize,(nx+1)*TXSize-1));
shiftedData( Eigen::seq(nShift(nR,nx),tSize-1) , Eigen::seq(nx*TXSize,(nx+1)*TXSize-1)) = Data( Eigen::seq(0,nShift2(nR,nx)-1) , Eigen::seq(nx*TXSize,(nx+1)*TXSize-1));
}
RFDataKK(Eigen::all,Eigen::seq(nR*TXSize,(nR+1)*TXSize-1) ) = shiftedData.reshaped(tSize*TXSize,xSize).rowwise().sum().reshaped(tSize,TXSize);
}
}
void checkArguments(matlab::mex::ArgumentList outputs, matlab::mex::ArgumentList inputs) {
std::shared_ptr<matlab::engine::MATLABEngine> matlabPtr = getEngine();
matlab::data::ArrayFactory factory;
if (inputs.size() != 4) {
matlabPtr->feval(u"error",
0, std::vector<matlab::data::Array>({ factory.createScalar("Four inputs required") }));
}
if (inputs[0].getNumberOfElements() != 5) {
matlabPtr->feval(u"error",
0, std::vector<matlab::data::Array>({ factory.createScalar("Need 5 input parameters") }));
}
if (inputs[0].getType() != matlab::data::ArrayType::INT32) {
matlabPtr->feval(u"error",
0, std::vector<matlab::data::Array>({ factory.createScalar("Input parameter must be 32 bit integer") }));
}
if (inputs[1].getType() == matlab::data::ArrayType::DOUBLE ||
inputs[1].getType() != matlab::data::ArrayType::COMPLEX_DOUBLE) {
matlabPtr->feval(u"error",
0, std::vector<matlab::data::Array>({ factory.createScalar("Input RFData must be type complex double") }));
}
if (inputs[1].getDimensions().size() != 2) {
matlabPtr->feval(u"error",
0, std::vector<matlab::data::Array>({ factory.createScalar("Input must be m-by-n dimension") }));
}
// TODO: Implement remaining checks
}
template <typename T>
const T* getDataPtr(matlab::data::Array arr) {
const matlab::data::TypedArray<T> arr_t = arr;
matlab::data::TypedIterator<const T> it(arr_t.begin());
return it.operator->();
}
template <typename T>
T* getOutDataPtr(matlab::data::Array& arr) {
auto range = matlab::data::getWritableElements<T>(arr);
return range.begin().operator->();
}
};
When I perform a speed test in Matlab after compiling with the following parameters (with mingw GCC version 8.3), I find that I have not gained a meaningful speedup for the array sizes I am working with. Why is the speedup only a factor of 2? I am running the code on a machine with 24 cores, I would expect at least an order of magnitude improvement, especially since the code seems practically identical.
Compilation and testing code:
mingwFlags = {'CXXFLAGS="$CXXFLAGS -march=native -std=c++14 -fno-math-errno -ffast-math -fopenmp -DNDEBUG -w -Wno-error"',...
'LDFLAGS="$LDFLAGS -fopenmp"','CXXOPTIMFLAGS="-O3"'};
% ipath is the location of the eigen library.
tic; mex(ipath,mingwFlags{1},mingwFlags{2},mingwFlags{3},'CompressKKV2.cpp'); toc
tic; mex(ipath,mingwFlags{1},mingwFlags{2},mingwFlags{3},'CompressKKIndices.cpp'); toc
% some preinitialization and data loading. Dummy data could be included here. The point is to test performance
% Timing test:
nTrials = 100;
T = zeros(nTrials,4);
for i = 1:nTrials
tic; RawDataKK2 = CompressKKV2(param,cRF(1:param(5),p.ConnMap),s,p.RXangle);
RawDataKK2 = reshape(RawDataKK2,[p.szRFframe+1,p.na,p.nRX]); T(i,1) = toc;
tic; RawDataKK3=DataCompressKKTest(Data,p.RXangle,s); T(i,2) = toc;
end
chkT = mean(T,1)
Timing results:
chkT =
0.4172 0.8304
The values of the input parameters for my test cases are as follows:
tSize = 2688; xSize = 192; TXSize = 15; RXSize = 16; RFlen = tSize*TXSize;
The rest of the data can take random values for testing performance as long as the datatypes are correct. I think this code will work with other positive values for the above variables as well as long as they remain the same order of magnitude.
shiftedData
matrix inside the parallel region by splitting theparallel for
into aparallel
section with afor
pragma inside. I also don't get why you zero-initialize the matrix. Are there parts that are not overwritten by the loop overxSize
? Your use ofEigen::seq
instead of the more commonmatrix.block(top, left, rows, cols)
ormatrix.middleRows(top, count)
makes this very hard to read, IMHO. \$\endgroup\$circshift
implementation itself seems reasonable to me. It would be better to do it over columns rather than rows but of course that requires transposing somewhere. I looked at the assembly (godbolt) of the central loop (includes a cleaned up, more readable version) and the rowwise sum does not get vectorized due to the reshaping. You might be better off doing the summation on-the-fly without copying into a temporary matrix. Just super simple one scalar per thread per iteration \$\endgroup\$reshape
with aMap
, otherwise Eigen cannot properly analyse the access pattern \$\endgroup\$