Профилирование скриптов и отказ от ненужных подоболочек
Измеряйте время выполнения скриптов и заменяйте ресурсоёмкие цепочки вроде cat и grep встроенными альтернативами
«Профилирование скриптов и отказ от ненужных подоболочек» — бесплатный урок Linux Command Line & Bash Scripting Mastery на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Linux Command Line & Bash Scripting Mastery, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.
Почему производительность скриптов важна
Медленно работающие скрипты Bash тратят время CI, блокируют задания cron и раздражают пользователей. Большинство замедлений вызвано не сложной логикой, а ненужным созданием дочерних процессов: каждая вызываемая внешняя команда запускает новый дочерний процесс.
В этом уроке Вы научитесь:
- измерять, на что действительно уходит время, с помощью
timeиbash -x - выявлять шаблоны с чрезмерным созданием процессов, например бесполезное использование cat
- заменять внешние команды более быстрыми встроенными средствами оболочки
- осознанно использовать дочерние оболочки и избегать их, когда они не дают преимуществ
Цель — писать скрипты, выполняющие ту же работу с меньшим числом дочерних процессов и за меньшее реальное время.
Измерение времени работы скрипта со встроенной командой time
Самый простой инструмент профилирования — встроенная команда оболочки time. Добавьте её перед любой командой или конвейером, чтобы получить три измерения:
- real — фактически прошедшее время (сколько Вы действительно ждёте)
- user — время CPU, затраченное на код в пользовательском пространстве
- sys — время CPU, затраченное в ядре (системные вызовы, ввод-вывод)
Большая разница между real и user+sys обычно означает, что скрипт ожидает ввод-вывод или создаёт много дочерних процессов. Сначала запустите time для всего скрипта, чтобы подтвердить наличие проблемы, и только потом что-либо оптимизируйте.
#!/usr/bin/env bash
# Time a whole script block
time {
for i in $(seq 1 1000); do
echo "line $i"
done | grep -c "5"
}
# Output example:
# 271
# real 0m0.045s
# user 0m0.038s
# sys 0m0.012sТрассировка выполнения с помощью bash -x и PS4
bash -x выводит каждую команду перед её выполнением — это трассировка выполнения. Она показывает, какие строки выполняются чаще всего и не вызываются ли внешние программы чаще, чем Вы ожидали.
По умолчанию перед каждой отслеживаемой строкой выводится префикс +. Вы можете дополнить префикс с помощью PS4, добавив временные метки и превратив трассировку в лёгкий профилировщик:
PS4раскрывается перед каждой отслеживаемой командой- Добавление
$EPOCHREALTIME(bash 5+) или$(date +%s%N)обеспечивает наносекундное разрешение - Перенаправьте стандартный поток ошибок в файл и обработайте его после выполнения, чтобы найти медленные участки
#!/usr/bin/env bash
# Run with: bash -x ./myscript.sh 2>trace.log
# Or embed tracing inside the script:
export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE}:${LINENO}: '
set -x
slow_function() {
local result
result=$(cat /etc/hostname) # fork — slow
echo "host: $result"
}
slow_function
set +x
# trace.log now contains timestamps so you can diff
# adjacent lines to find which step took longest.Что такое ненужная дочерняя оболочка
Дочерняя оболочка — это дочерняя копия текущего процесса оболочки. Она создаётся в следующих случаях:
- Подстановка команды:
$(command) - Группировка в скобках:
( commands ) - Передача данных в конструкцию оболочки через конвейер:
cmd | while read ...
Дочерние оболочки необходимы, когда Вам действительно нужна изоляция или конвейер. Они становятся ненужными, если используются только для вызова внешней программы, с которой могла бы справиться сама оболочка, или если встроенная команда без причины оборачивается в дополнительный процесс.
Создание каждой дочерней оболочки занимает около 1–5 мс в современной системе Linux. В цикле, выполняющемся 10 000 раз, 1000 ненужных дочерних оболочек добавляют 1–5 секунд чистых накладных расходов.
Классический антишаблон: бесполезное использование cat
cat file | grep pattern — самый известный антишаблон с чрезмерным созданием процессов. Он запускает два процесса (cat и grep), соединённых конвейером, хотя одна только команда grep может напрямую читать файл.
Исправление простое: передайте имя файла непосредственно команде, которая умеет работать с файлами. Если инструмент не принимает имена файлов, это называется перенаправлением ввода; если принимает — просто не используйте cat.
- Медленно:
cat file | grep pattern— 2 процесса, 1 конвейер - Быстро:
grep pattern file— 1 процесс, без конвейера - Тоже быстро:
grep pattern < file— 1 процесс, перенаправление стандартного ввода (без буфера конвейера)
#!/usr/bin/env bash
# Create a sample file
seq 1 10000 > /tmp/numbers.txt
# --- Slow: useless cat ---
time cat /tmp/numbers.txt | grep -c "^5"
# --- Fast: grep reads the file directly ---
time grep -c "^5" /tmp/numbers.txt
# Both print the same count; the second is measurably faster
# because it skips the cat process and the inter-process pipe.Замена внешних команд встроенными средствами оболочки
Для многих однострочных преобразований существует встроенный эквивалент, полностью избегающий создания дочернего процесса. Сравните следующие распространённые замены:
echo ${#var}вместоecho "$var" | wc -c— длина строки${var^^}и${var,,}вместоecho "$var" | tr 'a-z' 'A-Z'— изменение регистра (bash 4+)${var//search/replace}вместоecho "$var" | sed 's/search/replace/'— простая замена[[ "$var" =~ pattern ]]вместоecho "$var" | grep -q pattern— проверка соответствия регулярному выражениюread -r line < fileвместоline=$(head -n1 file)— чтение первой строки
Ни одна из этих встроенных команд не создаёт дочерний процесс. Экономия при одном вызове невелика, но внутри циклов она быстро становится значительной.
#!/usr/bin/env bash
sentence="hello world from bash"
# --- Fork-heavy ---
upper_slow=$(echo "$sentence" | tr 'a-z' 'A-Z')
length_slow=$(echo "$sentence" | wc -c)
# --- Builtin equivalents (zero extra processes) ---
upper_fast=${sentence^^}
length_fast=${#sentence}
echo "Slow upper : $upper_slow"
echo "Fast upper : $upper_fast"
echo "Slow length: $length_slow"
echo "Fast length: $length_fast"Избегание дочерних оболочек внутри циклов
Подстановка команды внутри цикла умножает затраты на создание дочернего процесса на число итераций. Цикл, выполняющийся 500 раз и содержащий один вызов $(date), создаёт 500 дочерних процессов только для получения временных меток.
Стратегии снижения накладных расходов цикла:
- Перенесите неизменяемые команды за пределы цикла (вычислите один раз и используйте повторно)
- Предпочитайте арифметическое раскрытие
$(( expr ))— это встроенная возможность, а не создание дочернего процесса - Используйте
printfвместо вызоваdate, если требуется только форматирование - Объединяйте внешние вызовы: сначала соберите данные, затем обработайте их один раз за пределами цикла
#!/usr/bin/env bash
# Demonstrate: compute-once vs fork-per-iteration
# Bad: $(date) forks 1000 times
time (
for i in $(seq 1 1000); do
ts=$(date +%s) # fork each iteration
echo "$i $ts" > /dev/null
done
)
# Good: capture once, reuse
time (
ts=$(date +%s) # fork exactly once
for i in $(seq 1 1000); do
echo "$i $ts" > /dev/null
done
)Дочерние оболочки конвейера и проблема области видимости переменных
В bash (в отличие от ksh/zsh) каждая команда в конвейере выполняется в собственной дочерней оболочке. Это означает, что переменные, заданные внутри конвейера, теряются после его завершения.
Это одновременно ошибка корректности и проблема производительности: Вы можете передавать данные в while read, ожидая собрать их, а затем обнаружить, что переменная осталась пустой.
Два решения:
- Используйте подстановку процесса
while read line; do ...; done < <(command)— цикл while выполняется в текущей оболочке, а не в дочерней - Используйте параметр lastpipe (
shopt -s lastpipe) — он заставляет последний сегмент конвейера выполняться в текущей оболочке (bash 4.2+)
#!/usr/bin/env bash
count=0
# --- Bug: count is always 0 after pipe (subshell) ---
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "After pipe : count=$count" # prints 0
# --- Fix 1: process substitution (no subshell for while) ---
count=0
while read -r n; do
(( count++ ))
done < <(seq 1 5)
echo "Process sub : count=$count" # prints 5
# --- Fix 2: lastpipe option ---
shopt -s lastpipe
count=0
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "lastpipe : count=$count" # prints 5Измерение затрат на дочерние оболочки с помощью микротеста
Накладные расходы дочерних оболочек легко показать с помощью небольшого теста производительности. Сравните арифметическую операцию, выполненную через $(( )) (встроенная возможность), с той же операцией, переданной через expr (внешний процесс).
Результаты на типичном компьютере с Linux показывают, что 10 000 вызовов expr занимают около 5 секунд, тогда как такое же количество вызовов $(( )) занимает менее 0,1 секунды — разница в 50 раз при одинаковом результате.
Этот шаблон тестирования также полезен, когда нужно измерить любую оптимизацию: выполните обе версии N раз в цикле и сравните их с помощью time.
#!/usr/bin/env bash
N=500
# External command (fork per call)
time (
x=0
for ((i=0; i<N; i++)); do
x=$(expr $x + 1) # forks expr each time
done
echo "expr result: $x"
)
# Arithmetic builtin (no fork)
time (
x=0
for ((i=0; i<N; i++)); do
(( x++ )) # pure builtin
done
echo "builtin result: $x"
)Использование строк here для отказа от конвейеров с echo
Распространённый шаблон echo "$var" | command передаёт переменную в стандартный ввод. Он создаёт два процесса (echo и command) и конвейер. Строка here (<<<) даёт тот же результат, используя только один процесс: внешняя команда читает данные из временного буфера, управляемого ядром.
grep pattern <<< "$var"— один процесс, без конвейераread -r field1 field2 <<< "$line"— разделение переменной без внешнего инструментаwc -w <<< "$sentence"— подсчёт слов в переменной
Строки here особенно полезны внутри часто выполняемых циклов, где важен каждый созданный процесс.
#!/usr/bin/env bash
data="The quick brown fox"
# --- Fork-heavy: echo spawns a child ---
word_count_slow=$(echo "$data" | wc -w)
echo "Slow word count: $word_count_slow"
# --- Fast: here-string, only wc spawns ---
word_count_fast=$(wc -w <<< "$data")
echo "Fast word count: $word_count_fast"
# --- Even better: use parameter expansion (zero forks) ---
# Split into array, count elements
read -ra words <<< "$data"
echo "Zero-fork count: ${#words[@]}"Практический рефакторинг: до и после
Рассмотрим реалистичный скрипт, который обрабатывает файл журнала, и применим всё изученное. Исходная версия объединяет cat, grep, awk и tr с помощью конвейеров. В переработанной версии число процессов сокращено с 8 до 2.
Основные внесённые изменения:
- Удалён
cat—grepчитает файл напрямую tr '[:lower:]' '[:upper:]'заменён на${var^^}echo "$line" | grep -qзаменён на[[ $line =~ ]]- Использован
read -rс подстановкой процесса вместо цикла while, подключённого через конвейер
После рефакторинга снова выполните time ./script.sh, чтобы подтвердить улучшение. Всегда измеряйте результат, а не делайте предположений.
#!/usr/bin/env bash
# Create a sample log
printf 'ERROR: disk full\nINFO: started\nERROR: timeout\nINFO: done\n' \
> /tmp/sample.log
# === BEFORE (fork-heavy) ===
time (
cat /tmp/sample.log \
| grep 'ERROR' \
| while read -r line; do
label=$(echo "$line" | tr '[:lower:]' '[:upper:]')
echo "[ALERT] $label"
done
)
# === AFTER (builtin-first) ===
time (
while IFS= read -r line; do
echo "[ALERT] ${line^^}"
done < <(grep 'ERROR' /tmp/sample.log)
)Проверка знаний: область видимости подоболочки
Проверьте, насколько хорошо Вы понимаете подоболочки конвейеров и знаете, как избежать потери изменений переменных, внесённых внутри конвейера.
Итоги урока: сначала профилируйте, создавайте меньше процессов
В этом уроке Вы научились выявлять и устранять наиболее распространённые источники ненужного создания процессов в скриптах bash.
Основные выводы:
- Используйте
timeи обогащённыйPS4вариантbash -x, чтобы проводить измерения до оптимизации - Бесполезный cat — самый распространённый антишаблон: передавайте имена файлов непосредственно командам, которые их принимают
- Заменяйте
echo "$var" | commandстрокой здесь (command <<< "$var") или встроенной командой - Подстановки параметров (
${var^^},${var//s/r},${#var}) заменяют множество вызововtr,sedиwc - Подоболочки конвейеров поглощают изменения переменных — используйте подстановку процесса или
shopt -s lastpipe - Переносите вызовы неизменяемых команд за пределы циклов; предпочитайте арифметику
$(( ))вместоexpr
Практическое правило: сначала проводите измерения, по возможности заменяйте внешние команды встроенными и проверяйте улучшение повторным измерением.
Часто задаваемые вопросы
Урок «Профилирование скриптов и отказ от ненужных подоболочек» бесплатный?
Да — полный текст урока «Профилирование скриптов и отказ от ненужных подоболочек» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Linux Command Line & Bash Scripting Mastery, подпишись на CoddyKit PRO. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.
Чему я научусь в уроке «Профилирование скриптов и отказ от ненужных подоболочек»?
Измеряйте время выполнения скриптов и заменяйте ресурсоёмкие цепочки вроде cat и grep встроенными альтернативами Ты практикуешь Linux Command Line & Bash Scripting Mastery с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Linux Command Line & Bash Scripting Mastery?
Предыдущий опыт не требуется. Linux Command Line & Bash Scripting Mastery на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Профилирование скриптов и отказ от ненужных подоболочек»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Linux Command Line & Bash Scripting Mastery?
Да. Каждый урок Linux Command Line & Bash Scripting Mastery включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Профилирование скриптов и отказ от ненужных подоболочек
- Параллелизм с xargs -P и фоновыми заданиями
- Оркестрация рабочих нагрузок с GNU parallel
- Потоковые конвейеры и именованные каналы для пропускной способности