Как измерить время выполнения программы и сохранить его в переменной

61

Чтобы выяснить, сколько времени занимают определенные операции в скрипте Bash (v4 +), я хотел бы проанализировать выходные данные timeкоманды «по отдельности» и (в конечном итоге) перехватить ее в переменной Bash ( let VARNAME=...).

Теперь я использую time -f '%e' ...(или, скорее, command time -f '%e' ...из-за встроенного Bash), но так как я уже перенаправил вывод выполненной команды, я действительно потерян, чтобы узнать, как я собираюсь захватить вывод timeкоманды. В основном проблема здесь , чтобы отделить выход из timeот выхода выполненной команды (ов).

Что мне нужно, так это функциональность подсчета количества времени в секундах (целых числах) между началом команды и ее завершением. Это не обязательно должна быть timeкоманда или соответствующий встроенный элемент.


Изменить: учитывая два полезных ответа ниже, я хотел бы добавить два разъяснения.

  1. Я не хочу отбрасывать вывод выполненной команды, но на самом деле не имеет значения, окажется ли он на stdout или stderr.
  2. Я бы предпочел прямой подход, а не косвенный (т. Е. Перехватывать вывод напрямую, а не сохранять его в промежуточных файлах).

Решение, использующее dateдо сих пор, подходит к тому, что я хочу.

0xC0000022L
источник
Самый прямой способ получить данные и обработать их, продолжая при этом нормально работать, - это сделать это в программе на C, используя fork(), execvp()и wait3()/wait4(). В конечном итоге это то, что время и друзья делают. Я не знаю простого способа сделать это в bash / perl без перенаправления в файл или подобного подхода.
penguin359
Существует связанный с этим вопрос , который вы могли бы найти интересное здесь происходит .
Калеб
@Caleb: спасибо. Действительно интересно. Однако для моих целей достаточно сделать это в сценарии.
0xC0000022L

Ответы:

85

Чтобы получить выходные данные timeв var, используйте следующее:

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

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

Вы также можете просто запросить один тип времени, например, utime:

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

Чтобы получить время, которое вы также можете использовать date +%s.%N, возьмите его до и после выполнения и рассчитайте разницу:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
binfalse
источник
4
Я не хотел выбрасывать вывод команды. Поэтому я думаю, что ваш третий блок кода ближе всего к тому, что я имел в виду. Хотя я бы написал последний как DIFF=$((END-START)), используя арифметические выражения. :) ... Спасибо за ответ. +1
0xC0000022L
1
@STATUS_ACCESS_DENIED: Bash не выполняет арифметику с плавающей запятой, поэтому, если вы хотите получить лучшее разрешение, чем второе ( .%Nв коде binfalse), вам нужны bcили более сложные вычисления.
Жиль "ТАК - перестань быть злым"
@ Жиль: я знаю. Как я уже писал в моем вопросе целые числа в порядке. Не нужно иметь разрешение выше второго. Но спасибо, дата вызова должна быть изменена. Я понял это, хотя.
0xC0000022L
6
К вашему сведению, форматирование даты% N не работает в Mac OS X, оно просто возвращает «N». Хорошо в Ubuntu.
Дэвид
Обратите внимание, что хотя time (cmd) 2> somethingработа по перенаправлению вывода синхронизации выполняется file, она не предназначена (согласно документации), не используется в других оболочках, где timeесть ключевое слово, и может рассматриваться как ошибка . Я бы не стал полагаться на это, так как это может не сработать в будущих версиях bash.
Стефан Шазелас
17

В bash выходные данные timeконструкции возвращаются к стандартной ошибке, и вы можете перенаправить стандартную ошибку конвейера, на которую она влияет. Итак , давайте начнем с командой , которая записывает его вывода и ошибок streamas: sh -c 'echo out; echo 1>&2 err'. Чтобы не перепутать поток ошибок команды с выводом из time, мы можем временно перенаправить поток ошибок команды в другой файловый дескриптор:

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

Это записывает outв fd 1, errв fd 3 и время в 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:/')

Было бы приятнее иметь errна fd 2 и время на fd 3, поэтому мы поменяем их местами, что обременительно, потому что нет прямого способа поменять местами два дескриптора файла:

