Como medir o tempo de execução do programa e armazená-lo dentro de uma variável

61

Para descobrir quanto tempo levam certas operações em um script Bash (v4 +), eu gostaria de analisar a saída do timecomando "separadamente" e (em última análise) capturá-la em uma variável Bash ( let VARNAME=...).

Agora, estou usando time -f '%e' ...(ou melhor, command time -f '%e' ...por causa do Bash interno), mas como já redirecionei a saída do comando executado, estou realmente perdido quanto à maneira como capturaria a saída do timecomando. Basicamente, o problema aqui é separar a saída da timesaída do (s) comando (s) executado (s).

O que eu quero é a funcionalidade de contar a quantidade de tempo em segundos (números inteiros) entre o início de um comando e sua conclusão. Não precisa ser o timecomando ou o respectivo built-in.


Editar: dadas as duas respostas úteis abaixo, eu queria acrescentar dois esclarecimentos.

  1. Eu não quero jogar fora a saída do comando executado, mas realmente não importa se ele termina em stdout ou stderr.
  2. Eu preferiria uma abordagem direta sobre uma indireta (ou seja, capturando a saída diretamente, em vez de armazená-la em arquivos intermediários).

A solução usando dateaté agora chega perto do que eu quero.

0xC0000022L
fonte
A maneira mais direta de obter e manipular os dados e ainda assim deixá-los rodar normalmente seria fazê-los em um programa C usando fork(), execvp()e wait3()/wait4(). Em última análise, é isso que tempo e amigos estão fazendo. Não conheço uma maneira simples de fazer isso no bash / perl sem redirecionar para um arquivo ou abordagem semelhante.
penguin359
Há uma pergunta relacionada que você pode achar interessante acontecendo aqui .
Caleb
@ Caleb: obrigado. Realmente interessante. No entanto, para meus propósitos, é suficiente fazê-lo em um script.
0xC0000022L

Respostas:

85

Para obter a saída de timeem um var, use o seguinte:

usr@srv $ mytime="$(time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$mytime"

real    0m0.006s
user    0m0.001s
sys     0m0.005s

Você também pode pedir apenas um tipo de horário, por exemplo, utime:

usr@srv $ utime="$( TIMEFORMAT='%lU';time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$utime"
0m0.000s

Para obter o tempo que você também pode usar date +%s.%N, faça isso antes e depois da execução e calcule o diff:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
binfalse
fonte
4
Eu não queria jogar fora a saída do comando. Acho que seu terceiro bloco de código é o mais próximo do que eu tinha em mente. Embora eu escrevesse o último como DIFF=$((END-START)), usando as expressões aritméticas. :) ... obrigado pela resposta. +1
0xC0000022L
11
@STATUS_ACCESS_DENIED: O Bash não faz aritmética de ponto flutuante; portanto, se você quiser algo melhor que a segunda resolução ( .%Nno código do binfalse), precisará de bccálculos mais sofisticados.
Gilles 'SO- stop be evil'
@ Gilles: eu sei. Como escrevi na minha pergunta, números inteiros são bons. Não é necessário ter uma resolução maior que a segunda. Mas obrigado, a invocação da data teria que ser alterada. Eu tinha percebido isso, no entanto.
0xC0000022L
6
Para sua informação, a formatação da data% N parece não funcionar no Mac OS X, mas apenas retorna "N". Ok no Ubuntu.
David
Observe que, embora o time (cmd) 2> somethingtrabalho de redirecionar a saída de tempo para file, não seja feito (conforme a documentação), não ocorre em outros shells onde timeexiste uma palavra-chave e pode ser considerado um bug . Eu não confiaria nele, pois pode não funcionar em versões futuras do bash.
Stéphane Chazelas
17

No bash, a saída da timeconstrução vai para o erro padrão e você pode redirecionar o erro padrão do pipeline que ele afeta. Então, vamos começar com um comando que grava seus streamas de saída e de erro: sh -c 'echo out; echo 1>&2 err'. Para não misturar o fluxo de erros do comando com a saída de time, podemos desviar temporariamente o fluxo de erros do comando para um descritor de arquivo diferente:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; }

