CPU 是免费的,但 bash 脚本没有使用所有 CPU 资源

Gau*_*ani 5 performance shell bash shell-script time

我运行了一个简单的脚本来生成一个包含 6 个字段的大型(10000000 行)csv 文件,其中使用while循环在每行/行中更改了一些字段。这台机器有全部 (32) 个 CPU 空闲,大量 RAM (~31 Gb) 也是空闲的。

我用命令为脚本计时

/usr/bin/time -v bash script.01.sh

运行了大约 2 个小时后,我得到了以下统计数据:

正在计时的命令:“bash script.01.sh”
用户时间(秒):1195.14
系统时间(秒):819.71
此作业获得的 CPU 百分比:27%
已用(挂钟)时间(h:mm:ss 或 m: ss): 2:01:10
平均共享文本大小 (kbytes): 0
平均未共享数据大小 (kbytes): 0
平均堆栈大小 (kbytes): 0
平均总大小 (kbytes): 0
最大驻留集大小 (kbytes): 4976
平均驻留集大小(千字节):0
主要(需要 I/O)页面错误:0
次要(回收帧)页面错误:3131983488
自愿上下文切换:22593141
非自愿上下文切换:10923348
交换:0
文件系统输入:0
文件系统输出:2182920
发送的
套接字消息:0 接收的套接字消息:0
传递的信号:0
页大小(字节):4096
退出状态:0

我想知道为什么我的脚本只使用了 27% 的 CPU?磁盘 IO 根本没有什么(在 vmstat 输出中看到)。那么是什么导致了限制呢?脚本中的代码?

这是脚本:

#!/usr/bin/env bash
number=1

while [[ $number -lt 10000001 ]] ; do  
    fname="FirstName LastName $"
    lname=""  
    email="fname.lname.$number@domain.com"  
    password="1234567890"  
    altemail="lname.fname.$number@domain.com"  
    mobile="9876543210"      

    echo "$fname,$lname,$email,$password,$altemail,$mobile" >> /opt/list.csv
    number=$(expr $number + 1)  
done  
Run Code Online (Sandbox Code Playgroud)

Mar*_*ick 14

通过使用strace,我看到该行

number=$(expr $number + 1)
Run Code Online (Sandbox Code Playgroud)

导致 fork、路径搜索和 exec expr。(我在 Ubuntu 上使用 bash 4.2.45)。文件系统、磁盘和进程开销导致 bash 只获得大约 28% 的 CPU。

当我将该行更改为仅使用 shell 内置操作时

((number = number + 1))
Run Code Online (Sandbox Code Playgroud)

bash 使用了大约 98% 的 CPU,脚本在半小时内运行。这是在单 CPU 1.5GHz 赛扬上。

该脚本不执行任何并行运行的操作,因此拥有 32 个空闲 CPU 不会有太大帮助。但是,您当然可以将其并行化,例如,将其拆分为 10 个并行运行的 100 万次迭代循环,写入 10 个不同的文件,然后使用cat组合它们。

@Arthur2e5 添加了以下示例程序:

max=1000000 step=40000 tmp="$(mktemp -d)"
# Spawning. For loops make a bit more sense in a init-test-incr pattern.
for ((l = 0; l < max; l += step)); do (
    for ((n = l + 1, end = (step + l > max ? max : step + l);
      n <= end; n++)); do
        # Putting all those things into the `buf` line gives you a 1.8x speedup.
        fname="FirstName LastName \$"
        lname=""  
        email="fname.lname.$n@domain.com"  
        password="1234567890"  
        altemail="lname.fname.$n@domain.com"  
        mobile="9876543210"
        buf+="$fname,$lname,$email,$password,$altemail,$mobile"$'\n'
    done
    printf '%s\n' "$buf" > "$tmp/$l" ) &
done # spawning..
wait
# Merging. The filename order from globbing will be a mess,
# since we didn't format $l to some 0-prefixed numbers.
# Let's just do the loop again.
for ((l = 0; l < max; l += step)); do
    printf '%s\n' "$(<"$tmp/$l")" >> /opt/list.csv
done # merging..
rm -rf -- "$tmp" # cleanup
Run Code Online (Sandbox Code Playgroud)

  • 如果您可以在不使脚本难以理解的情况下尽可能多地使用内置函数,我会这样做。但是如果速度很重要,并且如果您不需要 shell 的文件名扩展或进程流水线或后台功能,实际上使用另一种语言(例如 `awk` 或 `perl`)可能会更好。例如,当我用 `awk` 重写你的脚本时,运行只需要 50 秒。 (2认同)