起源
最近几个月除了正常的刷版号,折腾 Android Kernel 以外我还学了一点点 C 语言和比较基本的编译原理。为此我专门写了一个 BrainFuck Compiler(简称 bfc,下同)。主要是 BrainFuck(简称 bf,下同) 只有8个字符,学习起来还是比较简单的,就是有点费手。不过当我在测试 awib.bf 的时候发现在生成 C 代码时总会比其他的 bf 慢上零点几秒。我一开始考虑到可能是 awib 比较大处理比较慢的问题。
直到我在折腾如何优化内核的时候偶然了解到 pgo ,里面就提到了 gcov 这个工具。于是就开始了今天的折腾。
安装
由于 gcc 自带 gcov,且不需要 lcov 这种可视化工具,所以只需要安装 perf 即可,而 perf 命令是来自 linux-tools:
1 | $ sudo apt install linux-tools* |
使用
在编译的时候语言通过参数启用插桩:
1 | gcc -fprofile-arcs -ftest-coverage bfc.c -O0 -g3 -o bfc |
然后会生成 bfc.gcno 文件。
这个时候再运行一下 perf:
1 | $ perf record ./bfc -f tests/awib.b awub.c |
此时会生成 bfc.gcda 和 perf.data
这个时候再运行一下:
1 | $ perf report |
我们得到了更详细的数据:
1 | 71.77% bfc bfc [.] generate_c_code |
这个时候我们就知道是 generate_c_code 这个函数使用了71.77% 的 cpu。
接下来使用:
1 | $ gcov bfc.c |
生成 bfc.c.gcov 查看 generate_c_code 函数中有哪些行执行了多次从而占用了大量的 cpu:
1 | -: 670: // 添加适当的缩进 |
这个是添加缩进,但是这个不是大头,先暂且略过
然后发现了:
1 | 1202: 775: if (loop_depth > 0) { |
不难看出这个循环执行了 1100w 次是导致 cpu 占用过大的罪魁祸首。
优化
在 Brainfuck 解释器中,当执行到 ](循环结束)时,需要快速找到它对应的 [(循环开始)的位置,以便决定是否跳回循环开头。
这个 for 循环在每次遇到]时就遍历整个 bf 代码,而 awib 本身就是一个用 bf 写的 bf 解释器,代码量非常大。所以就出现了一个 for 循环执行了最坏的情况,即 O(n) 次。
而且我当时并不知道我事先在处理语法错误的位置的时候已经预处理了(这个还是问 llm 才得到的,我一开始并不知道,我笨,我紫菜):
1 | static void build_bracket_mapping() { |
最后的解决办法就是扬掉遍历,使用预处理:
1 | - int start_pc = -1; |
根据 perf report 得知优化明显:
1 | 18.56% bfc libc.so.6 [.] _IO_fwrite |
查看循环次数
1 | 1202: 775: if (loop_depth > 0) { |
可以看到调用次数仅为 1202 次,符合预期。
-------------本文结束感谢您的阅读-------------