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 性能分析
每月下载量 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
将产生以下输出,其中不同的 a
和 b
行已被组合在一起。
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搜索。
有时当存在多个打印语句时,输出行可能会被混合在一起。由于通常有大量输出行,所以几行垃圾输出几乎从不重要。
通常,同时使用counts
和counts -i
查看同一个日志文件很有用;每个都提供了对数据的不同见解。
要找出哪个函数调用的调用点是热点,可以直接对调用点进行测试。但很容易遗漏一个,并且需要多次重复相同的打印语句。另一种选择是在函数中添加一个额外的字符串或整数参数,从每个调用点传递一个唯一的值,然后在函数内部打印该值。
有时查看原始日志以及counts
的输出也是有用的,因为输出行的顺序可能很有信息量。
依赖项
~485KB