Valgrind Callgrind 性能瓶颈分析

2024-02-19 05:48

本文主要是介绍Valgrind Callgrind 性能瓶颈分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文

Valgrind Callgrind是一个可以分析代码并报告其资源使用情况的程序。这是Valgrind提供的另一个工具,它还可以帮助检测内存问题。事实上,Valgrind框架支持多种运行时分析工具,包括memcheck(检测内存错误/泄漏)、massif(报告堆使用情况)、helgrind(检测多线程竞争条件)和callgrind/cachegrind(评测CPU/缓存性能)。本工具指南将向你介绍Valgrind callgrind评测工具。

代码分析

代码分析器是一种分析程序并报告其资源使用情况的工具(其中“资源”是内存、CPU周期、网络带宽等)。程序优化的第一步是使用分析器从代表性的程序执行中收集实际的定量数据。分析数据将深入了解资源消耗的模式和峰值,因此你可以确定是否存在问题,如果存在,问题集中在哪里,从而使你能够将精力集中在最需要关注的段落上。你还可以使用探查器进行测量和重新测量,以验证你的努力是否取得了成果。
大多数代码分析器通过动态分析进行操作——也就是说,它们观察正在执行的程序并进行实时测量——而不是静态分析,后者检查源代码并预测行为。动态探查器以多种方式运行:一些通过向程序中插入计数代码,另一些以高频率对其活动进行采样,另一些在模拟环境中运行程序并内置监控。
用于评测的标准C/unix工具是gprof,gnu评测器。这个不虚饰的工具是一个统计采样器,用于跟踪在功能级别花费的时间。它定期拍摄运行程序的快照,并跟踪函数调用堆栈(就像你在crash reporter中所做的那样!)以观察活动。如果你对gprof的功能和使用感到好奇,可以查看在线gprof手册。
Valgrind分析工具是cachegrind和callgrind。cachegrind工具模拟一级/二级缓存并统计缓存未命中/命中。callgrind工具统计函数调用和每个调用中执行的CPU指令,并构建函数调用图。callgrind工具包括一个从cachegrind中采用的缓存模拟功能,因此你可以实际使用callgrind进行CPU和缓存评测。callgrind工具的工作原理是使用额外的指令来检测程序,这些指令记录活动并保留计数器。

指令计数

要评测,你可以在Valgrind下运行程序,并显式请求callgrind工具(如果未指定,该工具默认为memcheck)。

valgrind --tool=callgrind program-to-run program-arguments

上面的命令启动valgrind并在其中运行程序。由于Valgrind的检测,程序将正常运行,尽管速度稍慢。完成后,它会报告收集的事件总数:

==22417== Events    : Ir
==22417== Collected : 7247606
==22417==
==22417== I   refs:      7,247,606

Valgrind已将上述700万个收集事件的信息写入名为callgrind.out.pid的输出文件。(pid替换为进程id,在上面的运行中为22417,id显示在最左边的列中)。
callgrind输出文件是一个文本文件,但其内容不是供你阅读的。相反,你可以在此输出文件上运行注解器callgrind_annotate,以有用的方式显示信息(用进程id替换pid):

callgrind_annotate --auto=yes callgrind.out.pid

注解器的输出将以Ir计数给出,Ir计数是“指令读取”事件。输出显示每个函数内发生的事件总数(即执行的指令数),并显示按计数递减顺序排序的函数列表。你的高流量函数将列在顶部。callgrind_annotate选项--auto=yes通过报告每个C语句的计数来进一步细分结果(如果没有auto选项,则会在函数级别汇总计数,这通常过于粗糙而没有用处)。
那些用大指令注解的行是程序的“热点”。这些高流量执行路径具有大量计数,因为它们被执行多次和/或对应于大量指令序列。在加速你的程序方面,努力简化这些执行路径将为你带来最好的回报。

添加缓存模拟

为了额外监控缓存命中/未命中,请使用--simulate cache=yes选项调用valgrind callgrind,如下所示:

valgrind --tool=callgrind --simulate-cache=yes program-to-run program-arguments

现在输出在最后的简要摘要将包括收集的访问L1和L2缓存的事件,如下所示:

==16409== Events    : Ir Dr Dw I1mr D1mr D1mw I2mr D2mr D2mw
==16409== Collected : 7163066 4062243 537262 591 610 182 16 103 94
==16409==
==16409== I   refs:      7,163,066
==16409== I1  misses:          591
==16409== L2i misses:           16
==16409== I1  miss rate:       0.0%
==16409== L2i miss rate:       0.0%
==16409==
==16409== D   refs:      4,599,505  (4,062,243 rd + 537,262 wr)
==16409== D1  misses:          792  (      610 rd +     182 wr)
==16409== L2d misses:          197  (      103 rd +      94 wr)
==16409== D1  miss rate:       0.0% (      0.0%   +     0.0%  )
==16409== L2d miss rate:       0.0% (      0.0%   +     0.0%  )
==16409==
==16409== L2 refs:           1,383  (    1,201 rd +     182 wr)
==16409== L2 misses:           213  (      119 rd +      94 wr)
==16409== L2 miss rate:        0.0% (      0.0%   +     0.0%  )

