我应该如何解释这些 VTune 结果?

Posted

技术标签:

【中文标题】我应该如何解释这些 VTune 结果?【英文标题】:How should I interpreter these VTune results? 【发布时间】:2017-05-08 09:40:53 【问题描述】:

我正在尝试使用 OpenMP 并行化 this 代码。 OpenCV(使用 IPP 构建以获得最佳效率)用作外部库。

我在parallel fors 中遇到了 CPU 使用不平衡的问题,但似乎没有负载不平衡。正如您将看到的,这可能是因为KMP_BLOCKTIME=0,但由于外部库(IPP、TBB、OpenMP、OpenCV),这可能是必要的。在其余的问题中,您将找到更多可以下载的详细信息和数据。

这些是指向我的 VTune 结果的 Google Drive 链接:

c755823 basic KMP_BLOCKTIME=0 30 runs : 环境变量 KMP_BLOCKTIME 在相同输入的 30 次运行中设置为 0 的基本热点

c755823 basic 30 runs : 同上,但默认 KMP_BLOCKTIME=200

c755823 advanced KMP_BLOCKTIME=0 30 runs : 同第一个,但是高级热点

有兴趣的可以把原代码发给你。

在我的 Intel i7-4700MQ 上,应用程序运行 10 次的实际挂钟时间平均约为 0.73 秒。我使用 icpc 2017 update 3 编译代码,并带有以下编译器标志:

INTEL_OPT=-O3 -ipo -simd -xCORE-AVX2 -parallel -qopenmp -fargument-noalias -ansi-alias -no-prec-div -fp-model fast=2 -fma -align -finline-functions    
INTEL_PROFILE=-g -qopt-report=5 -Bdynamic -shared-intel -debug inline-debug-info -qopenmp-link dynamic -parallel-source-info=2 -ldl

此外,我设置了KMP_BLOCKTIME=0,因为默认值 (200) 会产生巨大的开销。

