0Pricing
Linux Command Line & Bash Scripting Mastery · Урок

Профилирование скриптов и отказ от ненужных подоболочек

Измеряйте время выполнения скриптов и заменяйте ресурсоёмкие цепочки вроде 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 — локальная установка не требуется.

Все уроки этого курса

  1. Профилирование скриптов и отказ от ненужных подоболочек
  2. Параллелизм с xargs -P и фоновыми заданиями
  3. Оркестрация рабочих нагрузок с GNU parallel
  4. Потоковые конвейеры и именованные каналы для пропускной способности
← Назад к Linux Command Line & Bash Scripting Mastery