Как эффективно узнать время выполнения скрипта?

Вопрос или проблема

Я хотел бы отобразить время выполнения скрипта.

То, что я сейчас делаю, – это –

#!/bin/bash
date  ## выводим дату в начале
# содержимое скрипта
date  ## выводим дату в конце

Это просто показывает время начала и окончания скрипта. Возможно ли отображать более детализированный вывод, такой как время процессора/время ввода-вывода и т.д.?

Просто используйте time при вызове скрипта:

time yourscript.sh

Вывод (“# комментарии не являются частью вывода”):

real    2m5.034s # <-- Фактическое время, затраченное с начала до конца.
user    0m0.000s # <-- Время процессора в пользовательском пространстве.
sys     0m0.003s # <-- Время процессора в ядре.

Если time – это не вариант,

start=`date +%s`
stuff
end=`date +%s`

runtime=$((end-start))

или, если вам нужна точность до субсекунд и у вас установлен bc,

start=`date +%s.%N`
stuff
end=`date +%s.%N`

runtime=$( echo "$end - $start" | bc -l )

Просто вызовите times без аргументов при выходе из вашего скрипта.

С ksh или zsh вы также можете использовать time вместо. С zsh time также даст вам реальное время, помимо пользовательского и системного времени процессора.

Чтобы сохранить код завершения вашего скрипта, вы можете сделать так:

ret=$?; times; exit "$ret"

Или вы можете также добавить ловушку на EXIT:

trap times EXIT

Таким образом, times будет вызываться каждый раз, когда оболочка завершается, и статус выхода будет сохранен.

$ bash -c 'trap times EXIT; : {1..1000000}'
0m0.932s 0m0.028s
0m0.000s 0m0.000s
$ zsh -c 'trap time EXIT; : {1..1000000}'
shell  0.67s user 0.01s system 100% cpu 0.677 total
children  0.00s user 0.00s system 0% cpu 0.677 total

Также обратите внимание, что все из bash, ksh и zsh имеют специальную переменную $SECONDS, которая автоматически увеличивается каждую секунду. В zsh и ksh93 эта переменная также может быть сделана плавающей (с typeset -F SECONDS), чтобы получить более точные значения. Это только реальное время, не время процессора.

Мой метод для bash:

# Сброс счетчика времени BASH
SECONDS=0
    # 
    # выполняем действия
    # 
ELAPSED="Elapsed: $(($SECONDS / 3600))hrs $((($SECONDS / 60) % 60))min $(($SECONDS % 60))sec"

Я немного запаздываю, но хотел бы опубликовать свое решение (для субсекундной точности), на случай если кто-то наткнется на эту тему через поиск. Вывод в формате дней, часов, минут и, наконец, секунд:

res1=$(date +%s.%N)

# выполняем действия здесь

res2=$(date +%s.%N)
dt=$(echo "$res2 - $res1" | bc)
dd=$(echo "$dt/86400" | bc)
dt2=$(echo "$dt-86400*$dd" | bc)
dh=$(echo "$dt2/3600" | bc)
dt3=$(echo "$dt2-3600*$dh" | bc)
dm=$(echo "$dt3/60" | bc)
ds=$(echo "$dt3-60*$dm" | bc)

LC_NUMERIC=C printf "Общее время выполнения: %d:%02d:%02d:%02.4f\n" $dd $dh $dm $ds

Надеюсь, это будет полезно кому-то!

[редактировать] Вам нужно учитывать все символы в определении поля в bash printf, если вы хотите дополнять секунды до 2 цифр перед точкой, вам нужно определить это как %07.4f (все цифры и точка также считаются в длину поля), так что строка должна выглядеть так:
LC_NUMERIC=C printf "Общее время выполнения: %d:%02d:%02d:%07.4f\n" $dd $dh $dm $ds

Лично я люблю оборачивать весь код скрипта в какую-то “главную” функцию, вот так:

main () {
 echo выполняется ...
}

# действия ...

# вызываем функцию в самом конце скрипта
time main

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

Этот вопрос достаточно старый, но в попытках найти свой любимый способ сделать это, эта тема стала популярной… и я удивлён, что никто не упомянул об этом:

perf stat -r 10 -B sleep 1

‘perf’ – это инструмент анализа производительности, который включен в ядро под ‘tools/perf’ и часто доступен для установки как отдельный пакет (‘perf’ в CentOS и ‘linux-tools’ в Debian/Ubuntu). Вики Linux Kernal по perf содержит много другой информации об этом.

Запуск ‘perf stat’ дает довольно много деталей, включая среднее время выполнения прямо в конце:

1.002248382 seconds time elapsed                   ( +-  0.01% )
#!/bin/bash
start=$(date +%s.%N)

# ЗДЕСЬ БУДЕТ КОД

end=$(date +%s.%N)    
runtime=$(python -c "print(${end} - ${start})")

echo "Время выполнения составило $runtime"

Да, это вызывает Python, но если вас это устраивает, то это довольно неплохое, лаконичное решение.

Небольшая функция оболочки, которую можно добавить перед командами для измерения их времени:

#!/bin/bash

tm_date() {
  local start
  start=$(date +%s)
  "$@"
  local exit_code=$?
  echo >&2 "потратил ~$(($(date +%s) - start)) секунд. завершился с кодом ${exit_code}."
  return ${exit_code}
}

tm_secs() {
    local start=$EPOCHSECONDS
    "$@"
    local exit_code=$?
    echo >&2 "потратил ~$((EPOCHSECONDS - start)) секунд. завершился с кодом ${exit_code}."
    return ${exit_code}
}

tm_millisecs() {
    local start=${EPOCHREALTIME/./}
    "$@"
    local exit_code=$?
    echo >&2 "потратил ~$(( (${EPOCHREALTIME/./} - start)/1000 )) миллисекунд. завершился с кодом ${exit_code}."
    return ${exit_code}
}

tm_microsecs() {
    local start=${EPOCHREALTIME/./}
    "$@"
    local exit_code=$?
    echo >&2 "потратил ~$((${EPOCHREALTIME/./} - start)) микросекунд. завершился с кодом ${exit_code}."
    return ${exit_code}
}

Затем используйте это в своем скрипте или в командной строке, вот так:

tm original_command со всеми его параметрами

Например,

tm_date sleep 1
tm_secs sleep 1
tm_millisecs sleep 1
tm_microsecs sleep 1

Вывод будет таким:

потратил ~1 секунд. завершился с кодом 0.
потратил ~1 секунд. завершился с кодом 0.
потратил ~1002 миллисекунд. завершился с кодом 0.
потратил ~1001540 микросекунд. завершился с кодом 0.

Принятое решение с использованием time записывает в stderr.
Решение с использованием times записывает в stdout.
Решения с использованием $SECONDS не имеют субсекундной точности.
Другие решения предполагают вызов внешних программ, таких как date или perf, что неэффективно.
Если вас устраивает любое из этих, используйте это.

Но если вам нужно эффективное решение, чтобы получить времена с миллисекундной точностью и нужно, чтобы они были в переменных таким образом, что оригинальный вывод остается нетронутым, вы можете объединить подстановку процессов с некоторыми перенаправлениями вокруг time, что намного быстрее, чем вызов внешних программ и позволяет перенаправления вокруг обертки времени скрипта, как в оригинальной команде/скрипте.

# Подготовка:
Cmd=vgs  # пример программы, которую нужно замерить, которая записывает в stdout и stderr
Cmd="eval { echo stdout; echo stderr >&2; sleep 0.1; }"  # другой пример; замените на свой
TIMEFORMAT="%3R %3U %3S"  # сделать вывод времени легко для парсинга

Выберите один из следующих вариантов анализа вывода time, подходящий для ваших нужд:
Самый короткий вариант, где stdout $Cmd записывается в stderr и ничего не записывается в stdout:

read Elapsed User System < <({ time $Cmd 2>&3; } 3>&2 2>&1 >&3)

Длиннее вариант, который сохраняет оригинальный stdout и stderr отдельно:

{ read Elapsed User System < <({ time $Cmd 2>&4; } 4>&2 2>&1 >&3); } 3>&1

Самый сложный вариант включает закрытие дополнительных файловых дескрипторов, так что $Cmd вызывается, как если бы не было этой оберточной функции замера времени вокруг него, и команды lvm, такие как vgs, не жаловались бы на утечку файловых дескрипторов:

{ read Elapsed User System < <({ time $Cmd 2>&4 4>&-; } 4>&2 2>&1 >&3 3>&-); } 3>&1

Вы даже можете подделать плавающее дополнение в bash без вызова bc, что было бы значительно медленнее:

CPU=`printf %04d $((10#${User/.}+10#${System/.}))`  # замените на вашу последующую обработку
echo CPU ${CPU::-3}.${CPU: -3} s, Elapsed $Elapsed s >&2  # перенаправлено независимо от $Cmd

Возможные выводы с двумя примерами $Cmd на медленном процессоре:

Файловый дескриптор 3 (/dev/pts/1) утек на вызове vgs. Основной PID 10756: bash
Файловый дескриптор 4 (/dev/pts/1) утек на вызове vgs. Основной PID 10756: bash
  VG   #PV #LV #SN Attr   VSize VFree
  b3     3  24   0 wz--n- 1.31t 1.19t
CPU 0.052 s, Elapsed 0.056 s

Или:

stdout
stderr
CPU 0.008 s, Elapsed 0.109 s
  1. Просто используйте time [любая команда]. Например: time sleep 1 будет ожидать реальное время (т.е. как по часам) ~1.000 до ~1.020 сек, как показано здесь:

     $ time sleep 1
    
     real    0m1.011s
     user    0m0.004s
     sys 0m0.000s
    

    Какая прекрасная вещь. Вы можете поставить любую команду после этого, и она выводит результат в приятном, читаемом виде. Я действительно люблю использовать это для измерения времени сборок. Например:

     # измеряем время вашей сборки "make"
     time make
    
     # измеряем время вашей сборки "Bazel"
     time bazel build //path/to/some:target
    

    …или для операций git, которые потенциально могут быть действительно долгими, чтобы я мог развивать реалистичные ожидания:

     # измеряем, сколько времени потребуется для извлечения из огромного репозитория, когда
     # я работаю из дома во время COVID-19. Обратите внимание: `git pull`
     # гораздо медленнее, чем просто извлечение одной ветки
     # которая вам нужна с помощью `git pull origin `, поэтому просто извлекайте
     # или затаскивайте, что вам нужно!
     time git pull origin master
    
  2. Для более настроенных нужд в измерении времени, когда вам может потребоваться манипулировать выводом или преобразовывать его в другие формы, в bash используйте внутреннюю переменную $SECONDS. Вот демонстрация, включая преобразование этих секунд в другие единицы, такие как плавающие минуты:

    Обратите внимание, что dt_min округляется с 0.01666666666... (1 секунда = столько минут) до 0.017 в этом случае, поскольку я использую функцию printf для округления. Части sleep 1; ниже – это то место, где вы вызовете свой скрипт для выполнения и измерения, но я просто жду 1 секунду вместо этого ради этого демонстрационного примера.

    Команда:

     start=$SECONDS; sleep 1; end=$SECONDS; dt_sec=$(( end - start )); \
     dt_min=$(printf %.3f $(echo "$dt_sec/60" | bc -l)); \
     echo "dt_sec = $dt_sec; dt_min = $dt_min"
    

    Вывод:

     dt_sec = 1; dt_min = 0.017
    

Связано:

  1. Узнайте больше о bc и printf в моем ответе здесь: https://stackoverflow.com/questions/12722095/how-do-i-use-floating-point-division-in-bash/58479867#58479867
  2. Я не помню, где впервые узнал о команде time, но, возможно, это было в @Trudbert’s answer right here.
#!/bin/csh
#PBS -q glean
#PBS -l nodes=1:ppn=1
#PBS -l walltime=10:00:00
#PBS -o a.log
#PBS -e a.err
#PBS -V
#PBS -M [email protected]
#PBS -m abe
#PBS -A k4zhang-group
START=$(date +%s)
for i in {1..1000000}
do
echo 1
done
END=$(date +%s)
DIFF=$(echo "$END - $START" | bc)
echo "Это занимает DIFF=$DIFF секунд, чтобы завершить эту задачу..."

Использовать встроенную команду времени bash?

time: time [-p] PIPELINE
    Выполняет PIPELINE и выводит сводку реального времени, пользовательского времени CPU,
    и времени системного CPU, затраченного на выполнение PIPELINE после его завершения.
    Код возврата - это код возврата PIPELINE. Опция `-p' выводит
    сводку времени в немного другом формате. Это использует
    значение переменной TIMEFORMAT как формат вывода.

Пример:

TIMEFORMAT="Команда заняла %Rs"
time {
    sleep 0.1
}

Вывод:

Команда заняла 0.108s

С использованием только bash также возможно измерить и рассчитать время выполнения для части shell-скрипта (или прошедшее время для всего скрипта):

start=$SECONDS

... # выполняем времязатратные действия

end=$SECONDS

Теперь вы можете просто вывести разницу:

echo "длительность: $((end-start)) секунд."

если вам нужно только инкрементальное время выполнения, делайте:

echo "длительность: $((SECONDS-start)) секунд прошло.."

Вы также можете сохранить длительность в переменную:

let diff=end-start

Функция измерения времени, основанная на SECONDS, ограничена только секундным уровнем точности, не использует никаких внешних команд:

time_it() {
  local start=$SECONDS ts ec
  printf -v ts '%(%Y-%m-%d_%H:%M:%S)T' -1
  printf '%s\n' "$ts Запущено $*"
  "$@"; ec=$?
  printf -v ts '%(%Y-%m-%d_%H:%M:%S)T' -1
  printf '%s\n' "$ts Завершено $*; прошедшее время = $((SECONDS-start)) секунд"
  # убедитесь, что возврат кода команды, чтобы вызывающий мог использовать его
  return "$ec"
}

Например:

time_it sleep 5

выводит

2019-03-30_17:24:37 Запущено sleep 5 2019-03-30_17:24:42 Завершено
sleep 5; прошедшее время = 5 секунд

Еще одно встроенное решение (согласно: https://stackoverflow.com/a/385422/1350091) это:

/usr/bin/time -v command
#!/bin/bash
begin=$(date +"%s")

Скрипт

termin=$(date +"%s")
difftimelps=$(($termin-$begin))
echo "$(($difftimelps / 60)) минут и $(($difftimelps % 60)) секунд прошло для выполнения скрипта."

похожие на ответ @LeZuse, но поддерживают аргументы скрипта:

#!/bin/bash

main () {
   echo "мой первый аргумент это $1"
   # содержимое скрипта здесь
}

# "$@" будет развернут до аргументов скрипта
time main "$@"

Альтернатива, если вы хотите измерить несколько частей скрипта:

#!/bin/bash
mts=$(date +%s%3N);mtl=$mts

sleep 1
mtc=$(date +%s%3N);printf "строка $LINENO: %.3fs [+%.3fs]\\n" "$((mtc - mts))e-3" "$((mtc - mtl))e-3";mtl=$mtc

sleep 1.5
mtc=$(date +%s%3N);printf "строка $LINENO: %.3fs [+%.3fs]\\n" "$((mtc - mts))e-3" "$((mtc - mtl))e-3";mtl=$mtc

вывод:

строка 5: 1.007s [+1.007s]
строка 8: 2.514s [+1.507s]

Таким образом, он возвращает общее время выполнения и время с последней измеренной строки.

Вот вариант ответа Алекса. Я заботился только о минутах и секундах, но я также хотел, чтобы он был оформлен по-другому. Так что я сделал так:

start=$(date +%s)
end=$(date +%s)
runtime=$(python -c "print '%u:%02u' % ((${end} - ${start})/60, (${end} - ${start})%60)")

Чтобы вычислить прошедшее время для bash-скрипта в миллисекундах:

start_time=$(date +%s%3N)  # время начала в миллисекундах

sleep 1  # <-- код для замера здесь

end_time=$(date +%s%3N)  # # время окончания в миллисекундах
duration_ms=$((end_time - start_time))  # длительность в миллисекундах

echo "Время выполнения в мс: $duration_ms"

Проверено в bash версии 5.0.17.


Ссылки

В bash версии 3, как описано здесь, могут быть проблемы:

С bash V3 команда $(date +%s%N) вернет что-то вроде
1692824647N: и когда вы попытаетесь вычислить разницу, вы
получите 1692824647N: значение слишком велико для базовой ошибки. В bash V4
эта проблема решена, но если вы все еще на bash 3 и хотите
преодолеть эту проблему, лучше использовать
/usr/local/gnu/coreutils/bin/date библиотеку от GNU coreutils.

Ответ или решение

Для измерения времени выполнения скрипта в оболочке Bash существует несколько эффективных методов, которые позволяют вам получить как общее время выполнения, так и более детальные показатели, такие как время процессора и ввод-вывод. Вот несколько наиболее распространенных подходов.

1. Использование встроенной команды time

Самый простой способ измерить время выполнения скрипта – это запустить его с помощью команды time:

time ./your_script.sh

Это выведет три значения:

  • real: фактическое время выполнения (стена).
  • user: время, проведенное в пользовательском пространстве (использование CPU для выполнения процесса).
  • sys: время, проведенное в ядре (системные вызовы).

Вывод будет примерно таким:

real    0m1.034s
user    0m0.004s
sys     0m0.000s

2. Измерение времени с помощью переменных SECONDS

Переменная SECONDS автоматически увеличивается на 1 каждую секунду. Вы можете использовать ее для измерения времени выполнения:

SECONDS=0  # Сброс счётчика
# Ваш код
echo "Время выполнения: $SECONDS секунд."

Это простой способ, но он имеет ограничение по точности (измеряет только целые секунды).

3. Для более точного измерения (в миллисекундах)

Если вам нужна большая точность, вы можете использовать команду date:

start=$(date +%s.%N)  # Время начала с наносекундами
# Ваш код
end=$(date +%s.%N)  # Время окончания
runtime=$(echo "$end - $start" | bc)  # Разница
echo "Время выполнения: $runtime секунд."

4. Использование команды times

Вы также можете использовать встроенную команду times, которая предоставит общую информацию о времени процессора:

# Здесь выполняются ваши команды
times

Когда вы вызываете эту команду, вы получите информацию о процессе, включая время, проведённое в пользовательском пространстве и в ядре.

5. Измерение времени на отдельных участках кода

Если вам нужно замерять производительность на определённых участках, вы можете создать свою функцию:

time_it() {
    local start=$(date +%s.%N)
    "$@"
    local exit_code=$?
    local end=$(date +%s.%N)
    local runtime=$(echo "$end - $start" | bc)
    echo "Команда '$*' завершилась за $runtime секунд."
    return $exit_code
}

# Пример использования
time_it sleep 1

6. Использование инструмента perf

Для более глубокого анализа производительности вы можете использовать инструмент perf, который предоставляет детальную информацию о времени выполнения и производительности:

perf stat ./your_script.sh

Заключение

В зависимости от ваших требований к точности и детализации, вы можете выбрать один из предложенных методов. Для большинства задач простой time будет достаточен, тогда как для более сложных сценариев анализа может потребоваться использование date, times или специализированных инструментов вроде perf.

Оцените материал
Добавить комментарий

Капча загружается...