【发布时间】:2021-12-27 17:03:36
【问题描述】:
在使用 Matlab 的 C++ MEX API 的项目中,我必须计算超过 100,000 个 x 值的值 exp(j * 2pi * x),其中 x 始终为正双精度值。我编写了一些辅助函数,它们使用欧拉公式将计算分解为 sin/cos。然后我应用范围缩减的方法将我的值减少到它们在域 [0,T/4] 中的对应点,其中 T 是我正在计算的指数的周期。我跟踪 [0, T] 中的哪个象限,原始值稍后会落入。然后,我可以使用 horner 形式的泰勒级数多项式计算三角函数,并根据原始值所在的象限应用适当的移位。有关此技术中某些概念的更多信息,请查看 this answer。下面是这个函数的代码:
Eigen::VectorXcd calcRot2(const Eigen::Ref<const Eigen::VectorXd>& idxt) {
Eigen::VectorXd vidxt = idxt.array() - idxt.array().floor();
Eigen::VectorXd quadrant = (vidxt.array()*2+0.5).floor();
vidxt.array() -= (quadrant.array()*0.5);
vidxt.array() *= 2*3.14159265358979;
const Eigen::VectorXd sq = vidxt.array()*vidxt.array();
Eigen::VectorXcd M(vidxt.size());
M.real() = fastCos2(sq);
M.imag() = fastSin2(vidxt,sq);
M = (quadrant.array() == 1).select(-M,M);
return M;
}
我分析了使用 std::chrono 调用此函数的代码段,并对函数的平均调用次数超过 500 次(其中对 mex 函数的每次调用通过在循环中调用 calcRot2 来处理所有 100,000 多个值。每次迭代都通过大约 200 个值到 calcRot2)。我发现以下平均运行时间:
runtime with calcRot2: 75.4694 ms
runtime with fastSin/Cos commented out: 50.2409 ms
runtime with calcRot2 commented out: 30.2547 ms
看看这两种极端情况的区别,似乎 calcRot 对运行时的贡献很大。但是,其中只有一部分来自 sin/cos 计算。我会假设 Eigen 的隐式矢量化和编译器将使函数中其他操作的运行时有效地忽略不计。 (floor shouldn't be a problem!) 性能瓶颈到底在哪里?
这是我正在执行的编译命令(它使用我认为与 gcc 相同的 MinGW64):
mex(ipath,'CFLAGS="$CFLAGS -O3 -fno-math-errno -ffast-math -fopenmp -mavx2"','LDFLAGS="$LDFLAGS -fopenmp"','DAS.cpp','DAShelper.cpp')
参考代码
作为参考,这里是调用定时器的主mex函数中的代码段,以及调用calcRot2()的辅助函数:
MEX 函数调用:
chk1 = std::chrono::steady_clock::now();
// Calculate beamformed signal at each point
Eigen::MatrixXcd bfVec(p.nPoints,1);
#pragma omp parallel for
for (int i = 0; i < p.nPoints; i++) {
calcPoint(idxt.col(i),SIG,p,bfVec(i));
}
chk2 = std::chrono::steady_clock::now();
auto diff3 = chk2 - chk1;
计算点:
void calcPoint(const Eigen::Ref<const Eigen::VectorXd>& idxt,
const Eigen::Ref<const Eigen::MatrixXcd>& SIG,
Parameters& p, std::complex<double>& bfVal) {
Eigen::VectorXcd pRot = calcRot2(idxt*p.fc/p.fs);
int j = 0;
for (auto x : idxt) {
if(x >= 0) {
int vIDX = static_cast<int>(x);
bfVal += (SIG(vIDX,j)*(vIDX + 1 - x) + SIG(vIDX+1,j)*(x - vIDX))*pRot(j);
}
j++;
}
}
澄清
为了澄清,这条线
(vidxt.array()*2+0.5).floor()
意味着屈服:
0 if vidxt is between [0,0.25]
1 if vidxt is between [0.25,0.75]
2 if vidxt is between [0.75,1]
这里的想法是,当 vidxt 处于第二个区间时(对于周期为 2pi 的函数,单位圆上的象限 2 和 3),那么该值需要映射到它的负值。否则,范围缩小会将值映射到正确的值。
【问题讨论】:
-
调用
floor与使用地板强制转换为 int 不同。该函数还执行一系列数组操作。我们不知道这是什么大小的数组,但这些显然不是免费的。 -
实际上,您最好在数组上编写一个循环并对每个数组元素进行完整计算。这样你就不会一遍又一遍地遍历数组,这对缓存的使用很不利。
-
在之前的实现中,我迭代了每个数组元素并执行了完整的计算。性能稍差一些,总共大约 90 毫秒。我切换到这种方法,希望使用 Eigen 可以让编译器矢量化。此外,在之前的实施中,我使用静态转换为 int 而不是地板。静态铸造和地板之间几乎没有区别。然而,当使用 Eigen 的铸件和地板时,由于某种原因,铸件需要更长的时间。
-
对于快速正弦+余弦,请参阅github.com/microsoft/DirectXMath/blob/jan2021/Inc/… 那里的指数:github.com/microsoft/DirectXMath/blob/jan2021/Inc/… 该代码用于 FP32 向量,但应该相对容易移植到 FP64。
-
在您的澄清中,
0.25和0.75都是两个独立的包含范围的一部分。 (而且它们完全可以表示为二进制浮点数,所以它们发生了什么很重要)。您的意思是[0, 0.25)或(0.25, 0.75]或(0.25, 0.75)?或者您不在乎为那些边缘情况获得的两个相邻索引中的哪一个,取哪个允许最大优化?
标签: c++ performance eigen simd mex