使用Bash计算程序的平均执行时间

13

要获取任何可执行文件(例如a.out)的执行时间,我可以简单地编写time ./a.out。这将输出实时时间、用户时间和系统时间。

是否可以编写一个bash脚本,运行程序多次,并计算和输出平均实际执行时间?


2
需要注意的唯一警告是,根据您的代码在缓存中保留的程度,您的后续运行将因缓存而人为地更快。 - David C. Rankin
4个回答

19

您可以编写一个循环来收集 time 命令的输出并将其管道传递给 awk 来计算平均值:

您可以编写一个循环来收集 time 命令的输出并将其管道传递给 awk 来计算平均值:
avg_time() {
    #
    # usage: avg_time n command ...
    #
    n=$1; shift
    (($# > 0)) || return                   # bail if no command given
    for ((i = 0; i < n; i++)); do
        { time -p "$@" &>/dev/null; } 2>&1 # ignore the output of the command
                                           # but collect time's output in stdout
    done | awk '
        /real/ { real = real + $2; nr++ }
        /user/ { user = user + $2; nu++ }
        /sys/  { sys  = sys  + $2; ns++}
        END    {
                 if (nr>0) printf("real %f\n", real/nr);
                 if (nu>0) printf("user %f\n", user/nu);
                 if (ns>0) printf("sys %f\n",  sys/ns)
               }'
}

例子:

avg_time 5 sleep 1

会给你

real 1.000000
user 0.000000
sys 0.000000

这可以很容易地增强为:

  • 在执行之间休眠一定的时间
  • 在执行之间随机休眠一段时间(在一定范围内)

man timetime -p 的含义:

   -p
      When in the POSIX locale, use the precise traditional format

      "real %f\nuser %f\nsys %f\n"

      (with  numbers  in seconds) where the number of decimals in the
      output for %f is unspecified but is sufficient to express the
      clock tick accuracy, and at least one.
您可能也想看看这个命令行基准测试工具:

sharkdp/hyperfine


当我将实际程序作为参数传递时,您的方法会导致错误“awk:division by zero”,我认为这个错误可能是由于舍入误差引起的,因为我的程序执行时间非常短。 - mooncow
请使用更新后的答案并查看是否有效。另外,请展示您的命令。 - codeforester
1
我相信程序现在已经正确运行。使用 time 进行一次任意的运行,结果为 real 0m0.121s,而运行脚本则为 real 0.105000。我用的命令是 avg_time 10 ./a.out 来执行我的程序。 - mooncow
1
注意!将N的总执行时间四舍五入后除以结果可能不准确,请参见我的回答 - F. Hauri - Give Up GitHub
1
尝试这个:avg_time 1000 sleep .001 !! - F. Hauri - Give Up GitHub
你为什么同时使用 &>2>&1 - Sapphire_Brick

4

总执行时间与单次执行时间之和

注意!将N个执行时间的总和除以N是不精确的!

相反,我们可以通过将N次迭代的总执行时间除以N来计算。

avg_time_alt() { 
    local -i n=$1
    local foo real sys user
    shift
    (($# > 0)) || return;
    { read foo real; read foo user; read foo sys ;} < <(
        { time -p for((;n--;)){ "$@" &>/dev/null ;} ;} 2>&1
    )
    printf "real: %.5f\nuser: %.5f\nsys : %.5f\n" $(
        bc -l <<<"$real/$n;$user/$n;$sys/$n;" )
}

注意: 这里使用 bc 而不是 awk 来计算平均值。为此,我们需要创建一个临时的 bc 文件:

printf >/tmp/test-pi.bc "scale=%d;\npi=4*a(1);\nquit\n" 60

这将计算出带有60位小数的,然后安静地退出。(您可以根据您的主机调整小数位数。)
演示:
avg_time_alt 1000 sleep .001
real: 0.00195
user: 0.00008
sys : 0.00016

avg_time_alt 1000 bc -ql /tmp/test-pi.bc
real: 0.00172
user: 0.00120
sys : 0.00058

codeforester的函数 将回答:

avg_time 1000 sleep .001
real 0.000000
user 0.000000
sys 0.000000

avg_time 1000 bc -ql /tmp/test-pi.bc
real 0.000000
user 0.000000
sys 0.000000

参考choroba的回答,使用Linux/proc实现替代方案

好的,你可以考虑:

avgByProc() { 
    local foo start end n=$1 e=$1 values times
    shift;
    export n;
    { 
        read foo;
        read foo;
        read foo foo start foo
    } < /proc/timer_list;
    mapfile values < <(
        for((;n--;)){ "$@" &>/dev/null;}
        read -a endstat < /proc/self/stat
        {
            read foo
            read foo
            read foo foo end foo
        } </proc/timer_list
        printf -v times "%s/100/$e;" ${endstat[@]:13:4}
        bc -l <<<"$[end-start]/10^9/$e;$times"
    )
    printf -v fmt "%-7s: %%.5f\\n" real utime stime cutime cstime
    printf "$fmt" ${values[@]}
}

这基于 /proc

man 5 proc | grep [su]time\\\|timer.list | sed  's/^/>   /'
            (14) utime  %lu
            (15) stime  %lu
            (16) cutime  %ld
            (17) cstime  %ld
     /proc/timer_list (since Linux 2.6.21)

然后现在:

avgByProc 1000 sleep .001
real   : 0.00242
utime  : 0.00015
stime  : 0.00021
cutime : 0.00082
cstime : 0.00020

utimestime分别代表bash本身的用户时间系统时间,而cutimecstime则代表子进程用户时间子进程系统时间这是最有趣的部分

注:在这种情况下(sleep命令),不会使用大量资源。

avgByProc 1000 bc -ql /tmp/test-pi.bc
real   : 0.00175
utime  : 0.00015
stime  : 0.00025
cutime : 0.00108
cstime : 0.00032

这将变得更加清晰... 当然,由于按顺序访问timer_listself/stat而不是原子性地访问,可能会出现real(基于纳秒)和c?[su]time(基于ticks即1/100秒)之间的差异!


1
非常棒的见解!我非常喜欢你的解决方案。我同意这是一个更好的方法。但愿 time -p 能给出更精确的数字。 - codeforester
@codeforester,你测试过我的替代方案了吗?它基于/proc/subShellPid/stat,显示了子进程的系统和用户时间。 - F. Hauri - Give Up GitHub
对于使用逗号而不是小数点的非英语区域,bc 将产生 (standard_in) 1: syntax error 的错误,必须使用类似以下的命令:shopt -s expand_aliases; alias bc="sed 's/,/./g' | bc | sed 's/\./,/g'" 不需要 -l,但在第一个示例中建议使用 scale=5 而不是 %.5f,因为 bc 会破坏长行。 - dlazesz
啊!对于每个值,都要使用 sed | bc | sed。最好强制使用 LANG=C bc!!! - F. Hauri - Give Up GitHub

1

来自bashoneliners

  • 为了i18n支持而适应将(,)转换成(.)
  • 硬编码为10,根据需要进行调整
  • 仅返回“真实”值,即您最可能想要的值

Oneliner

for i in {1..10}; do time $@; done 2>&1 | grep ^real | sed s/,/./ | sed -e s/.*m// | awk '{sum += $1} END {print sum / NR}'

我制作了一个“更完整”的版本
  • 输出每次执行的结果,以便您知道正确的内容被执行
  • 显示每次运行时间,以便您查看异常值

但是,如果您需要高级功能,请使用hyperfine。

GREEN='\033[0;32m'
PURPLE='\033[0;35m'
RESET='\033[0m'

# example: perf sleep 0.001
# https://serverfault.com/questions/175376/redirect-output-of-time-command-in-unix-into-a-variable-in-bash
perfFull() {
    TIMEFORMAT=%R                       # `time` outputs only a number, not 3 lines
    export LC_NUMERIC="en_US.UTF-8"     # `time` outputs `0.100` instead of local format, like `0,100`

    times=10

    echo -e -n "\nWARMING UP ${PURPLE}$@${RESET}"
    $@ # execute passed parameters

    echo -e -n "RUNNING ${PURPLE}$times times${RESET}"

    exec 3>&1 4>&2                                   # redirects subshell streams
    durations=()
    for _ in `seq $times`; {
        durations+=(`{ time $@ 1>&3 2>&4; } 2>&1`)   # passes stdout through so only `time` is caputured
    }
    exec 3>&- 4>&-                                   # reset subshell streams

    printf '%s\n' "${durations[@]}"

    total=0
    for duration in "${durations[@]}"; {
        total=$(bc <<< "scale=3;$total + $duration")
    }

    average=($(bc <<< "scale=3;$total/$times"))
    echo -e "${GREEN}$average average${RESET}"
}

0

记录执行的开始和结束时间,然后将差值除以执行次数可能会更容易。

#!/bin/bash
times=10
start=$(date +%s)
for ((i=0; i < times; i++)) ; do
    run_your_executable_here
done
end=$(date +%s)
bc -l <<< "($end - $start) / $times"

我使用bc计算平均值,因为Bash不支持浮点数运算。
要获得更高的精度,您可以切换到纳秒:
start=$(date +%s.%N)

同样地,对于$end也是如此。


+%s 替换为 '+%s.%N' 会抛出错误 (standard_in) 1: illegal character: N - mooncow
@mooncow:那么你的“date”与我的不同(GNU 8.25)。 - choroba
在最近的Linux内核下,使用纯Bash,你可以这样做:{ read foo;read foo;read foo foo now foo; } </proc/timer_list,而不需要任何fork,然后echo $now表示纳秒级别的uptime - F. Hauri - Give Up GitHub
如果/proc/timer_list的权限为-r--r--r-- root root,则返回“是”。 - F. Hauri - Give Up GitHub
@choroba 你可以看一下我的Alternative, inspired by choroba's answer,基于/proc - F. Hauri - Give Up GitHub

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接