最近写项目的时候,为了提升性能,把原来一些单线程的代码改成了并行运行。这里我用到的用于评估性能提升效果的工具是Oracle Developer Studio,不过刚上手时,发现网上相关的教程和博客较少,有些功能的使用也是摸索着过来的,这一过程可谓是十分痛苦了……如今距离初次接触这一工具已经过去了一段时间,在这里分享一下Oracle Developer Studio工具的使用方法和一些小tips,希望可以帮到有需要的朋友。本篇博客中使用pthread实现了简单的多线程功能,并以此为例,展示了多线程代码的性能分析流程。
1. 工具介绍
Oracle Developer Studio 是一套功能强大且全面的开发工具,专为高性能计算和并行应用的开发而设计。它通过提供优化的编译器、强大的调试工具和深入的性能分析器。也就是说,其实这套工具并不仅仅可以用于线程分析,它还能提供以下功能:
- 编译:提供高度优化的 C、C++ 和 Fortran 编译器,能够生成高效的机器代码,适用于多核、并行系统的应用程序,并且支持 OpenMP、MPI 等并行编程模型,能够帮助开发者更好地利用多核处理器的优势。
- 调试:提供基于 GUI 的调试工具(dbx, 使用方式类似gdb),支持多线程和分布式程序的调试。
- 性能分析:支持对程序的性能进行分析,包括 CPU 使用率、内存占用、I/O 操作和多线程性能等。
可见这套工具的功能还是十分强大的,不过今天我们只介绍一下如何使用Developer Studio来进行线程分析。
2. 性能分析实例
2.1 单线程示例分析
我们实现一个简单的例子:
#include <iostream>
#include <vector>
using namespace std;
void fillVec(vector<vector<int>> & vec) {
for (int i = 0; i < vec.size(); ++i) {
for (int j = 0; j < 10000; ++j) {
vec[i].push_back(j);
}
}
return;
}
int main() {
vector<vector<int>> vec(10000);
fillVec(vec);
return 0;
}
为了让程序不要那么快跑完导致完全看不出效果,我们在这里写一个10000×10000的循环来作为演示。接下来我们要编译文件生成可执行文件,然后用Oracle的工具来收集程序运行期间的线程调用情况:
> g++ -o main main.cpp //生成可执行文件main
> collect main //使用工具运行可执行文件,收集数据
Creating experiment database test.1.er (Process ID: 3773) ...
>
> ls //结束后我们看看当前路径下有些什么文件
main main.cpp test.1.er
我们使用命令collect来收集程序运行的信息,结束后我们会看到在当前运行路径下生成了一个后缀为.er的文件夹,这就是工具用来存储收集到的信息的地方。接下来我们用analyzer来打开文件夹看一看:
> analyzer test.1.er
这条命令会打开一个GUI界面,如果项目很庞大,运行时间很长的话,这里的加载时间也会比较久。打开后首先是一个Overview的界面,可以看到项目运行的CPU Time和Experiment Duration Time:
可以看到,Overview中显示了两个时间,都是2.1秒。这里的意思是:这个程序总共运行了2.1秒(Experiment Duration Time),CPU所有线程运行的时间(Total CPU Time)加起来也是2.1秒。因为目前我们的程序是单线程运行的,所以这里的两个时间相同。稍后我们会尝试用多线程再次实现该功能,届时再来对比两者的差异。
我们常用到的几个功能也一并展示在左边。在我个人的实际分析经历中,主要用到的功能分别是Functions, Timeline, Call Tree和Callers-Callees. 接下来逐一介绍一下它们:
1. Functions
这里完整记录了程序的函数调用情况,图中可以看到我们作为示例的函数fillVec。
值得注意的是左侧的Total CPU Time被分为了两类:Exclusive和Inclusive. 在这里,Exclusive time 指的是某个函数自身执行所消耗的时间,不包括它调用的其他函数的时间。也就是说,exclusive time 只计算函数内部执行的指令,而忽略它调用的其他函数的执行时间。例如,如果函数 A
调用了函数 B
和 C
,exclusive time 仅为 A
自身的代码执行时间,不包括 B
和 C
的执行时间。
Inclusive time 则包括某个函数的执行时间以及它调用的所有其他函数的执行时间。也就是说,inclusive time 是指某个函数从开始执行到结束时,所花费的总时间,包括它调用的所有子函数的执行时间。例如,对于函数 A
,inclusive time 是 A
的执行时间加上它调用的函数 B
和 C
的执行时间之和。
2. Timeline
这里展示的是线程的调用情况。一般来说,我们可以很直观地在这里看到程序运行期间多线程的执行情况。由于目前我们还没有执行多线程操作,所以这里只有一个线程T:1。
3. Call Tree
这里层次化地展示了函数的调用情况,另外也以百分比展示了函数占用调用时间的百分比。从这个例子可以看出,fillVec函数被main给调用,而且fillVec的操作占据了main函数100%的时间。而在fillVec函数内,std:vector的push_back操作占用了整个函数的79%的时间。
4. Callers-Callees
这个功能可以展示特定函数的调用和被调用情况。在我们的例子中,可以看出,对于函数fillVec来说,它的调用者是main函数(上方),而它本身则调用了vector::push_back和vector::operator[]方法(下方)。注意此时fillVec左侧展示的CPU Time为exclusive time, 因为它调用的两个函数所花费的时间被分别展示在了界面下方。
我们用于线程分析的基本功能已经介绍完毕,接下来我们尝试多线程地实现一下fillVec功能,来看看性能会发生什么变化。
2.2 多线程功能实现
这里我们使用pthread实现fillVec函数,对于外部的for循环,我们把它替换为多线程并行处理,并在编译时添加-pthread选项来告诉编译器链接 POSIX 线程库:
#include <iostream>
#include <vector>
#include <thread>
#include <functional>
using namespace std;
void fillVecSegment(vector<vector<int>> &vec, int start, int end) {
for (int i = start; i < end; ++i) {
for (int j = 0; j < 10000; ++j) {
vec[i].push_back(j);
}
}
}
void fillVec(vector<vector<int>> &vec) {
int numThreads = thread::hardware_concurrency(); // 获取可用的硬件线程数量
vector<thread> threads;
int chunkSize = vec.size() / numThreads; // 每个线程处理的块大小
// 为每个线程分配一部分任务
for (int i = 0; i < numThreads; ++i) {
int start = i * chunkSize;
int end = (i == numThreads - 1) ? vec.size() : (i + 1) * chunkSize; // 最后一个线程可能多余
threads.push_back(thread(fillVecSegment, ref(vec), start, end));
}
// 等待所有线程完成
for (auto &t : threads) {
t.join();
}
}
int main() {
vector<vector<int>> vec(10000);
fillVec(vec);
return 0;
}
编译,使用Oracle运行,收集信息并打开:
> g++ -o mainMultiThread main.cpp -pthread //生成可执行文件main, 增加-pthread
> collect mainMultiThread //使用工具运行可执行文件,收集数据
Creating experiment database test.2.er (Process ID: 16249) ...
>
> ls //结束后我们看看当前路径下有些什么文件
main mainMultiThread main.cpp test.1.er test.2.er
> analyzer test.2.er //打开多线程版本的文件
接下来,打开Oracle Developer Studio后,我们会看到Overview界面显示的程序运行时间和之前相比发生了一些变化:
这一次,Total CPU Time仍然是大约2.1秒,但是Experiment Duration从之前的2.1秒下降到了1.2秒,这意味着程序顺利地以多线程模式运行。一般来说,Total CPU Time的变化不会太明显,因为多线程技术只是把任务分配给多个CPU线程做处理,而实际的任务总量并不会增多或是减少。另一方面,启动多线程、合并多线程往往会有一些额外的消耗,因此Total CPU Time可能会因为使用多线程而略微增大。另一方面,Experiment Duration 代表的是程序实际运行的时间,并行程序往往能够让程序运行时间减少。
* 特别值得注意的是,对于大型项目来说,这里的Experiment Duration并不应该被作为判断优化是否成功的标准。原因有二:
1. 对于动辄运行时间上万秒的程序,如果只是做了局部并行优化,往往在总的运行时间上表现不够直观,需要我们做进一步细节分析。
2. 程序多次运行有时是在不同服务器上实现的,会导致每次运行的时间结果有一些偏差。这会给我们的分析带来一定干扰。如果我们的改动较为轻微的话,可能会出现优化效果还抵不过程序运行时间的波动的情况。
接下来我们结合先前介绍的功能来对程序进行进一步的分析:
观察Functions界面,fillVecSegment函数承担起之前fillVec的功能,这里Inclusive CPU Time仍然保持在2.1秒左右,符合我们的预期。
Call Tree界面如图,和之前的结构相似,但值得注意的是,函数的调用结构比之前简单的 "main -> fillVec" 要复杂了许多。这是由于我们使用pthread实现了多线程化,程序内部包含了许多和pthread有关的调用关系(例如图上的std::thread::_Invoker, std::thread::_State_impl...)。
类似地,来到Callers-Callees界面,可以看到fillVecSegment函数的调用关系和单线程模式下相比不同。接下来就可以来到最直观、最常用的界面:Timeline了:
这里我们不难看出,这里总共展示了3个Thread,而中间部分有两个线程在并行运行(T:1和T:2),并行结束后由T:3 统一进行了join等操作。我们选中中间的某一段,右下角会显示当前程序在运行的函数,如图所示:
从右下角的信息我们可以看到,这里确实是在运行fillVecSegment函数,结合图形界面我们才可以确定,我们的多线程确实启动成功了。另外,从上方的Time我们可以看出,多线程的过程大约从0.3s开始,直到约1.2s结束,和之前的单线程对比,确实让程序运行得更快了。
在实际的分析过程中,我们也应该遵循这样的原则:找到我们多线程化的代码位置,查看多线程启用情况,结合上方的程序运行时间进行性能分析。
2.3 对于大型项目
我们简单的例子到这里就告一段落了。然而,在处理大型项目时,往往会遇到更加棘手的问题,其中曾经最让我感到头痛的就是:程序运行总时长已经上万秒了,我个人修改的函数所占用的时间最多只有几十秒,我该如何在漫长且凌乱的Timeline中找到我所修改的函数呢?
如果我们对项目运行流程比较熟悉,可以通过查看Timeline中的函数调用过程,来快速定位关注的函数位置。而对于多人协同开发/对总体项目流程不熟悉的情况来说,有一些小tips能帮助你快速找到修改的函数:
在Timeline界面正上方,有一个由红绿蓝三色圆形组成的图标(Call Stack Function Colors, Ctrl+Shift+F),我们打开后,会出现Function Colors. 这个选项用于给Timeline中的代码块染色。而我们找到函数的方法很简单:把其他函数的颜色设置成灰色,再把我们关心的函数设置成红色。
在选项卡中选中一个颜色作为不关注的颜色(这里以灰色为例),然后点击下方Set All Functions:
可以看到,当前Timeline中的所有函数都变成了灰色。接下来我们希望找出fillVecSegment函数,所以在下方输入fillVec,把匹配模式设定为Contains,这里的意思是“我们希望寻找函数名中包含fillVec关键字的函数”。然后点击左侧set Functions.
进行到这一步后,我们会发现,Timeline中并没有如我们所预期地出现红色段落。这里暂且以刚刚的代码为例,我们点击多线程中的一个代码块:
然后查看右下角的Call Stack:
可以发现,fillVecSegment函数确实被染上红色了。那么为什么Timeline中看不到效果呢?原因同样可以在Call Stack界面中找到。可以看出,这里同时还调用了许多其他函数,都用灰色显示,而红色段只占其中的很小一部分。所以答案很显然:Timeline中显示的长度太短,以至于无法看到红色。让我们回到Timelin中,点击上方左侧的+号来把每个线程显示的信息拉高:
这样我们终于能直观地看出哪些地方调用到了fillVecSegment函数了。
3. 写在最后
至此我们的示例就结束了。最后还想分享一些我在面对大型项目时实际遇到的问题:
我有这样一段函数结构:
void A {
B();
C();
}
我将A函数更改为多线程实现,其中我大概知道B函数运行较慢,而C函数运行应该很快。在使用Oracle Developer Studio进行分析的时候,我发现在Functions界面、Call Tree界面中无法查询到C函数,但是A和B却都被调用了,Timeline也显示代码被正常地多线程化了,效果很好。遇到这样的情况可能是因为Oracle在进行函数采样时,采样间隔较长,因此没有采到运行时间过短的C函数。因此,有时进行线程分析时也需要选择合适的函数,否则可能会出现找不到函数的情况。
以上就是全部内容,希望能够帮助到大家。
标签:多线程,函数,C++,fillVec,调用,线程,Studio,main From: https://blog.csdn.net/m0_52437597/article/details/141937795