首页 > 数据库 >C++ 多线程代码性能分析——Oracle Developer Studio工具教程

C++ 多线程代码性能分析——Oracle Developer Studio工具教程

时间:2024-09-09 13:54:30浏览次数:9  
标签:多线程 函数 C++ fillVec 调用 线程 Studio main

        最近写项目的时候,为了提升性能,把原来一些单线程的代码改成了并行运行。这里我用到的用于评估性能提升效果的工具是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被分为了两类:ExclusiveInclusive. 在这里,Exclusive time 指的是某个函数自身执行所消耗的时间,不包括它调用的其他函数的时间。也就是说,exclusive time 只计算函数内部执行的指令,而忽略它调用的其他函数的执行时间。例如,如果函数 A 调用了函数 BC,exclusive time 仅为 A 自身的代码执行时间,不包括 BC 的执行时间。

        Inclusive time 则包括某个函数的执行时间以及它调用的所有其他函数的执行时间。也就是说,inclusive time 是指某个函数从开始执行到结束时,所花费的总时间,包括它调用的所有子函数的执行时间。例如,对于函数 A,inclusive time 是 A 的执行时间加上它调用的函数 BC 的执行时间之和。

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

相关文章

  • C++20 协程:异步编程的新纪元
    C++20引入了协程(coroutines),这是一种全新的异步编程模型,使得编写异步代码变得更加简洁和直观。本文将详细介绍C++20协程的概念、功能演变及其在实际项目中的应用。通过本文,你将了解到协程的基本原理、语法和如何利用协程来简化异步编程。1.协程的概念协程(coroutine)是......
  • C++---内存管理
    1C/C++内存分布栈区:由编译器自动分配和释放,存放运行时候的局部变量,函数参数,返回数据,返回地址。堆区:一般由程序员自己分配,然后自己释放,例如栈的实现malloc开辟的数组空间。数据段(静态区):存放全局变量,静态数据,常量,程序结束后自动释放。代码段(常量区):存放常量字符串和可执行代......
  • C++--static成员和友元
    1static声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化静态定义的成员变量在类外定义,变量类型类名::变量名=value的形式。此外,static还可以在类里面定义......
  • Visual Studio 卸载和移除项目的区别
    1.卸载项目(UnloadProject)操作描述:卸载项目是将项目从当前解决方案中暂时移除,但项目文件仍然保留在磁盘上。这个操作不会删除项目文件,只是将项目从解决方案的视图中移除。卸载后,项目不再参与编译和调试,但项目文件和内容仍然存在于你的文件系统中。卸载项目通常用于不再需要频繁访......
  • 多线程篇(阻塞队列- DelayQueue)(持续更新迭代)
    目录一、简介二、基本原理四、代码示例简单定时调度任务多消费者定时调度任务得出结论四、应用场景一、简介DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到......
  • 多线程篇(阻塞队列- PriorityBlockingQueue)(持续更新迭代)
    目录一、简介二、类图三、源码解析1.字段讲解2.构造方法3.入队方法put浮调整比较器方法的实现入队图解4.出队方法takedequeue下沉调整比较器方法的实现出队图解四、总结一、简介PriorityBlockingQueue队列是JDK1.5的时候出来的一个阻塞队列。但是该队......
  • C++期末复习超详细总结知识点(期末冲刺)
    指数形式(即浮点形式)1.5、字符型常量作用:字符型变量用于显示单个字符语法:charch='a';注意1:在显示字符型变量时,用单引号将字符括起来,不要用双引号注意2:单引号内只能有一个字符,不可以是字符串C和C++中字符型变量只占用1个字节。字符常量只能包括一个字符,如......
  • 揭秘 C++ List 容器背后的实现原理,带你构建自己的双向链表
    本篇博客,我们将详细讲解如何从头实现一个功能齐全且强大的C++List容器,并深入到各个细节。这篇博客将包括每一步的代码实现、解释以及扩展功能的探讨,目标是让初学者也能轻松理解。一、简介1.1、背景介绍在C++中,std::list是一个基于双向链表的容器,允许高效的插入和......
  • 如何实现标准库般强大的 C++ Vector?:从动态扩容到移动语义到迭代器全覆盖
    在C++中,std::vector是最常用的动态数组容器。它具有高效的内存管理、动态扩容、随机访问等特点。在这篇博客中,我们将从零开始,实现一个功能强大、灵活、并且具有高性能的Vector类,具备std::vector的大部分功能,包括动态扩容、迭代器、模板支持、随机访问等,尽可能模仿C+......
  • androidstudio报错devicemanager出错问题
    2024-09-0911:01:57,029[1446798]WARN-Emulator:Pixel8ProAPI35-Failedtoprocess.inifileC:\Users\钁f旦.android\avd<build>.iniforreading.如如何解决1.查日志C:\Users\董浩\AppData\Local\Google\AndroidStudio2024.1\log这个是默认位置我的错误是202......