在 Visual Studio 2017 年启用 Open MP 支持会减慢代码速度
Enabling Open MP Support in Visual Studio 2017 slows down codes
我正在尝试使用 OpenMP 来加速我的神经网络计算代码。由于我使用的是 Visual Studio 2017,因此我需要在 属性 工作表中启用 OpenMP 支持。然而,在我这样做之后,即使我没有在代码中包含任何#pragma omp
,代码的某些部分也会减慢大约 5 倍。
我已经隔离了这些部分,发现是这个特定的函数导致了问题:
void foo(Eigen::Matrix<float,3,Eigen::Dynamic> inputPts)
{
std::vector<Eigen::MatrixXf> activation;
activation.reserve(layerNo);
activation.push_back(inputPts);
int inputNo = inputPts.cols();
for (int i = 0; i < layerNo - 2; i++)
activation.push_back(((weights[i]*activation[i]).colwise()+bias[i]).array().tanh());
activation.push_back(((weights[layerNo - 2]*activation[layerNo - 2]).colwise()+bias[layerNo - 2]));
val = activation[layerNo - 1]/scalingFactor;
std::vector<Eigen::MatrixXf> delta;
delta.reserve(layerNo);
Eigen::Matrix<float, 1, Eigen::Dynamic> seed;
seed.setOnes(1, inputNo);
delta.push_back(seed);
for (int i = layerNo - 2; i >= 1; i--)
{
Eigen::Matrix<float,Eigen::Dynamic,Eigen::Dynamic>
d_temp = weights[i].transpose()*delta[layerNo - 2 - i],
d_temp2 = 1 - activation[i].array().square(),
deltaLayer = d_temp.cwiseProduct(d_temp2);
delta.push_back(deltaLayer);
}
grad = weights[0].transpose()*delta[layerNo - 2];
}
两个 for 循环的速度明显变慢(从 ~3ms 到 ~20ms)。奇怪的是,这个函数虽然在程序中被调用了很多次,但只有一部分受到影响。
我已经包含了头文件<omp.h>
。我不确定这是否是由于到处都在使用的 Eigen 库。我尝试按照 official site 中的建议定义 EIGEN_DONT_PARALLELIZE
并调用 Eigen::initParallel()
,但没有帮助。
奇怪的是,我什至没有包含任何 parallel pragma
,处理 OpenMP 函数应该没有任何开销吧?为什么它还在变慢?
如果启用了 OpenMP,Eigen 的矩阵矩阵产品默认是多线程的。问题可能是以下组合:
- 您的 CPU 是超线程的,例如,您有 4 个物理内核能够 运行 8 个线程。
- OpenMP 不允许知道物理内核的数量,因此 Eigen 将启动 8 个线程。
- Eigen 的矩阵-矩阵乘积内核经过全面优化,利用了近 100% 的 CPU 容量。因此,运行在单个内核上没有空间容纳两个这样的线程,并且性能显着下降(缓存污染)。
因此,解决方案是将 OpenMP 线程的数量限制为物理内核的数量,例如通过设置 OMP_NUM_THREADS 环境变量。也可以通过在编译时定义宏EIGEN_DONT_PARALLELIZE
来禁用Eigen的多线程。
doc 中的更多信息。
有关超线程如何降低性能的更多详细信息:
使用超线程,您可以在单个内核上以交错方式拥有两个线程 运行。他们交替执行每条指令。如果您的线程未使用少于 CPU 资源的一半(在计算方面),那么这是一个胜利,因为您将利用更多的计算单元。但是,如果单个线程已经使用了 100% 的计算单元(如经过良好优化的矩阵-矩阵乘积的情况),那么您将失去性能,因为 1) 管理两个线程的自然开销和 2) 因为 L1缓存现在由两个不同的任务共享。矩阵-矩阵内核在设计时考虑到了精确的 L1 容量。使用两个线程,您的 L1 缓存几乎变得无效。这意味着大多数时候您不是获取非常快的 L1 缓存,而是访问速度慢得多的 L2 缓存,因此性能会大幅下降。与 Linux 和 Windows 不同,在 OSX 上我没有观察到这种性能下降,很可能是因为如果 CPU 已经太多,系统能够取消调度第二个线程忙。
我正在尝试使用 OpenMP 来加速我的神经网络计算代码。由于我使用的是 Visual Studio 2017,因此我需要在 属性 工作表中启用 OpenMP 支持。然而,在我这样做之后,即使我没有在代码中包含任何#pragma omp
,代码的某些部分也会减慢大约 5 倍。
我已经隔离了这些部分,发现是这个特定的函数导致了问题:
void foo(Eigen::Matrix<float,3,Eigen::Dynamic> inputPts)
{
std::vector<Eigen::MatrixXf> activation;
activation.reserve(layerNo);
activation.push_back(inputPts);
int inputNo = inputPts.cols();
for (int i = 0; i < layerNo - 2; i++)
activation.push_back(((weights[i]*activation[i]).colwise()+bias[i]).array().tanh());
activation.push_back(((weights[layerNo - 2]*activation[layerNo - 2]).colwise()+bias[layerNo - 2]));
val = activation[layerNo - 1]/scalingFactor;
std::vector<Eigen::MatrixXf> delta;
delta.reserve(layerNo);
Eigen::Matrix<float, 1, Eigen::Dynamic> seed;
seed.setOnes(1, inputNo);
delta.push_back(seed);
for (int i = layerNo - 2; i >= 1; i--)
{
Eigen::Matrix<float,Eigen::Dynamic,Eigen::Dynamic>
d_temp = weights[i].transpose()*delta[layerNo - 2 - i],
d_temp2 = 1 - activation[i].array().square(),
deltaLayer = d_temp.cwiseProduct(d_temp2);
delta.push_back(deltaLayer);
}
grad = weights[0].transpose()*delta[layerNo - 2];
}
两个 for 循环的速度明显变慢(从 ~3ms 到 ~20ms)。奇怪的是,这个函数虽然在程序中被调用了很多次,但只有一部分受到影响。
我已经包含了头文件<omp.h>
。我不确定这是否是由于到处都在使用的 Eigen 库。我尝试按照 official site 中的建议定义 EIGEN_DONT_PARALLELIZE
并调用 Eigen::initParallel()
,但没有帮助。
奇怪的是,我什至没有包含任何 parallel pragma
,处理 OpenMP 函数应该没有任何开销吧?为什么它还在变慢?
如果启用了 OpenMP,Eigen 的矩阵矩阵产品默认是多线程的。问题可能是以下组合:
- 您的 CPU 是超线程的,例如,您有 4 个物理内核能够 运行 8 个线程。
- OpenMP 不允许知道物理内核的数量,因此 Eigen 将启动 8 个线程。
- Eigen 的矩阵-矩阵乘积内核经过全面优化,利用了近 100% 的 CPU 容量。因此,运行在单个内核上没有空间容纳两个这样的线程,并且性能显着下降(缓存污染)。
因此,解决方案是将 OpenMP 线程的数量限制为物理内核的数量,例如通过设置 OMP_NUM_THREADS 环境变量。也可以通过在编译时定义宏EIGEN_DONT_PARALLELIZE
来禁用Eigen的多线程。
doc 中的更多信息。
有关超线程如何降低性能的更多详细信息: 使用超线程,您可以在单个内核上以交错方式拥有两个线程 运行。他们交替执行每条指令。如果您的线程未使用少于 CPU 资源的一半(在计算方面),那么这是一个胜利,因为您将利用更多的计算单元。但是,如果单个线程已经使用了 100% 的计算单元(如经过良好优化的矩阵-矩阵乘积的情况),那么您将失去性能,因为 1) 管理两个线程的自然开销和 2) 因为 L1缓存现在由两个不同的任务共享。矩阵-矩阵内核在设计时考虑到了精确的 L1 容量。使用两个线程,您的 L1 缓存几乎变得无效。这意味着大多数时候您不是获取非常快的 L1 缓存,而是访问速度慢得多的 L2 缓存,因此性能会大幅下降。与 Linux 和 Windows 不同,在 OSX 上我没有观察到这种性能下降,很可能是因为如果 CPU 已经太多,系统能够取消调度第二个线程忙。