Shell 编程/优化
如果性能是主要问题,则应使用编译语言(如 C)或至少是更高性能的解释语言(如 Python),而不是 shell 脚本。此外,性能通常受调用的外部命令(以及调用开销)和 shell 本身的影响,而不是特定的脚本或其逻辑。因此,优化的主要方法是调用更少或更便宜的外部命令,或使用更优化的 shell,通常是针对脚本优化的更简单的 shell,如 ash 或 dash,而不是复杂的 shell,如 bash。
但是,一些更精细的优化是可能的,并且可以使用性能分析来识别由单独命令而不是脚本本身引起的瓶颈。
最简单的方法是,以详细模式运行脚本(在 bash 中,使用 -x
标志或在脚本顶部使用 set -x
),并查看执行明显挂起的位置 - 这是一种快速识别瓶颈的方法。
接下来,使用 time 来测量脚本或任何特定命令所使用的时间。这可能是一个 shell 内置命令,如 bash 中的命令,但(外部)GNU time 程序提供了更复杂的分析。您可以按如下方式调用它们(最后一个假设了 time 二进制文件的路径)
time ./foo
`which time` -v ./foo
/usr/bin/time -v ./foo
特别有用的数据是
- 真实时间/用户时间/系统时间(挂钟时间、用户空间 CPU 时间、操作系统 CPU 时间) - 底线
- 上下文切换 - 衡量切换到外部程序或内核,以及返回
- 页面错误
这使您可以快速测量更改的影响。
请注意,此测量结果是有噪声的 - 特别是如果其他任务正在运行 - 并且运行脚本和 time 命令本身会产生开销。此外,由于缓存为空,性能在第一次运行时通常会更慢。为了处理噪声和缓存,请终止其他任务(或通过 top 检查系统是否没有负载)并运行脚本几次。为了校准基线开销,请对真实(外部)命令、真实(内置的,如果可用)以及空脚本计时,以查看总体流程和脚本开销。
对于运行时间更长的脚本,在 Linux 上,您可以在 /proc/$pid/status
中查看一些信息,您可以通过 watch /proc/$pid/status
来监控。请注意,您可以使用 grep 只选择某些数据,例如上下文切换(ctxt)。
对于系统调用,strace,尤其是 strace -c
,会分解系统调用的成本。
更详细地,您可以使用 date 对脚本的各个部分进行计时,并比较连续的时间戳。例如,使用 GNU date 和纳秒精度
date --rfc-3339=ns
这可能足以识别瓶颈,尽管二分搜索可能很繁琐。
要分析整个脚本,最简单的方法是打开详细模式(在 bash 中)并通过 date 管道,这会为每一行生成时间戳;您也可以在 bash 中设置 PS4,每次调用 date(主要开销)或使用内置的日期格式说明符(在 Bash 4.2 或更高版本中)。一些简单的操作(使用 tee 和 paste)可以生成一个带有每行所需时间的带注释的脚本。请参阅 F. Hauri 的回答,了解如何 分析 bash shell 脚本? - 请注意(目前)行号会少一个(时间是前一行的成本) - 或者使用 bashProfiler。
这种分析对于具有简单流程的脚本(一系列命令)已经足够了,但不能检测循环或递归中的热点。类似于 gperf 的更复杂的性能分析器不适用于 shell 脚本,但(假设脚本行是唯一的,如果需要,可以通过添加手动行号来确保)简单地处理带时间戳的日志可以让你跨行求和,并识别热点。
优化 shell 脚本的一项关键技术是使用内置命令替换对外部命令的调用。这消除了大量的进程开销以及相关的上下文切换和页面错误,这些是导致性能缓慢的主要原因。一个很好的例子是在 bash 中使用字符串操作,而不是对 sed 进行外部调用。
查看差异(在 bash 中)的一种简单方法是对调用内置的 true 和外部 true 程序进行性能分析
#!/bin/bash
for i in {1..1000} ; do true ; done
#!/bin/bash
for i in {1..1000} ; do /bin/true ; done
通过以下方式进行性能分析
/usr/bin/time -v ./true-int-1000
/usr/bin/time -v ./true-ext-1000
调用外部命令的脚本会慢很多,这主要是因为有更多的上下文切换(至少多 2000 次,因为切换到 true 然后再返回);以及页面错误 - 实际命令什么也不做(成功了)。