Bash 内置命令 vs 外部命令:性能对比
通过使用 shell 内置命令进行测试、算术和字符串处理,同时批量处理外部命令,来加速 Bash 脚本。
Bash 内置命令 vs 外部命令:性能对比
当 Bash 脚本运行缓慢时,通常是因为循环中启动了数千个外部进程。Bash 内置命令与外部命令的选择是一个实际的性能考量:对于简单任务使用 shell 自身的功能,而将外部工具留给它们更擅长的任务。
本指南将展示内置命令在哪些方面有帮助,外部命令在哪些场景下仍然合理,以及如何避免最常见的进程创建陷阱。
理解 Bash 中的命令执行
当 Bash 遇到一个命令时,它会依次解析别名、shell 关键字、函数、内置命令,然后通过 PATH 查找命令。这个解析过程很重要,因为任何在当前 shell 内部处理的操作都避免了启动一个独立的程序。
1. 内置命令
Bash 内置命令是直接在 Bash shell 可执行文件内部实现的函数。它们不需要调用操作系统的 fork() 和 exec() 系统调用。由于执行完全发生在现有的 shell 进程内部,内置命令提供了卓越的性能、最小的开销以及对 shell 变量和状态的即时访问。
内置命令的关键特性:
- 速度: 最快的执行路径。
- 开销: 几乎为零的开销,因为不会创建新进程。
- 环境: 它们直接操作当前的 shell 环境。
2. 外部命令
外部命令是独立的可执行文件(通常位于 /bin、/usr/bin 等目录中)。当 Bash 执行外部命令时,它必须:
fork()一个新的子进程。- 在该子进程中
exec()外部程序。 - 等待子进程完成。
这种开销对于单次执行来说微不足道,但在循环或高频操作中会迅速累积,使得外部命令比其内置对应命令慢得多。
性能对决:内置命令的实际应用
为了说明性能差异,考虑一些 Bash 同时提供内置和外部替代方案的常见任务。
示例 1:字符串操作和长度计算
计算变量的长度是一个经典的性能测试案例。
| 命令类型 | 命令 | 描述 |
|---|---|---|
| 内置 | ${#variable} |
参数扩展计算长度。极快。 |
| 外部 | expr length "$variable" |
调用外部 expr 工具。慢。 |
性能提示: 始终使用参数扩展 (${#var}) 来计算长度,而不是 expr length 或管道到 wc -c。
示例 2:字符串替换
替换变量中的子字符串是另一个常见操作。
| 命令类型 | 命令 | 描述 |
|---|---|---|
| 内置 | ${variable//pattern/replacement} |
参数扩展替换。快。 |
| 外部 | sed 's/pattern/replacement/g' |
调用外部 sed 工具。慢。 |
代码对比示例:
TEXT="hello world hello"
# 内置(快)
NEW_TEXT_1=${TEXT//hello/goodbye}
# 外部(慢)
NEW_TEXT_2=$(echo "$TEXT" | sed 's/hello/goodbye/g')
示例 3:循环和迭代
在迭代时,循环内部使用的命令至关重要。
| 命令类型 | 命令 | 描述 |
|---|---|---|
| 内置 | read |
高效地逐行读取输入。 |
| 外部 | grep、awk、cut |
在循环内将数据管道传输到外部工具会强制重复创建进程。 |
while read 反模式 vs 内置命令:
一种常见的慢速模式是在循环内将文件内容通过管道传输到外部命令:
# 慢:为每一行生成 'grep'
while read LINE; do
echo "Processing: $LINE" | grep "important"
done < input.txt
优化策略: 如果可能,使用 Bash 内置命令或内部重定向来避免循环内的外部命令。
关键性能 Bash 内置命令
优先使用这些内置命令而不是它们的外部等价物,将显著提升脚本的速度:
| 任务类别 | 内置命令 | 外部替代方案(较慢) |
|---|---|---|
| 算术 | (( expression )) |
expr、bc |
| 文件测试 | [[ ... ]] 或 Bash 内置的 [ ... ] |
外部 /usr/bin/test 或 /usr/bin/[ |
| 字符串操作 | ${var/pat/rep}、${#var} |
sed、awk、expr |
| 循环/文件读取 | read |
grep、awk、sed(当迭代使用时) |
| 加载 Shell 代码 | source 或 . filename |
作为子进程运行另一个脚本 |
算术示例
内置(快):
COUNTER=0
(( COUNTER++ ))
if (( COUNTER > 10 )); then echo "Done"; fi
外部(慢):
COUNTER=$(expr "$COUNTER" + 1)
if [ "$COUNTER" -gt 10 ]; then echo "Done"; fi
何时必须使用外部命令
虽然内置命令应该是基本操作的首选,但对于 Bash 无法原生或高效处理的任务,外部工具仍然是必不可少的。在以下情况下必须使用外部命令:
- 高级文本处理: 复杂的模式匹配、多行操作或由
awk、sed或perl等工具提供的特定格式化。 - 系统工具: 与操作系统深度交互的命令,如
ls、ps、find、mount或网络工具(curl、ping)。 - 外部文件: 读取或写入 Bash 重定向难以处理的复杂格式文件。
外部命令使用的最佳实践
如果必须使用外部命令,请尽量减少其调用次数。与其在循环内运行外部命令,不如重构逻辑,在单个外部调用中处理整个数据批次。
低效: 使用 stat 逐个处理 1000 个文件。
高效: 使用一次 find 调用结合 stat,或一个单独的 awk 脚本一次性收集所有需要的元数据。
总结
Bash 的性能优化始于避免不必要的进程创建。对于算术、测试和简单的字符串操作,默认使用内置命令。当外部工具是正确选择时,对一批数据运行一次,而不是每行或每个文件运行一次。
- 默认使用内置命令: 对于算术(
(( )))、字符串操作(${...})和测试([[ ]]),始终选择 shell 内置命令。 - 避免循环中的 I/O: 重构循环,使用单个外部命令调用进行批处理,而不是许多小调用。
- 使用参数扩展: 对于字符串长度,优先使用
${#var}而不是wc或expr。 - 认识权衡: 当所需功能在 Bash 中不可用或难以实现时,使用外部工具。