9个版本 (5个稳定版)

1.0.4 2023年9月25日
1.0.3 2023年7月28日
0.2.0 2022年2月25日
0.1.2 2020年12月7日
0.1.1 2020年9月14日

#11 in 性能分析

Download history 57/week @ 2024-07-22

每月下载量 57次

无授权

21KB
199

概述

counts 是一个用于临时性能分析的命令行工具。它统计文本文件中的行频率,类似于改进版的Unix命令链 sort | uniq -c

您可以将它与感兴趣程序中的日志打印语句结合使用,以获取非常有价值、特定领域的性能分析数据。

安装

crates.io 安装

cargoinstall counts

这需要Rust 1.59或更高版本。编译的二进制文件将被放入 ~/.cargo/bin/

更新安装

cargoinstall --forcecounts

简单使用示例

考虑以下输入。

a 1
b 2
c 3
d 4
d 4
c 3
c 3
d 4
b 2
d 4

counts 产生以下输出。

10 counts:
(  1)        4 (40.0%, 40.0%): d 4
(  2)        3 (30.0%, 70.0%): c 3
(  3)        2 (20.0%, 90.0%): b 2
(  4)        1 (10.0%,100.0%): a 1

它给出了总行数,并按频率显示所有唯一行,按频率排序,并显示个别和累积百分比。

或者,当使用 -i 标志调用时,它将为每行分配一个整数权重,该权重由行上出现的最后一个整数决定(如果没有这样的整数,则为1)。在相同的输入下,counts - 产生以下输出。

30 counts (weighted integral)
(  1)       16 (53.3%, 53.3%): d 4
(  2)        9 (30.0%, 83.3%): c 3
(  3)        4 (13.3%, 96.7%): b 2
(  4)        1 ( 3.3%,100.0%): a 1

总计数和每行计数现在按权重计算;输出结合了频率和大小测量。

可以使用 - 标志使用分数权重,可以是整数或分数,形式为 mm.nn

允许使用负权重。在输出中,每个条目按其总权重的绝对值排序。这意味着大正数和大负数都会出现在顶部附近。

有时,您可能想要将具有不同权重但其他方面相同的行组合在一起。可以使用 -e 标志在应用权重后擦除权重,通过用 NNN 替换它们。考虑以下输入。

a 1
b 2
a 3
b 4
a 5

counts -i 将产生以下输出,这并不有趣。

15 counts (weighted integral)
(  1)        5 (33.3%, 33.3%): a 5
(  2)        4 (26.7%, 60.0%): b 4
(  3)        3 (20.0%, 80.0%): a 3
(  4)        2 (13.3%, 93.3%): b 2
(  5)        1 ( 6.7%,100.0%): a 1

counts -i -e 将产生以下输出,其中不同的 ab 行已被组合在一起。

15 counts (weighted integral, erased)
(  1)        9 (60.0%, 60.0%): a NNN
(  2)        6 (40.0%,100.0%): b NNN

更复杂的用法示例

例如,我向Firefox的堆分配器添加了打印语句,以便它为每次分配打印一行,显示其类别、请求大小和实际大小。带有此工具的Firefox的简短运行产生了包含527万行的77MB文件。counts 为此文件产生了以下输出。

5270459 counts
( 1) 576937 (10.9%, 10.9%): small 32 (32)
( 2) 546618 (10.4%, 21.3%): small 24 (32)
( 3) 492358 ( 9.3%, 30.7%): small 64 (64)
( 4) 321517 ( 6.1%, 36.8%): small 16 (16)
( 5) 288327 ( 5.5%, 42.2%): small 128 (128)
( 6) 251023 ( 4.8%, 47.0%): small 512 (512)
( 7) 191818 ( 3.6%, 50.6%): small 48 (48)
( 8) 164846 ( 3.1%, 53.8%): small 256 (256)
( 9) 162634 ( 3.1%, 56.8%): small 8 (8)
( 10) 146220 ( 2.8%, 59.6%): small 40 (48)
( 11) 111528 ( 2.1%, 61.7%): small 72 (80)
( 12) 94332 ( 1.8%, 63.5%): small 4 (8)
( 13) 91727 ( 1.7%, 65.3%): small 56 (64)
( 14) 78092 ( 1.5%, 66.7%): small 168 (176)
( 15) 64829 ( 1.2%, 68.0%): small 96 (96)
( 16) 60394 ( 1.1%, 69.1%): small 88 (96)
( 17) 58414 ( 1.1%, 70.2%): small 80 (80)
( 18) 53193 ( 1.0%, 71.2%): large 4096 (4096)
( 19) 51623 ( 1.0%, 72.2%): small 1024 (1024)
( 20) 45979 ( 0.9%, 73.1%): small 2048 (2048)

不出所料,小分配占主导地位。但如果我们将每个条目按其大小加权呢?counts -i 产生了以下输出。

