背景
最近工作上经常需要导出硬件模块的寄存器,而导出脚本在HAPS
(一种FPGA仿真平台)的执行速度很慢,一开始我以为是执行导出的定制版lookat
命令有问题,加了每个步骤的时间戳后没发现问题,于是转而寻找整个脚本的性能瓶颈。
介绍下脚本工作流程,大致就是逐行读取每个硬件模块的寄存器dump信息(每个模块占一行),再将dump信息解析出来,构造好寄存器基地址、size和输出路径名后,传递给lookat
命令执行。
dump信息在文件中的编码如下:
[DUMP INFO]:pipe=0:token=ioblk_itp_rdma_0 dump_frm_buf=0 ptn=1 phyaddr=0x91b67ec0 size=3520
可以看出,基本是key=value
这样的格式,导出脚本在解析上面这行配置信息时,大量使用了awk
脚本:
line_str=$1
pipe_info=`echo ${line_str} | awk -F':' '{print $2}'`
pipe_id=`echo ${pipe_info} | awk -F'=' '{print $2}'`
# parse info
cmdbuf_mem_info=`echo ${line_str} | awk -F':' '{print $3}'`
token=`echo ${cmdbuf_mem_info} | awk '{print $1}' | awk -F'=' '{print $2}'`
buf_idx=`echo ${cmdbuf_mem_info} | awk '{print $2}' | awk -F'=' '{print $2}'`
ptn_idx=`echo ${cmdbuf_mem_info} | awk '{print $3}' | awk -F'=' '{print $2}'`
phyaddr=`echo ${cmdbuf_mem_info} | awk '{print $4}' | awk -F'=' '{print $2}'`
size=`echo ${cmdbuf_mem_info} | awk '{print $5}' | awk -F'=' '{print $2}'`
# 调用lookat
lookat ${phyaddr} -t ${size} -f "${out_dir}/pipe_${pipe_id}/buf_${buf_idx}/bin/ptn_${ptn_idx}/${token}.bin"
可以看出,脚本解析的思路就是先用冒号:
或空格
分离出想要的key=value对,再用等号=
分离出value,而key则是临时定义的环境变量。
在解析代码的前后加时间戳,发现这坨东西就是性能瓶颈!
优化思路
关键是减少awk的使用
虽然awk跟HAPS上的其他常用Linux命令都是链接到busybox
,但我猜测不同的命令对应的是不同的busybox代码段,所以cut
的运行开销大概率是比awk
低的。
不过用cut替换awk的性能提升应该有限,最关键的是减少解析key=value的次数。
解决方案
eval命令解析key=value
注意到dump信息的格式都是key=value,等号前后没有空格,这不恰好是环境变量
的定义格式吗?我可以把这些空格隔开的key=value传递给eval
命令,eval就会在当前bash里创建跟key同名的环境变量,并且其值就是value,正好实现了内层解析!
eval命令的手册:
eval [arg ...]
The args are read and concatenated together into a single
command. This command is then read and executed by the
shell, and its exit status is returned as the value of eval.
If there are no args, or only null arguments, eval returns
0.
示例:
eval "ptn=1 phyaddr=0x91b67ec0 size=3520"
echo $ptn $phyaddr $size
示例输出:
1 0x91b67ec0 3520
cut命令分离出key=value
cut命令可以根据用户指定的分隔符分割字符串,考虑到dump信息的分隔符不统一,有些是冒号有些是空格,而我们需要统一成空格,所以需要分别指定cut的输入分隔符和输出分隔符:
cut命令的手册:
BusyBox v1.35.0 (2023-01-13 13:00:13 CST) multi-call binary.
Usage: cut [OPTIONS] [FILE]...
Print selected fields from FILEs to stdout
-b LIST Output only bytes from LIST
-c LIST Output only characters from LIST
-d SEP Field delimiter for input (default -f TAB, -F run of whitespace)
-O SEP Field delimeter for output (default = -d for -f, one space for -F)
-D Don't sort/collate sections or match -fF lines without delimeter
-f LIST Print only these fields (-d is single char)
-F LIST Print only these fields (-d is regex)
-s Output only lines containing delimiter
-n Ignored
示例:
echo "[DUMP INFO]:pipe=0:token=ioblk_itp_rdma_0 dump_frm_buf=0 ptn=1 phyaddr=0x91b67ec0 size=3520" | cut -d ':' -O ' ' -F 2,3
示例输出:
pipe=0 token=ioblk_itp_rdma_0 dump_frm_buf=0 ptn=1 phyaddr=0x91b67ec0 size=3520
用反引号将cut的输出转换成eval的输入
因为标准输出和命令行输入是不同的IO途径,因此需要用反引号实现转换,即Command Substitution
Command Substitution
Command substitution allows the output of a command to replace the command name. There are two forms:
$(command)
or
`command`
综合起来
那一坨解析代码优化为下面一行:
eval `echo ${line_str} | cut -d ':' -O ' ' -F 2,3`
# 调用lookat
lookat ${phyaddr} -t ${size} -f "${out_dir}/pipe_${pipe}/buf_${dump_frm_buf}/bin/ptn_${ptn}/${token}.bin"
可以看出,最终输出跟那一坨完全一致,只是环境变量pipe_id重命名成dump信息自带的pipe,buf_idx重命名成自带的dump_frm_buf,ptn_idx重命名成自带的ptn,完美!
优化效果
脚本执行时间缩短了90%
总结
- eval命令特别适合key=value这种格式的配置文件的解析。
- profiling很重要,不然很容易误导优化方向。