我们可以将代码分成 3 个并行区域(为了提高效率,只包裹在一个 #pragma parallel 中)和一个之前的串行区域,这大约是算法的 25%(并且不能并行化)。

我会尝试描述它们(或者你可以直接跳到代码结构):

    我们创建一个parallel 区域以避免创建新并行区域的开销。最终结果是填充矩阵对象cv::Mat descriptor 的行。我们有 3 个共享的 std::vector 对象:(a)blurs 这是一个模糊链(不可并行化),使用 OpenCV 的 GuassianBlur(它使用高斯模糊的 IPP 实现)(b)hessResps(大小已知,比如 32) (c) findAffineShapeArgs(未知大小,但按数千个元素的顺序,比如 2.3k)(d)cv::Mat descriptors(未知大小,最终结果)。在串行部分,我们填充 `blurs,这是一个只读向量。 在第一个并行区域中,hessResps 使用 blurs 填充,没有任何同步机制。 在第二个并行区域中,findLevelKeypoints 使用 hessResps 以只读方式填充。由于findAffineShapeArgs 的大小未知,我们需要一个局部向量localfindAffineShapeArgs,它将在下一步中附加到findAffineShapeArgs 由于findAffineShapeArgs 是共享的并且它的大小未知,我们需要一个critical 部分,每个localfindAffineShapeArgs 都附加到它上面。 在第三个并行区域中,每个findAffineShapeArgs用于生成最终cv::Mat descriptor的行。同样,由于descriptors 是共享的,我们需要一个本地版本cv::Mat localDescriptors。 最后一个critical 部分push_back 每个localDescriptorsdescriptors。请注意,这是非常快的,因为 cv::Mat 是“有点”智能指针,所以我们使用 push_back 指针。

这是代码结构:

cv::Mat descriptors;
std::vector<Mat> blurs(blursSize);
std::vector<Mat> hessResps(32);
std::vector<FindAffineShapeArgs> findAffineShapeArgs;//we don't know its tsize in advance

#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++)
    
       hessResps[/**/] = hessianResponse(/*...*/);
    

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++)
    findLevelKeypoints(localfindAffineShapeArgs, hessResps[/*...*], /*...*/); //populate localfindAffineShapeArgs with push_back


#pragma omp critical
    findAffineShapeArgs.insert(findAffineShapeArgs.end(), localfindAffineShapeArgs.begin(), localfindAffineShapeArgs.end());


#pragma omp barrier
#pragma omp for schedule(dynamic) nowait
for(int i=0; i<findAffineShapeArgs.size(); i++)

  findAffineShape(findAffineShapeArgs[i]);


#pragma omp critical
  for(size_t i=0; i<localRes.size(); i++)
    descriptors.push_back(localRes[i].descriptor);


在问题的最后,你可以找到FindAffineShapeArgs

我正在使用 Intel Amplifier 查看热点并评估我的应用程序。

OpenMP 潜在增益分析表明,如果存在完美的负载平衡,潜在增益将为 5.8%,因此我们可以说工作负载在不同 CPU 之间是平衡的。

这是 OpenMP 区域的 CPU 使用率直方图(请记住,这是连续运行 10 次的结果):

如您所见,平均 CPU 使用率为 7 核,这很好。

此 OpenMP 区域持续时间直方图显示,在这 10 次运行中,并行区域始终以相同的时间执行(分布在 4 毫秒左右):

这是呼叫者/被呼叫者标签:

供您参考:

interpolate 在最后一个并行区域被调用 l9_ownFilter*函数都在最后一个并行区域调用 samplePatch 在最后一个并行区域中被调用。 在第二个并行区域调用hessianResponse

现在,我的第一个问题是:我应该如何解释上面的数据?正如你所看到的,在许多函数中,“Effective Time by Utilization”有一半时间是“ok”的,如果内核更多,它可能会变得“Poor”(例如在 KNL 机器上,我将在其中测试下一个申请)。

最后,这是Wait and Lock分析结果:

现在,这是第一个奇怪的东西:line276Join Barrier(对应最昂贵的等待对象) is#pragma omp parallel`,所以是parallel region的开始。所以看来有人催生了线程之前。我错了吗?另外,等待时间比程序本身长(0.827s vs 1.253s 的 Join Barrier,我正在谈论)!但也许这是指所有线程的等待(而不是挂钟时间,这显然是不可能的,因为它比程序本身还要长)。

那么,第 312 行的 Explicit Barrier 就是上面代码的#pragma omp barrier,它的持续时间是 0.183s。

查看调用者/被调用者选项卡:

如你所见,大部分等待时间都很短,所以它指的是一个线程。但我确信我理解这一点。 我的第二个问题是:我们可以将其解释为“所有线程都在等待一个留在后面的线程吗?”。

FindAffineShapeArgs 定义:

struct FindAffineShapeArgs

    FindAffineShapeArgs(float x, float y, float s, float pixelDistance, float type, float response, const Wrapper &wrapper) :
        x(x), y(y), s(s), pixelDistance(pixelDistance), type(type), response(response), wrapper(std::cref(wrapper)) 

    float x, y, s;
    float pixelDistance, type, response;
    std::reference_wrapper<Wrapper const> wrapper;
;

摘要视图中按潜在增益排名前 5 位的平行区域仅显示一个区域(唯一一个)

查看“/OpenMP Region/OpenMP Barrier-to-Barrier”分组,这是最昂贵的循环的顺序:

第三个循环:

pragma omp for schedule(dynamic) nowait

for(int i=0; 我

是最昂贵的(据我所知),这是扩展视图的屏幕截图:

如您所见,许多功能来自 OpenCV,它利用 IPP 并且(应该)已经优化。展开其他两个函数(interpolate 和 samplePatch)会显示 [No call stack information]。所有其他功能都相同(在其他地区也是如此)。

第二个最昂贵的区域是第二个平行的:

#pragma omp for collapse(2) schedule(dynamic) nowait
for(int i=0; i<levels; i++)
    for (int j = 2; j < scaleCycles; j++)
    findLevelKeypoints(localfindAffineShapeArgs, hessResps[/*...*], /*...*/); //populate localfindAffineShapeArgs with push_back

这是展开的视图:

最后第三个最昂贵的是第一个循环:

#pragma omp for collapse(2) schedule(dynamic)
for(int i=0; i<levels; i++)
    for (int j = 1; j <= scaleCycles; j++)
    
       hessResps[/**/] = hessianResponse(/*...*/);
    

这是扩展视图:

如果您想了解更多信息,请使用我随附的 VTune 文件或直接询问!

【问题讨论】:

你应该尽量在开始的时候总结问题和你的问题,然后最终提供所有的数据。像现在这样,读起来太长了 感谢@Antonio 的评论,我更新了这个问题并做了一个小介绍,请查看。 很抱歉,我对 x86 硬件优化和那些诊断工具都没有经验,我刚收到通知要看看这个。在我的 ARM 机器上,我避免混合使用 openmptbbopencv 函数与 tbb 完美平衡运行,而在 opencv 之外我使用 openmp。我无法深入研究您的项目来判断这是否是问题所在。您是否尝试将其分解以尽可能降低复杂性? 【参考方案1】:

尝试阅读link 中的信息,尤其是关于“嵌套 OpenMP”的部分,因为英特尔 IPP 已经在其实施中使用了 OpenMP。根据我使用英特尔 IPP 和 OpenMP 的经验,如果您正在执行某种其他类型的多线程,并且当每个创建的线程到达 OpenMP 调用时,性能真的很差。此外,您可以尝试为每个并行区域设置 #pragma omp parallel for 而不是 #pragma omp for 并摆脱外部 #pragma omp parallel

【讨论】:

以上是关于我应该如何解释这些 VTune 结果?的主要内容,如果未能解决你的问题,请参考以下文章

如何汇总英特尔放大器批处理结果?

我应该如何解释 Apache 的 ab 基准测试工具的结果?

CPU 使用率和并发直方图的 VTUNE 结果

无法在 DPDK 上重现 Intel Vtune 分析示例的结果

我们怎么知道代码是线程安全的?

OpenMP、VTune、空闲线程