在该输出文件上运行callgrind_annotate时,注解现在将包括缓存活动和指令计数。

解释结果

了解Ir计数。Ir计数基本上是执行的汇编指令计数。一条C语句可以转换为1、2或多条汇编指令。请考虑下面由callgrind_annotate注解的段落。分析程序使用选择排序对1000个成员的数组进行排序。对swap函数的单个调用需要15条指令:3条指令用于开场白,3条指令用于赋值给tmp,4条指令用于从*b复制到*a,3条指令用于从tmp赋值,另外2条指令用于尾声。(请注意,开场白和尾声的成本在开头和结尾的大括号中进行了注解。)有1000次呼叫需要交换,总共占15000条指令。

        . void swap(int *a, int *b)3,000  {3,000      int tmp = *a;4,000      *a = *b;3,000      *b = tmp;2,000  }.. int find_min(int arr[], int start, int stop)3,000  {2,000      int min = start;2,005,000      for(int i = start+1; i <= stop; i++)4,995,000          if (arr[i] < arr[min])6,178              min = i;1,000      return min;2,000  }. void selection_sort(int arr[], int n)3  {4,005      for (int i = 0; i < n; i++) {9,000          int min = find_min(arr, i, n-1);7,014,178  => sorts.c:find_min (1000x)10,000          swap(&arr[i], &arr[min]);15,000  => sorts.c:swap (1000x).    }2  }.

callgrind_annotate包括一个函数调用摘要,按计数递减顺序排序,如下所示:

7,014,178  sorts.c:find_min [sorts]25,059  ???:do_lookup_x [/lib/ld-2.5.so]23,010  sorts.c:selection_sort [sorts]20,984  ???:_dl_lookup_symbol_x [/lib/ld-2.5.so]15,000  sorts.c:swap [sorts]

默认情况下,计数是独占的——函数的计数只包括在该函数中花费的时间,而不包括在它调用的函数中花费的时间。例如,selection_sort函数计算的23010条指令包括9000条设置和调用find_min的指令,但不包括find_min本身执行的700万条指令。另一种计算方法是包括在内的(如果你更喜欢这种统计方法,请使用选项--inclusive=yes来进行callgrind_annotate)将子函数调用的成本包含在上层总计中。一般来说,使用排他计数是突出瓶颈的好方法——花费时间最多的函数/语句是你希望减少调用次数或简化调用中发生的事情的函数/语句。通过寻找最高计数,可以很容易地发现流量最大的执行路径。在上面的代码中,工作集中在循环中以查找最小值——缓存最小数组元素而不是重新获取和展开循环等技术在这里可能很有用。
了解缓存统计信息。高速缓存模拟器模拟了一台具有拆分一级高速缓存(单独的指令I1和数据D1)的机器,该机器由一个统一的二级高速缓存(L2)支持。这符合大多数现代机器的一般缓存设计。缓存模拟器记录的事件包括:

  • Ir: I缓存读取(执行的指令)
  • I1mr: I1缓存读取未命中(指令不在I1缓存中,但在二级缓存中)
  • I2mr: 二级缓存指令读取未命中(指令不在I1或二级缓存中,必须从内存中提取)
  • Dr: D缓存读取(内存读取)
  • D1mr: D1缓存读取未命中(数据位置不在D1缓存中,但在L2中)
  • D2mr: 二级缓存数据读取未命中(位置不在D1或L2中)
  • Dw: D缓存写入(内存写入)
  • D1mw: D1缓存写入未命中(位置不在D1缓存中,但在L2中)
  • D2mw: 二级缓存数据写入未命中(位置不在D1或L2中)

同样,策略是使用callgrind_annotate来报告每个语句的命中/未命中计数,并查找那些导致总读/写次数过多或缓存未命中次数过多的语句。即使是少量的未命中也是非常重要的,因为L1未命中通常会花费5-10个周期,L2未命中可能会花费100-200个周期,因此即使是其中的几个也会带来很大的提升。
查看前一个选择排序程序的带注解的结果表明,代码对缓存非常友好——在沿着数组遍历时只发生了几次未命中,否则会出现大量I1和D1命中。