{ { { 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:/')

Это показывает, как вы можете постобработать выходные данные команды, но если вы хотите захватить как выходные данные команды, так и ее время, вам нужно работать усерднее. Использование временного файла является одним из решений. Фактически, это единственное надежное решение, если вам нужно зафиксировать как стандартную ошибку команды, так и ее стандартный вывод. Но в противном случае вы можете захватить весь вывод и использовать тот факт, что он timeимеет предсказуемый формат (если вы используете, time -pчтобы получить формат POSIX или TIMEFORMATпеременную, специфичную для 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*}

Если вы заботитесь только о времени настенных часов, запуск dateдо и после является простым решением (если немного более неточным из-за дополнительного времени, потраченного на загрузку внешней команды).

Жиль "ТАК - перестань быть злым"
источник
Вау, много для тестирования. Большое спасибо за обширный ответ. +1.
0xC0000022L
7

Со временем выходные данные команды выводятся на стандартный вывод, а время - на стандартный вывод. Итак, чтобы разделить их, вы можете сделать:

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

Но сейчас время в файле. Я не думаю, что Bash может напрямую помещать stderr в переменную. Если вы не против перенаправить вывод команды куда-то, вы можете сделать:

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

Когда вы сделаете это, вывод команды будет включен, outputfileи время, необходимое для выполнения, будет $FOO.

Мэринус
источник
1
о, я не поняла. Я знал, что timeпишет в stderr, но я не знал, что он объединит stdout и stderr выполненной команды в свой stdout. Но вообще нет прямого метода (т.е. без промежуточного файла)? +1.
0xC0000022L
3
@STATUS_ACCESS_DENIED: timeне объединяет команды stdoutи stderr. Способ, который я показал, предполагал, что вам нужен только вывод команды stdout. Так как bashбудет хранить только то, что выходит stdout, вам придется перенаправить. В случае, если вы можете безопасно отбросить команду stderr: время всегда будет на последней строке stderr. Если вам нужны оба выходных потока вашей команды, я бы предложил обернуть их в другой скрипт.
марин
6

Если вы находитесь bash(и не sh), и вам НЕ нужна точность менее секунды, вы можете полностью пропустить вызов dateи сделать это, не вызывая дополнительных процессов, не разделяя объединенный вывод и не собирая и не анализируя вывод. из любых команд:

# 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
источник
Приятно. Я никогда не слышал о
СЕКУНДАХ
Вы случайно не знаете, с какой версии Bash эта версия была представлена?
0xC0000022L
4

Для этой цели, вероятно, лучше использовать times(чем time), в bashкотором печатается накопленное пользовательское и системное время для оболочки и для процессов, запускаемых из оболочки, пример использования:

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

Смотрите: help -m timesдля получения дополнительной информации.

kenorb
источник
4

Во все предыдущие ответы, когда в OSX

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

ты можешь делать как

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
источник
1

Установить /bin/time(например pacman -S time)

Так что вместо ошибки при попытке -fпометки:

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

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

Вы можете фактически использовать это:

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

И получите то, что вы хотите - время в переменной (пример использует %eдля реального времени, для проверки других параметров man time):

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

echo "$variable"
Гжегож Вежовецкий
источник
/usr/bin/timeна многих системах
Старынкевич,
Если вы посмотрите внимательно, я указал на это в своем вопросе. timeэто оболочка, встроенная в Bash (и, вероятно, другие оболочки), но пока путь к timeисполняемому файлу находится внутри PATH, мы можем использовать ее, command timeчтобы убедиться, что мы запускаем внешнюю команду, а не встроенную.
0xC0000022L
1

Так же, как хедз-ап, работая с приведенными выше утверждениями и особенно принимая во внимание ответ Гжегожа. Я был удивлен, увидев на моей системе Ubuntu 16.04 Xenial эти два результата:

$ 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

У меня не установлены псевдонимы, и поэтому я не знаю, почему это происходит.

Пол Притчард
источник
1
timeтакже встроенная оболочка.
Старынкевич,
Если вы хотите убедиться, что вы запускаете команду, используйте command, если вы хотите убедиться, что вы используете встроенную команду, используйте builtin. Здесь нет магии. Используется helpдля определения доступных команд или type <command>для определения типа <command>(например, встроенная, внешняя команда, функция или псевдоним).
0xC0000022L
Базиль, ты совершенно прав. Я не заметил значение commandкоманды. Что является фактом, гарантирует, что / usr / bin / time вызывается. Это означает, что следующие два утверждения совместимы: $ command time -f %e sleep 4и$ /usr/bin/time -f %e sleep
Пол Притчард
0

попробуйте это, он запускает простую команду с аргументами и помещает времена $ real $ user $ sys и сохраняет код выхода.

Он также не разветвляет подоболочки и не вмешивается ни в какие переменные, кроме реального пользователя sys, и не мешает выполнению сценария.

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

например

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

примечание: это всего лишь простая команда, а не конвейер, компоненты которого запускаются в подоболочке

Эта версия позволяет указать в качестве $ 1 имя переменных, которые должны получить 3 раза:

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
}

например

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

и может быть полезно, если в конечном итоге он вызывается рекурсивно, чтобы не растоптать время; но тогда рус и т. д. должны быть объявлены локальными при их использовании.

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

Сэм Лиддикотт
источник