kmp OpenMP 中未知调用的巨大开销和旋转时间?
_kmp huge overhead and spin time for unkown calls in OpenMP?
我正在使用 Intel VTune 分析我的并行应用程序。
如您所见,在应用程序的开头有一个巨大的 Spin Time(表示为左侧的橙色部分):
它超过了应用程序持续时间的 28%(大约为 0.14 秒)!
如您所见,这些函数是 _clone
、start_thread
、_kmp_launch_thread
和 _kmp_fork_barrier
,它们看起来像 OpenMP 内部或系统调用,但未指定从哪里调用这些函数。
此外,如果我们放大本节的开头,我们可以注意到一个区域实例化,由所选区域表示:
但是,我从不调用initInterTab2d
而且我不知道它是否被我正在使用的一些实验室调用(尤其是OpenCV)。
深入挖掘和 运行 高级热点分析我发现了更多关于第一个未知函数的信息:
并扩展 Function/Call 堆栈选项卡:
但是,我还是不能真正理解为什么这些函数,为什么它们需要这么长时间以及为什么只有主线程在它们期间工作,而其他线程处于 "barrier" 状态。
如果您有兴趣,this 是部分代码的 link。
请注意,我只有一个 #pragma omp parallel
区域,这是该图像的选定部分(在右侧):
代码结构如下:
- 计算一些串行的、不可并行的东西。特别是,计算一个模糊链,由
gaussianBlur
表示(包含在代码末尾)。 cv::GaussianBlur
是一个利用 IPP 的 OpenCV 函数。
- 开始并行区域,其中使用了3个
parallel for
- 第一个调用
hessianResponse
- 单个线程将结果添加到共享向量。
- 第二个并行区域
localfindAffineShapeArgs
生成下一个并行区域使用的数据。由于负载不平衡,两个区域无法合并。
- 第三个区域以平衡的方式生成最终结果。
- 注:根据VTune的锁分析,
critical
和barrier
段不是自旋的原因
这是代码的主要功能:
void HessianDetector::detectPyramidKeypoints(const Mat &image, cv::Mat &descriptors, const AffineShapeParams ap, const SIFTDescriptorParams sp)
{
float curSigma = 0.5f;
float pixelDistance = 1.0f;
cv::Mat octaveLayer;
// prepare first octave input image
if (par.initialSigma > curSigma)
{
float sigma = sqrt(par.initialSigma * par.initialSigma - curSigma * curSigma);
octaveLayer = gaussianBlur(image, sigma);
}
// while there is sufficient size of image
int minSize = 2 * par.border + 2;
int rowsCounter = image.rows;
int colsCounter = image.cols;
float sigmaStep = pow(2.0f, 1.0f / (float) par.numberOfScales);
int levels = 0;
while (rowsCounter > minSize && colsCounter > minSize){
rowsCounter/=2; colsCounter/=2;
levels++;
}
int scaleCycles = par.numberOfScales+2;
//-------------------Shared Vectors-------------------
std::vector<Mat> blurs (scaleCycles*levels+1, Mat());
std::vector<Mat> hessResps (levels*scaleCycles+2); //+2 because high needs an extra one
std::vector<Wrapper> localWrappers;
std::vector<FindAffineShapeArgs> findAffineShapeArgs;
localWrappers.reserve(levels*(scaleCycles-2));
vector<float> pixelDistances;
pixelDistances.reserve(levels);
for(int i=0; i<levels; i++){
pixelDistances.push_back(pixelDistance);
pixelDistance*=2;
}
//compute blurs at all layers (not parallelizable)
for(int i=0; i<levels; i++){
blurs[i*scaleCycles+1] = octaveLayer.clone();
for (int j = 1; j < scaleCycles; j++){
float sigma = par.sigmas[j]* sqrt(sigmaStep * sigmaStep - 1.0f);
blurs[j+1+i*scaleCycles] = gaussianBlur(blurs[j+i*scaleCycles], sigma);
if(j == par.numberOfScales)
octaveLayer = halfImage(blurs[j+1+i*scaleCycles]);
}
}
#pragma omp parallel
{
//compute all the hessianResponses
#pragma omp for collapse(2) schedule(dynamic)
for(int i=0; i<levels; i++)
for (int j = 1; j <= scaleCycles; j++)
{
int scaleCyclesLevel = scaleCycles * i;
float curSigma = par.sigmas[j];
hessResps[j+scaleCyclesLevel] = hessianResponse(blurs[j+scaleCyclesLevel], curSigma*curSigma);
}
//we need to allocate here localWrappers to keep alive the reference for FindAffineShapeArgs
#pragma omp single
{
for(int i=0; i<levels; i++)
for (int j = 2; j < scaleCycles; j++){
int scaleCyclesLevel = scaleCycles * i;
localWrappers.push_back(Wrapper(sp, ap, hessResps[j+scaleCyclesLevel-1], hessResps[j+scaleCyclesLevel], hessResps[j+scaleCyclesLevel+1],
blurs[j+scaleCyclesLevel-1], blurs[j+scaleCyclesLevel]));
}
}
std::vector<FindAffineShapeArgs> localfindAffineShapeArgs;
#pragma omp for collapse(2) schedule(dynamic) nowait
for(int i=0; i<levels; i++)
for (int j = 2; j < scaleCycles; j++){
size_t c = (scaleCycles-2) * i +j-2;
//toDo: octaveMap is shared, need synchronization
//if(j==1)
// octaveMap = Mat::zeros(blurs[scaleCyclesLevel+1].rows, blurs[scaleCyclesLevel+1].cols, CV_8UC1);
float curSigma = par.sigmas[j];
// find keypoints in this part of octave for curLevel
findLevelKeypoints(curSigma, pixelDistances[i], localWrappers[c]);
localfindAffineShapeArgs.insert(localfindAffineShapeArgs.end(), localWrappers[c].findAffineShapeArgs.begin(), localWrappers[c].findAffineShapeArgs.end());
}
#pragma omp critical
{
findAffineShapeArgs.insert(findAffineShapeArgs.end(), localfindAffineShapeArgs.begin(), localfindAffineShapeArgs.end());
}
#pragma omp barrier
std::vector<Result> localRes;
#pragma omp for schedule(dynamic) nowait
for(int i=0; i<findAffineShapeArgs.size(); i++){
hessianKeypointCallback->onHessianKeypointDetected(findAffineShapeArgs[i], localRes);
}
#pragma omp critical
{
for(size_t i=0; i<localRes.size(); i++)
descriptors.push_back(localRes[i].descriptor);
}
}
Mat gaussianBlur(const Mat input, const float sigma)
{
Mat ret(input.rows, input.cols, input.type());
int size = (int)(2.0 * 3.0 * sigma + 1.0); if (size % 2 == 0) size++;
GaussianBlur(input, ret, Size(size, size), sigma, sigma, BORDER_REPLICATE);
return ret;
}
如果您认为 50 毫秒(眨眼的一小部分)一次成本是巨大的开销,那么您可能应该专注于您的工作流程。尝试以持久的方式使用一个完全初始化的进程(及其线程和数据结构)来增加每个 运行.
期间完成的工作
也就是说,可能会减少开销,但无论如何您都将非常依赖库的运行时间和初始化成本,从而限制您的性能可移植性。
你的性能分析也可能有问题。 AFAIK VTune 使用采样,您的数据表明采样间隔为 1 毫秒。这意味着您在应用程序的关键初始化路径中可能只有 50 个样本,对于可靠的分析来说太少了。 VTune 可能还具有某些形式的 OpenMP 检测,可在较小的时间范围内提供更准确的结果。在任何情况下,除非我确切知道测量有什么影响和方法,否则我会对仅超过 150 毫秒的性能测量持保留态度。
P.S。 运行 一个简单的代码,如:
#include <stdio.h>
#include <omp.h>
int main() {
double start = omp_get_wtime();
#pragma omp parallel
{
#pragma omp barrier
#pragma omp master
printf("%f s\n", omp_get_wtime() - start);
}
}
显示初始线程创建开销在 3 毫秒到 200 毫秒之间,在不同的系统/线程计数中使用英特尔 OpenMP 运行时间。
我正在使用 Intel VTune 分析我的并行应用程序。
如您所见,在应用程序的开头有一个巨大的 Spin Time(表示为左侧的橙色部分):
它超过了应用程序持续时间的 28%(大约为 0.14 秒)!
如您所见,这些函数是 _clone
、start_thread
、_kmp_launch_thread
和 _kmp_fork_barrier
,它们看起来像 OpenMP 内部或系统调用,但未指定从哪里调用这些函数。
此外,如果我们放大本节的开头,我们可以注意到一个区域实例化,由所选区域表示:
但是,我从不调用initInterTab2d
而且我不知道它是否被我正在使用的一些实验室调用(尤其是OpenCV)。
深入挖掘和 运行 高级热点分析我发现了更多关于第一个未知函数的信息:
并扩展 Function/Call 堆栈选项卡:
但是,我还是不能真正理解为什么这些函数,为什么它们需要这么长时间以及为什么只有主线程在它们期间工作,而其他线程处于 "barrier" 状态。
如果您有兴趣,this 是部分代码的 link。
请注意,我只有一个 #pragma omp parallel
区域,这是该图像的选定部分(在右侧):
代码结构如下:
- 计算一些串行的、不可并行的东西。特别是,计算一个模糊链,由
gaussianBlur
表示(包含在代码末尾)。cv::GaussianBlur
是一个利用 IPP 的 OpenCV 函数。 - 开始并行区域,其中使用了3个
parallel for
- 第一个调用
hessianResponse
- 单个线程将结果添加到共享向量。
- 第二个并行区域
localfindAffineShapeArgs
生成下一个并行区域使用的数据。由于负载不平衡,两个区域无法合并。 - 第三个区域以平衡的方式生成最终结果。
- 注:根据VTune的锁分析,
critical
和barrier
段不是自旋的原因
这是代码的主要功能:
void HessianDetector::detectPyramidKeypoints(const Mat &image, cv::Mat &descriptors, const AffineShapeParams ap, const SIFTDescriptorParams sp)
{
float curSigma = 0.5f;
float pixelDistance = 1.0f;
cv::Mat octaveLayer;
// prepare first octave input image
if (par.initialSigma > curSigma)
{
float sigma = sqrt(par.initialSigma * par.initialSigma - curSigma * curSigma);
octaveLayer = gaussianBlur(image, sigma);
}
// while there is sufficient size of image
int minSize = 2 * par.border + 2;
int rowsCounter = image.rows;
int colsCounter = image.cols;
float sigmaStep = pow(2.0f, 1.0f / (float) par.numberOfScales);
int levels = 0;
while (rowsCounter > minSize && colsCounter > minSize){
rowsCounter/=2; colsCounter/=2;
levels++;
}
int scaleCycles = par.numberOfScales+2;
//-------------------Shared Vectors-------------------
std::vector<Mat> blurs (scaleCycles*levels+1, Mat());
std::vector<Mat> hessResps (levels*scaleCycles+2); //+2 because high needs an extra one
std::vector<Wrapper> localWrappers;
std::vector<FindAffineShapeArgs> findAffineShapeArgs;
localWrappers.reserve(levels*(scaleCycles-2));
vector<float> pixelDistances;
pixelDistances.reserve(levels);
for(int i=0; i<levels; i++){
pixelDistances.push_back(pixelDistance);
pixelDistance*=2;
}
//compute blurs at all layers (not parallelizable)
for(int i=0; i<levels; i++){
blurs[i*scaleCycles+1] = octaveLayer.clone();
for (int j = 1; j < scaleCycles; j++){
float sigma = par.sigmas[j]* sqrt(sigmaStep * sigmaStep - 1.0f);
blurs[j+1+i*scaleCycles] = gaussianBlur(blurs[j+i*scaleCycles], sigma);
if(j == par.numberOfScales)
octaveLayer = halfImage(blurs[j+1+i*scaleCycles]);
}
}
#pragma omp parallel
{
//compute all the hessianResponses
#pragma omp for collapse(2) schedule(dynamic)
for(int i=0; i<levels; i++)
for (int j = 1; j <= scaleCycles; j++)
{
int scaleCyclesLevel = scaleCycles * i;
float curSigma = par.sigmas[j];
hessResps[j+scaleCyclesLevel] = hessianResponse(blurs[j+scaleCyclesLevel], curSigma*curSigma);
}
//we need to allocate here localWrappers to keep alive the reference for FindAffineShapeArgs
#pragma omp single
{
for(int i=0; i<levels; i++)
for (int j = 2; j < scaleCycles; j++){
int scaleCyclesLevel = scaleCycles * i;
localWrappers.push_back(Wrapper(sp, ap, hessResps[j+scaleCyclesLevel-1], hessResps[j+scaleCyclesLevel], hessResps[j+scaleCyclesLevel+1],
blurs[j+scaleCyclesLevel-1], blurs[j+scaleCyclesLevel]));
}
}
std::vector<FindAffineShapeArgs> localfindAffineShapeArgs;
#pragma omp for collapse(2) schedule(dynamic) nowait
for(int i=0; i<levels; i++)
for (int j = 2; j < scaleCycles; j++){
size_t c = (scaleCycles-2) * i +j-2;
//toDo: octaveMap is shared, need synchronization
//if(j==1)
// octaveMap = Mat::zeros(blurs[scaleCyclesLevel+1].rows, blurs[scaleCyclesLevel+1].cols, CV_8UC1);
float curSigma = par.sigmas[j];
// find keypoints in this part of octave for curLevel
findLevelKeypoints(curSigma, pixelDistances[i], localWrappers[c]);
localfindAffineShapeArgs.insert(localfindAffineShapeArgs.end(), localWrappers[c].findAffineShapeArgs.begin(), localWrappers[c].findAffineShapeArgs.end());
}
#pragma omp critical
{
findAffineShapeArgs.insert(findAffineShapeArgs.end(), localfindAffineShapeArgs.begin(), localfindAffineShapeArgs.end());
}
#pragma omp barrier
std::vector<Result> localRes;
#pragma omp for schedule(dynamic) nowait
for(int i=0; i<findAffineShapeArgs.size(); i++){
hessianKeypointCallback->onHessianKeypointDetected(findAffineShapeArgs[i], localRes);
}
#pragma omp critical
{
for(size_t i=0; i<localRes.size(); i++)
descriptors.push_back(localRes[i].descriptor);
}
}
Mat gaussianBlur(const Mat input, const float sigma)
{
Mat ret(input.rows, input.cols, input.type());
int size = (int)(2.0 * 3.0 * sigma + 1.0); if (size % 2 == 0) size++;
GaussianBlur(input, ret, Size(size, size), sigma, sigma, BORDER_REPLICATE);
return ret;
}
如果您认为 50 毫秒(眨眼的一小部分)一次成本是巨大的开销,那么您可能应该专注于您的工作流程。尝试以持久的方式使用一个完全初始化的进程(及其线程和数据结构)来增加每个 运行.
期间完成的工作也就是说,可能会减少开销,但无论如何您都将非常依赖库的运行时间和初始化成本,从而限制您的性能可移植性。
你的性能分析也可能有问题。 AFAIK VTune 使用采样,您的数据表明采样间隔为 1 毫秒。这意味着您在应用程序的关键初始化路径中可能只有 50 个样本,对于可靠的分析来说太少了。 VTune 可能还具有某些形式的 OpenMP 检测,可在较小的时间范围内提供更准确的结果。在任何情况下,除非我确切知道测量有什么影响和方法,否则我会对仅超过 150 毫秒的性能测量持保留态度。
P.S。 运行 一个简单的代码,如:
#include <stdio.h>
#include <omp.h>
int main() {
double start = omp_get_wtime();
#pragma omp parallel
{
#pragma omp barrier
#pragma omp master
printf("%f s\n", omp_get_wtime() - start);
}
}
显示初始线程创建开销在 3 毫秒到 200 毫秒之间,在不同的系统/线程计数中使用英特尔 OpenMP 运行时间。