--------------------------------------------------------------------------------
-- Auto-annotated source: sorts.c
--------------------------------------------------------------------------------Ir        Dr      Dw I1mr D1mr D1mw I2mr D2mr D2mw.       .     .  .  .  .  .  .  . void swap(int *a, int *b)3,000         0   1,000    1    0    0    1    .  . {3,000     2,000   1,000    .  .  .  .  .  .    int tmp = *a;4,000     3,000   1,000    .  .  .  .  .  .    *a = *b;3,000     2,000   1,000    .  .  .  .  .  .    *b = tmp;2,000     2,000       .  .  .  .  .  .  . }.       .     .  .  .  .  .  .  ..       .     .  .  .  .  .  .  . int find_min(int arr[], int start, int stop)3,000         0   1,000    1    0    0    1    .  . {2,000     1,000   1,000    0    0    1    0    0    1      int min = start;2,005,000 1,002,000 500,500    .  .  .  .  .  .    for(int i = start+1; i <= stop; i++)
4,995,000 2,997,000       0    0   32    0    0   19    .        if (arr[i] < arr[min])6,144     3,072   3,072    .  .  .  .  .  .            min = i;1,000     1,000       .  .  .  .  .  .  .    return min;2,000     2,000       .  .  .  .  .  .  . }.       .     .  .  .  .  .  .  . void selection_sort(int arr[], int n)3         0       1    1    0    0    1    .  . {4,005     2,002   1,001    .  .  .  .  .  .    for (int i = 0; i < n; i++) {9,000     3,000   5,000    .  .  .  .  .  .        int min = find_min(arr, i, n-1);7,014,144 4,006,072 505,572    1   32    1    1   19    1  => sorts.c:find_min (1000x)10,000     4,000   3,000    .  .  .  .  .  .        swap(&arr[i], &arr[min]);15,000     9,000   4,000    1    0    0    1    .  . => sorts.c:swap (1000x).       .     .  .  .  .  .  .  .    }2         2       .  .  .  .  .  .  . }

提示和技巧

关于有效使用callgrind的一些提示:

  • 通常,我们建议你在未经优化的情况下对编译的代码进行调试/测试(即,-O0),但衡量性能有点不同。即使在编译器的优化帮助下,你也希望执行优化的代码以找出存在哪些瓶颈。
  • Callgrind只测量所执行的代码,因此请确保你正在进行各种各样的、具有代表性的运行,以执行所有适当的代码路径。
  • 你可以比较多次运行的结果,以了解不同输入的性能变化。
  • Callgrind记录指令的计数,而不是在函数中花费的实际时间。如果你有一个瓶颈是文件I/O的程序,那么与读取和写入文件相关的成本不会显示在分析文件中,因为这些不是CPU密集型任务。
  • 如果编译器内联函数,它将不会显示为单独的条目。相反,内联函数的成本将包含在调用方的成本中。还要记住,inline关键字仅仅是建议性的;编译器可自行决定是否内联。
  • 通过使用选项--toggle-collect=function_name,可以将callgrind限制为仅计算指定函数中的指令。
  • 每个函数的注解通常过于粗糙而没有用处,逐行计数是获得更多有用细节的关键。你甚至可以下拉以观察程序集级别的事件计数。通过编辑Makefile以包含编译器标志-Wa,--gstabs -save-temps,使用程序集级调试信息构建代码。然后,在运行callgrind时,使用选项--dump-instr=yes,该选项对每个汇编指令的请求计数。当注解此输出时,callgrind_annotate现在将事件与汇编语句匹配。
  • 二级缓存未命中比一级未命中要昂贵得多,因此请注意D2mrD2mw计数较高的执行路径。你可以使用callgrind_annotate show/sort选项来关注关键事件,例如callgrind_annotate --show=D2mr --sort=D2mr将突出显示D2mr计数。
  • 为多线程程序做分析时,请使用--separate-threads=yes选项,从而可以为每个线程产生分析报告。
  • 在注解分析报告时,为方便查看函数的调用结构,可以使用--tree=both选项为函数的调用者和被调用者的数据行排在一起。
  • 还有其他callgrind功能,允许你微调模拟参数并控制收集事件的时间,以及注解器分析报告显示方式的许多选项。有关更多详细信息,请参阅联机Callgrind手册。

这篇关于Valgrind Callgrind 性能瓶颈分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/723670

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

【软考】希尔排序算法分析

目录 1. c代码2. 运行截图3. 运行解析 1. c代码 #include <stdio.h>#include <stdlib.h> void shellSort(int data[], int n){// 划分的数组,例如8个数则为[4, 2, 1]int *delta;int k;// i控制delta的轮次int i;// 临时变量,换值int temp;in