Isso grava outem fd 1, errem fd 3 e os tempos em fd 2:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } \
    3> >(sed 's/^/ERR:/') 2> >(sed 's/^/TIME:/') > >(sed 's/^/OUT:/')

Seria mais agradável ter errno fd 2 e os tempos no fd 3, por isso, nós os trocamos, o que é complicado porque não há maneira direta de trocar dois descritores de arquivo:

{ { { time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } 3>&2 2>&4; } 4>&3; } 3> >(sed 's/^/TIME:/') 2> >(sed 's/^/ERR:/') > >(sed 's/^/OUT:/')

Isso mostra como você pode pós-processar a saída do comando, mas se desejar capturar a saída do comando e seus horários, precisará trabalhar mais. Usar um arquivo temporário é uma solução. De fato, é a única solução confiável se você precisar capturar o erro padrão do comando e sua saída padrão. Mas, caso contrário, você pode capturar toda a saída e aproveitar o fato de timeter um formato previsível (se você usar time -ppara obter o formato POSIX ou a TIMEFORMATvariável específica do bash ).

nl=$'\n'
output=$(TIMEFORMAT='%R %U %S %P'; mycommand)
set ${output##*$nl}; real_time=$1 user_time=$2 system_time=$3 cpu_percent=$4
output=${output%$nl*}

Se você se importa apenas com o tempo do relógio de parede, executar o dateantes e o depois é uma solução simples (se um pouco mais imprecisa devido ao tempo extra gasto no carregamento do comando externo).

Gilles 'SO- parar de ser mau'
fonte
Uau, muito para testar. Muito obrigado pela extensa resposta. +1.
0xC0000022L
7

Com o tempo, a saída do comando sai no stdout e o tempo sai no stderr. Então, para separá-los, você pode fazer:

command time -f '%e' [command] 1>[command output file] 2>[time output file]

Mas agora o tempo está em um arquivo. Eu não acho que o Bash é capaz de colocar o stderr em uma variável diretamente. Se você não se importa em redirecionar a saída do comando para algum lugar, você pode:

FOO=$((( command time -f '%e' [command]; ) 1>outputfile; ) 2>&1; )

Quando você fizer isso, a saída do comando estará dentro outputfilee o tempo que levou para executar será $FOO.

marinus
fonte
11
ooh, eu não percebi. Eu sabia que timeescrevia para stderr, mas não sabia que ele mesclaria stdout e stderr do comando executado em seu stdout. Mas geralmente não há método direto (ou seja, sem arquivo intermediário)? +1.
0xC0000022L
3
@STATUS_ACCESS_DENIED: timenão mescla os comandos stdoute stderr. A maneira como mostrei assumiu que você só precisava da saída stdout do comando. Como bashapenas armazenará o que sai stdout, você precisará redirecionar. Caso você possa descartar com segurança o stderr do seu comando: o tempo sempre estará na última linha do stderr. Se você precisar dos dois fluxos de saída do comando, sugiro agrupá-lo em outro script.
Marinus
6

Se você está dentro bash(e não sh) e NÃO precisa de precisão de um segundo, pode pular a chamada datetotalmente e fazê-lo sem gerar nenhum processo extra, sem ter que separar a saída combinada e sem ter que capturar e analisar a saída de qualquer comando:

# SECONDS is a bash special variable that returns the seconds since set.
SECONDS=0
mycmd <infile >outfile 2>errfile
DURATION_IN_SECONDS=$SECONDS
# Now `$DURATION_IN_SECONDS` is the number of seconds.
sanpath
fonte
Agradável. Eu nunca ouvi falar de SEGUNDOS antes
Bruno9779 22/03
Você sabe começar com qual versão do Bash foi introduzida?
0xC0000022L 23/03
4

Para esse propósito, é provavelmente melhor usar times(que time) o bashqual imprime os tempos acumulados de usuário e sistema para o shell e para processos executados a partir do shell, por exemplo, use:

$ (sleep 2; times) | (read tuser tsys; echo $tuser:$tsys)
0m0.001s:0m0.003s

Veja: help -m timespara mais informações.

kenorb
fonte
4

Ao reunir todas as respostas anteriores, quando estiver no OSX

ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31+

você pode fazer como

microtime() {
    python -c 'import time; print time.time()'
}
compute() {
    local START=$(microtime)
    #$1 is command $2 are args
    local END=$(microtime)
    DIFF=$(echo "$END - $START" | bc)
    echo "$1\t$2\t$DIFF"
}
Loretoparisi
fonte
1

Instalar /bin/time(por exemplo pacman -S time)

Então, em vez de erro ao tentar -fsinalizar:

$ time -f %e sleep 0.5
bash: -f: command not found

real    0m0.001s
user    0m0.001s
sys     0m0.001s

Você pode realmente usá-lo:

$ /bin/time -f %e sleep 0.5
0.50

E obtenha o que deseja - tempo na variável (exemplo: usa %epara o tempo decorrido real, para outras opções, verifique man time):

#!/bin/bash
tmpf="$(mktemp)"
/bin/time -f %e -o "$tmpf" sleep 0.5
variable="$(cat "$tmpf")"
rm "$tmpf"

echo "$variable"
Grzegorz Wierzowiecki
fonte
/usr/bin/timeem muitos sistemas
Basile Starynkevitch
Se você olhar de perto, apontei isso na minha pergunta. timeé um shell embutido no Bash (e presumivelmente em outros shells), mas enquanto o caminho para o timeexecutável estiver PATH, podemos usar command timepara garantir que estamos executando o comando externo em oposição ao embutido.
0xC0000022L
1

Assim como um aviso, ao trabalhar com as declarações acima e, especialmente, tendo em mente a resposta de Grzegorz. Fiquei surpreso ao ver no meu sistema Ubuntu 16.04 Xenial esses dois resultados:

$ which time
/usr/bin/time

$ time -f %e sleep 4
-f: command not found
real    0m0.071s
user    0m0.056s
sys     0m0.012s

$ /usr/bin/time -f %e sleep 4
4.00

Eu não tenho nenhum apelido definido e, portanto, não sei por que isso está acontecendo.

Paul Pritchard
fonte
11
timetambém é um shell interno.
Basile Starynkevitch
Se você deseja garantir que está executando o comando, use command, se deseja garantir que está executando o builtin, use builtin. Não há mágica aqui. Use helppara descobrir os comandos disponíveis ou type <command>para descobrir qual <command>é o tipo (por exemplo, um interno, comando externo, função ou alias).
0xC0000022L
Basile, você está certo. Eu não tinha percebido o significado do commandcomando. O que garante que é chamado / usr / bin / time. Isso significa que as duas instruções a seguir são compatíveis: $ command time -f %e sleep 4e$ /usr/bin/time -f %e sleep
Paul Pritchard
0

tente isso, ele executa um comando simples com argumentos e coloca as vezes $ real $ user $ sys e preserva o código de saída.

Ele também não bifurca subconjuntos ou atropela nenhuma variável, exceto o sistema do usuário real, e não interfere na execução do script.

timer () {
  { time { "$@" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $?
  read -d "" _ real _ user _ sys _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

por exemplo

  timer find /bin /sbin /usr rm /tmp/
  echo $real $user $sys

note: apenas o comando simples não é um pipeline, cujos componentes são executados em uma subshell

Esta versão permite especificar como $ 1 o nome das variáveis ​​que devem receber as 3 vezes:

timer () {
  { time { "${@:4}" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $? "$@"
  read -d "" _ "$2" _ "$3" _ "$4" _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

por exemplo

  timer r u s find /bin /sbin /usr rm /tmp/
  echo $r $u $s

e pode ser útil se acabar sendo chamado recursivamente, para evitar pisar nos tempos; mas então rus etc deve ser declarado local em seu uso.

http://blog.sam.liddicott.com/2016/01/timeing-bash-commands.html

Sam Liddicott
fonte