2554515775 counts (weighted integral)
( 1) 501481472 (19.6%, 19.6%): large 32768 (32768)
( 2) 217878528 ( 8.5%, 28.2%): large 4096 (4096)
( 3) 156762112 ( 6.1%, 34.3%): large 65536 (65536)
( 4) 133554176 ( 5.2%, 39.5%): large 8192 (8192)
( 5) 128523776 ( 5.0%, 44.6%): small 512 (512)
( 6) 96550912 ( 3.8%, 48.3%): large 3072 (4096)
( 7) 94164992 ( 3.7%, 52.0%): small 2048 (2048)
( 8) 52861952 ( 2.1%, 54.1%): small 1024 (1024)
( 9) 44564480 ( 1.7%, 55.8%): large 262144 (262144)
( 10) 42200576 ( 1.7%, 57.5%): small 256 (256)
( 11) 41926656 ( 1.6%, 59.1%): large 16384 (16384)
( 12) 39976960 ( 1.6%, 60.7%): large 131072 (131072)
( 13) 38928384 ( 1.5%, 62.2%): huge 4864000 (4866048)
( 14) 37748736 ( 1.5%, 63.7%): huge 2097152 (2097152)
( 15) 36905856 ( 1.4%, 65.1%): small 128 (128)
( 16) 31510912 ( 1.2%, 66.4%): small 64 (64)
( 17) 24805376 ( 1.0%, 67.3%): huge 3097600 (3100672)
( 18) 23068672 ( 0.9%, 68.2%): huge 1048576 (1048576)
( 19) 22020096 ( 0.9%, 69.1%): large 524288 (524288)
( 20) 18980864 ( 0.7%, 69.9%): large 5432 (8192)

这表明分配的字节累积计数(2.55GB)主要由较大的分配大小混合所主导。

这个例子只是展示了 counts 可以做什么的一个小例子。

典型用法

当您已经了解某些内容时,这种技术通常很有用 - 例如,通用分析器显示某个特定函数很热 - 但您想了解更多信息。

  • 路径X、Y和Z执行了多少次?例如,在数据结构D中查找成功或失败了多少次?每次路径被命中时打印一个标识字符串。
  • 循环L迭代了多少次?循环计数分布是什么样子?它是否经常以低循环计数执行,或者以高循环计数执行很少,或者两者兼有?在循环前后打印迭代计数。
  • 在这个代码位置,哈希表H通常有多少个元素?少?多?混合?打印元素计数。
  • 在这个代码位置,向量V的内容是什么?打印内容。
  • 在这个代码位置,数据结构D使用了多少字节内存?打印字节大小。
  • 函数F的哪些调用点是热点?在调用点打印一个标识字符串。

然后使用 counts 来汇总数据。通常,这些特定领域的数据对于完全优化热点代码至关重要。

更糟的是更好

打印语句是获取此类信息的粗略方法,I/O和磁盘空间浪费。在许多情况下,您可以以更高效的方式使用机器资源来完成这项工作,例如,在代码中创建一个小型表数据结构来跟踪频率,然后在程序结束时打印该表。

但这将需要

  • 编写自定义表(收集和打印);
  • 决定在哪里定义表;
  • 可能将表暴露给多个模块;
  • 决定在哪里初始化表;以及
  • 决定在哪里打印表的内容。

这是很痛苦的事情,尤其是在一个你不完全理解的程序中。

或者,有时您可能需要通用分析器可以提供的信息,但运行该分析器对您的程序来说很麻烦,因为您想要分析的程序实际上位于另一层之下,设置正确需要付出努力。

相比之下,插入打印语句是微不足道的。任何测量都可以在极短的时间内设置完成。(重新编译往往是这个过程最慢的部分。)这鼓励了实验。您还可以在任何时候终止正在运行的程序,而不会丢失性能分析数据。

不要因为浪费机器资源而感到内疚;这是临时代码。有时您可能会得到大小为千兆的输出文件。但是counts之所以快速,是因为它非常简单。让机器为您做工作。(如果您有一台带有SSD的机器,这将有所帮助。)

临时性能分析

长期以来,我一直在心里使用“临时性能分析”这个术语来描述这种日志打印语句和基于频率的后续处理的组合。维基百科定义“临时”如下。

在英语中,它通常表示为针对特定问题或任务设计的解决方案,不具有通用性,且不打算适应其他目的

编写自定义代码以收集这种类型性能数据的过程——正如我在上一节中批评的那样——确实符合“临时”的定义。

但是counts之所以有价值,正是因为它使这种类型的自定义性能分析更少“临时”且更具重复性。我或许应该称其为“通用临时性能分析”或“不那么临时的性能分析”,但那些名字听起来并没有那么吸引人。

技巧

为打印语句使用无缓冲输出。在C和C++代码中,使用fprintf(stderr, ...)。在Rust代码中使用eprintln!dbg!

将stderr输出重定向到文件,例如my-prog 2> log

如果日志文件变得很大,将数据通过快速压缩器(如zstd)进行压缩可能是值得的。例如,使用以下命令写入和zstdcat log.zst | counts来处理:my-prog 2>&1 >/dev/null | zstd -f -o log.zst

有时程序会打印到stderr的其他输出行,这些行应该被counts忽略。(特别是如果它们包含counts会将其解释为权重的整数ID!)将所有日志行前面加上一个简短的标识符,然后使用grep $ID log | counts来忽略其他行。如果您使用多个前缀,可以单独或一起对每个前缀进行grep搜索。

有时当存在多个打印语句时,输出行可能会被混合在一起。由于通常有大量输出行,所以几行垃圾输出几乎从不重要。

通常,同时使用countscounts -i查看同一个日志文件很有用;每个都提供了对数据的不同见解。

要找出哪个函数调用的调用点是热点,可以直接对调用点进行测试。但很容易遗漏一个,并且需要多次重复相同的打印语句。另一种选择是在函数中添加一个额外的字符串或整数参数,从每个调用点传递一个唯一的值,然后在函数内部打印该值。

有时查看原始日志以及counts的输出也是有用的,因为输出行的顺序可能很有信息量。

依赖项

~485KB