本篇研究下Visual Studio自带的性能分析工具,针对C++代码,基于Visual Studio2022
文章目录
- CPU使用率
- 检测
- 并发可视化工具
- 使用率视图
- 线程视图
- 内核视图
- 并发可视化工具SDK
- 参考资料
CPU使用率
对于CPU密集型程序,我们可以通过分析程序的CPU使用率,找到程序的瓶颈,Visual Studio原生提供了对于CPU使用情况的诊断工具,具体使用方法如下:
-
在 Visual Studio 里打开我们的程序,在我们想分析的函数/代码块的起始位置和结束位置打上断点,比如这里我们想分析的是
getSum()
这个函数。
-
调试状态下运行我们的程序,然后打开诊断工具窗口(调试>窗口>显示诊断工具)
-
之后可以在诊断工具里,设置选择工具,选择是否查看 CPU使用率和内存使用
-
当程序运行到一个断点时,打开CPU 使用率下的记录 CPU 配置文件,之后就会开始记录CPU使用情况
-
继续执行我们的代码,然后程序运行到我们的第二个断点处,这是点开CPU使用率,就可以看到CPU使用率相关的信息了。
(1)CPU 总计:表示该函数函数体和调用的其他函数的总的CPU计数,及占比
(2)自CPU:表示该函数函数体本身的CPU计数,及占比
(3)排名靠前的函数:按自CPU计数从大到小排列,这里func1
函数自CPU 5152(99.52%)
(4)热路径:CPU计数最大部分的调用树,上面可以看到是getSum
中调用了func1
-
这里可以打开CPU 使用率的详细信息窗口,或者点击上面图中任意函数,导航到对应位置,这里视图默认显示是调用树,下面代码中会在左侧显示每一行代码的CPU计数。
-
我们可以选择视图为调用方/被调用方,选择对应的函数,当前函数就是我们选择的函数,可以看到调用函数就是调用当前函数的函数,调用的函数就是当前函数调用到的一些函数
-
也可以选择火焰图视图
检测
上面的CPU使用率是通过采样的方式得到,定时去检查CPU的调用堆栈,好处是不会产生大的开销,而检测是通过工具将代码注入到可捕获计时信息的二进制文件中,或通过使用hook在应用程序运行期间收集和发出精确计时和调用计数信息。相对于采样的方式,检测方法开销较大,但检测可提供更确切的调用计数和精确计时。
Visual Studio也提供了原生的检测工具,但对C++程序只能进行静态检测,具体步骤如下:
- 首先编译好的目标文件,在链接时需要加上
-PROFILE
选项 - 调试 > 性能探查器,打开性能探查器
. - 首先选择目标,这里选择启动项目,也可以选择链接好的可执行文件,选择检测选项,点击开始。
- 等程序运行结束或手动停止检测就会输出包含函数计时的报告
- 添加用户标记,这个功能目前还有问题,见下图,等修复好了更新
并发可视化工具
这里简单介绍下如何使用 Visual Studio 提供的并发可视化工具和相关的SDK。
并发可视化工具的使用比较简单,分析 > 并发可视化工具 > 从当前项目开始/启动新进程,启动新进程的话就选择我们要检测的 .exe 文件。
程序运行完之后就会输出报告。
使用率视图
使用率输出进程使用的平均逻辑核心数,这里逻辑核心数上限是 12 那么实际核心数就是 6,如果两个内核在某一给定时间段内均以 50% 的使用率运行,则此视图将显示使用一个逻辑内核。
CPU 使用率图颜色:
- 绿色表示系统中当前进程的逻辑内核使用率
- 浅灰色表示系统上其他进程的逻辑内核利用率。如果 CPU 图中的浅灰色百分比过高,则表示其他进程已使系统负载过重,你的进程可能会被这些进程抢占资源。若要减少其他进程使用的逻辑内核数,请减少系统上运行的逻辑内核数。
- 深灰色表示系统进程的逻辑内核消耗。无法直接控制这部分逻辑内核消耗,但由于这些消耗会影响用户进程可以使用的逻辑内核情况,因此了解这些消耗何时出现非常有用。
- 白色表示系统上未使用逻辑内核的可用性。如果可以找到更多的并行机会,这些核心则可用于你的进程。
线程视图
左上角选项卡选择线程视图
线程视图上半部分是时间线,时间线显示主计算机上的进程与所有物理磁盘设备中的所有线程的活动。它还显示 GPU 活动和标记事件。
在时间线上,X 轴上是时间,y 轴上是几个通道:
- 系统上每个磁盘驱动器两个 I/O 通道,一个通道用于读取,另一个用于写入
- 进程中每个线程一个通道
- 标记通道(如果跟踪中存在事件标记)。标记通道最初出现在生成这些事件的线程通道下。
- GPU 通道。
最初,线程按创建顺序进行排序,以便主应用线程处于第一位。在“排序依据”下拉列表中选择另一个选项,以按另一种标准(例如“执行”)对线程进行排序。
时间线的颜色指示线程在给定时间的状态”
- 绿色段表示已在执行
- 红色段指示已因同步而受阻
- 黄色段指示已被抢占
- 紫色段指示已参与设备 I/O
左侧有线程通道,停在通道名称处时,将显示给定线程的开始函数。并发可视化工具可检测到多种线程。下表列出了最常用的线程类型。
线程 | 描述 |
---|---|
主线程 | 启动应用的线程 |
工作线程 | 由应用程序主线程创建的线程 |
CLR工作线程 | 由公共语言运行时(CLR)创建的工作线程 |
调试器帮助程序 | 由 Visual Studio 调试器创建的帮助线程 |
ConRT 线程 | 由 Microsoft 并发运行时创建的线程 |
GDI线程 | 由GDIPlus创建的线程 |
OLE/RPC 线程 | 作为 RPC工作线程创建的线程 |
RPC | 作为RPC线程创建的线程 |
Winsock 线程 | 作为 Winsock 线程创建的线程 |
线程池 | 由 CLR 线程池创建的线程 |
内核视图
并发可视化工具里还有一个内核视图,显示线程执行如何映射到逻辑处理器核心。
与留在同一逻辑核心上的切换相比,跨核心上下文切换要花费更多的开销和性能。在上下文切换过程中,将保存并恢复处理器寄存器,执行操作系统内核代码,重新加载转换旁视缓冲项,并刷新处理器管道。因为缓存数据对其他核心上的此线程无效,所以跨核心上下文切换可能比其他上下文切换开销更大。相比之下,如果某个线程上下文切换到之前运行过的该线程的核心上,则有用的数据可能仍在缓存中。当跨核心上下文切换因试图管理线程关联而有所增加且性能出现下降时,请考虑是否要解决这一问题。首先消除线程关联,然后观察由此导致的跨核心行为。
下表描述了图例元素。
元素 | 定义 |
---|---|
线程名 | 显示上一个内核时间线中线程的颜色,以及该线程的名称 |
跨核心上下文切换 | 也从一个逻辑内核切换到另一个逻辑内核的线程的上下文切换数。 不区分从一个处理器芯片跨到另一个芯片的跨核心上下文切换,以及留在同一芯片上的跨核心上下文切换。 |
上下文切换总数 | 采样期间给定线程的上下文切换总数。 每次线程更改上下文(例如从执行到同步)时,将进行一次上下文切换计数。 |
跨越核心的上下文切换所占的百分比 | 通过跨核心上下文切换数除以上下文切换总数计算出的百分比。 此百分比越高,此特定线程的性能上的跨核心上下文切换的开销的整体效果越大。 |
并发可视化工具SDK
这里介绍一个简单的应用,给我们想要关注的函数部分/代码段打上标记,这样在线程视图中,就能看到对应标记的范围了。
对于带解决方案的应用,直接 分析 > 并发可视化工具 > 将 SDK 添加到项目中,然后在项目中 #include <cvmarkersobj.h>
,然后使用对应的命名空间 using namespace Concurrency::diagnostic
然后我们可以添加标记通道,创建对应的对象 marker_series series;
在对应的标记通道中,可以加入一个标志范围,delete
就可以结束这个范围,示例代码如下。
#include <iostream>
#include "cvmarkersobj.h"
#define MAX 10000
using namespace Concurrency::diagnostic;
void func1(int& sum) {
for (int i = 0; i < MAX; i++)
sum += i;
}
int getSum() {
int sum = 0;
for (int i = 0; i < 1000000; i++)
func1(sum);
return sum;
}
int main()
{
marker_series series;
span* flagSpan = new span(series, 1, _T("flag span"));
series.write_flag(_T("Here is the flag."));
int ans = getSum();
delete flagSpan;
std::cout << ans << std::endl;
int ans2 = getSum();
std::cout << ans2 << std::endl;
}
下面是对应的并发可视化工具视图,可以看到多了一个通道,同时有对应的标志区间。
当然我们也可以创建多个通道。
marker_series flagSeries(_T("flag series"));
span* flagSeriesSpan = new span(flagSeries, 1, _T("flag span"));
flagSeries.write_flag(1, _T("flag"));
// Sleep to even out the display in the Concurrency Visualizer.
int ans = getSum();
Sleep(50);
delete flagSeriesSpan;
std::cout << ans << std::endl;
marker_series messageSeries(_T("message series"));
span* messageSeriesSpan = new span(messageSeries, 1, _T("message span"));
messageSeries.write_message(1, _T("message"));
// Sleep to even out the display in the Concurrency Visualizer.
int ans2 = getSum();
Sleep(50);
delete messageSeriesSpan;
std::cout << ans2 << std::endl;
这样在视图中就可以看到多个通道和标记区间。
参考资料
通过分析 CPU 使用情况衡量应用程序性能(C#、Visual Basic、C++、F#)
了解探查器性能收集方法
在 Visual Studio(C#、C++、Visual Basic、F#)中检测 .NET 应用程序
内核视图
使用并发可视化工具标记 SDK
“使用率”视图
并发可视化工具中的线程视图