一尘不染

如何分析在 Linux 上运行的 C++ 代码?

javascript

我有一个在 Linux 上运行的 C++ 应用程序,我正在对其进行优化。如何确定我的代码的哪些区域运行缓慢?


阅读 281

收藏
2022-02-12

共1个答案

一尘不染

如果您的目标是使用探查器,请使用建议的探查器之一。

但是,如果您赶时间并且可以在调试器下手动中断程序,而程序主观上很慢,那么有一种简单的方法可以找到性能问题。

只需将其暂停几次,每次都查看调用堆栈。如果有一些代码浪费了一定比例的时间,20% 或 50% 或其他任何时间,这就是您在每个样本的行为中捕获它的概率。因此,这大致是您将看到它的样本的百分比。不需要有根据的猜测。如果您确实猜测问题是什么,这将证明或反驳它。

您可能会遇到多个大小不同的性能问题。如果您清除其中任何一个,其余的将占更大的百分比,并且在随后的传球中更容易被发现。这种放大效应,当复合多个问题时,可以导致真正巨大的加速因素。

警告:程序员往往对这种技术持怀疑态度,除非他们自己使用过。他们会说分析器会为您提供此信息,但只有当他们对整个调用堆栈进行采样,然后让您检查一组随机样本时,这才是正确的。(摘要是失去洞察力的地方。)调用图不会为您提供相同的信息,因为

  1. 他们没有在指令级别进行总结,并且
  2. 在存在递归的情况下,它们给出了令人困惑的摘要。

他们还会说它只适用于玩具程序,而实际上它适用于任何程序,而且它似乎在更大的程序上效果更好,因为它们往往有更多的问题要找到。他们会说它有时会发现没有问题的东西,但只有当你看到某样东西才会这样说。如果您在多个样本上发现问题,那就是真实的。

PS这也可以在多线程程序上完成,如果有一种方法可以在某个时间点收集线程池的调用堆栈样本,就像在 Java 中一样。

PPS作为一个粗略的概括,您的软件中的抽象层越多,您就越有可能发现这是性能问题的原因(以及获得加速的机会)。

补充:这可能不是很明显,但堆栈采样技术在存在递归的情况下同样有效。原因是删除一条指令所节省的时间近似为包含它的样本的分数,而不管它在样本中可能出现的次数。

我经常听到的另一个反对意见是:“它会随机停在某个地方,它会错过真正的问题”。这来自对真正问题的先验概念。性能问题的一个关键特性是它们违背预期。抽样告诉你有问题,你的第一反应是不相信。这是自然的,但您可以确定它是否发现问题是真实的,反之亦然。

补充:让我对它的工作原理做一个贝叶斯解释。假设有一些指令I(调用或其他)在调用堆栈上的某个部分f时间(因此花费那么多)。为简单起见,假设我们不知道是什么f,但假设它是 0.1, 0.2, 0.3, … 0.9, 1.0,并且这些可能性中的每一个的先验概率都是 0.1,所以所有这些成本都是同样可能的先验的。

然后假设我们只取了 2 个堆栈样本,并且我们I在两个样本上都看到了指令,指定为观察o=2/2f这为我们提供了 的频率的新估计I,根据这个:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

最后一列表示,例如,f>= 0.5 的概率为 92%,高于之前假设的 60%。

假设先前的假设不同。假设我们假设P(f=0.1)是 0.991(几乎确定),而所有其他可能性几乎是不可能的(0.001)。换句话说,我们事先确定的I是便宜。然后我们得到:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

现在它说P(f >= 0.5)是 26%,高于之前假设的 0.6%。所以贝叶斯允许我们更新我们对 的可能成本的估计I。如果数据量很小,它并不能准确地告诉我们成本是多少,只能告诉我们它足够大,值得修复。

另一种看待它的方式称为继承规则。如果你掷硬币两次,两次都出现正面,这说明硬币的可能重量是什么?尊重的回答方式是说它是一个 Beta 分布,具有平均值(number of hits + 1) / (number of tries + 2) = (2+1)/(2+2) = 75%

(关键是我们看到I不止一次。如果我们只看到一次,除了f> 0 之外,这并不能告诉我们太多。)

因此,即使是非常少量的样本也可以告诉我们很多关于它所看到的指令成本的信息。(平均而言,它会以与其成本成正比的频率看到它们。如果n抽取样本,并且f是成本,那么I将出现在nf+/-sqrt(nf(1-f))样本上。例如,n=10f=0.3,即3+/-1.4样本。)


补充:为了直观地了解测量和随机堆栈采样之间的区别:
现在有分析器可以对堆栈进行采样,即使是在挂钟时间,但结果是测量值(或热点路径,或热点,从中“瓶颈”很容易隐藏)。他们没有向您展示(他们很容易)是实际样本本身。如果您的目标是找到瓶颈,那么您需要查看的瓶颈数量平均为 2 除以所需时间的分数。因此,如果花费 30% 的时间,平均 2/.3 = 6.7 个样本会显示它,而 20 个样本显示它的机会是 99.2%。

这是检查测量值和检查堆栈样本之间差异的现成说明。瓶颈可能是这样的一个大块,也可能是许多小块,没有区别。

在此处输入图像描述

测量是水平的;它告诉您特定例程需要多少时间。采样是垂直的。如果有任何方法可以避免此时整个程序正在做什么,并且如果您在第二个示例中看到它,那么您已经找到了瓶颈。这就是不同之处——看到花费时间的全部原因,而不仅仅是多少。

2022-02-12