0Pricing
DevOps Bootcamp · Aula

Criação de perfis de desempenho e prevenção de subshells inúteis

Meça o tempo dos scripts e substitua padrões que criam muitos processos, como cadeias de cat e grep, por alternativas integradas.

Criação de perfis de desempenho e prevenção de subshells inúteis é uma aula grátis de DevOps Bootcamp no CoddyKit. Esta é a aula 1 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DevOps Bootcamp, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DevOps Bootcamp inclui 4 aulas no total.

Por que o desempenho dos scripts é importante

Scripts Bash lentos desperdiçam tempo de CI, bloqueiam tarefas do cron e frustram os usuários. A maior parte da lentidão não vem de uma lógica complexa — vem de criações desnecessárias de processos: cada comando externo chamado inicia um novo processo filho.

Nesta lição, você aprenderá a:

  • Medir onde o tempo é realmente gasto com time e bash -x
  • Identificar padrões problemáticos que criam muitos processos, como o uso inútil de cat
  • Substituir comandos externos por comandos internos do shell mais rápidos
  • Usar subshells intencionalmente e evitá-los quando não agregarem valor

O objetivo é escrever scripts que façam o mesmo trabalho com menos processos filhos e menos tempo de relógio.

Cronometragem de um script com o comando interno time

A ferramenta de criação de perfis mais simples é o comando interno do shell time. Coloque-o antes de qualquer comando ou canalização para obter três medições:

  • real — tempo decorrido no relógio (o tempo que você realmente espera)
  • user — tempo de CPU gasto no código do espaço do usuário
  • sys — tempo de CPU gasto no núcleo (chamadas do sistema, E/S)

Uma grande diferença entre real e user+sys geralmente significa que o script está esperando por E/S ou iniciando muitos processos filhos. Primeiro, execute time em torno do script inteiro para confirmar que há um problema antes de otimizar qualquer coisa.

#!/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

Rastreamento da execução com bash -x e PS4

bash -x exibe cada comando antes de executá-lo — isso é o rastreamento da execução. Ele mostra quais linhas são executadas com mais frequência e se programas externos estão sendo chamados mais vezes do que o esperado.

Por padrão, cada linha rastreada recebe o prefixo +. Você pode enriquecer o prefixo usando PS4 para incluir marcas de tempo, transformando o rastreamento em um criador de perfis leve:

  • PS4 é expandido antes de cada comando rastreado
  • A inclusão de $EPOCHREALTIME (bash 5+) ou $(date +%s%N) fornece resolução de nanossegundos
  • Redirecione a saída de erro para um arquivo e faça um pós-processamento para encontrar as seções lentas
#!/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.

O que é um subshell inútil

Um subshell é uma cópia filha do processo do shell atual. Ele é criado por:

  • Substituição de comando: $(command)
  • Agrupamento com parênteses: ( commands )
  • Canalização para uma construção do shell: cmd | while read ...

Subshells são necessários quando você realmente precisa de isolamento ou de uma canalização. Eles se tornam inúteis quando são usados apenas para chamar um programa externo que o próprio shell poderia executar, ou quando você envolve um comando interno em uma camada adicional de criação de processos sem motivo.

Cada criação de um subshell custa cerca de 1–5 ms em um sistema Linux moderno. Em um laço executado 10.000 vezes, 1000 subshells inúteis adicionam de 1 a 5 segundos de sobrecarga pura.

O padrão problemático clássico: uso inútil de cat

cat file | grep pattern é o padrão problemático mais famoso por criar muitos processos. Ele inicia dois processos (cat + grep) conectados por uma canalização, quando apenas grep pode ler o arquivo diretamente.

A correção é simples: passe o nome do arquivo diretamente ao comando que entende arquivos. Isso é chamado de redirecionamento de entrada quando a ferramenta não aceita nomes de arquivos, ou simplesmente de omitir cat quando aceita.

  • Lento: cat file | grep pattern — 2 processos, 1 canalização
  • Rápido: grep pattern file — 1 processo, sem canalização
  • Também rápido: grep pattern < file — 1 processo, redirecionamento da entrada padrão (sem buffer de canalização)
#!/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.

Substituição de comandos externos por comandos internos do shell

Muitas transformações de uma linha têm um equivalente interno que evita completamente a criação de um processo. Compare estas substituições comuns:

  • echo ${#var} em vez de echo "$var" | wc -c — comprimento da cadeia de caracteres
  • ${var^^} e ${var,,} em vez de echo "$var" | tr 'a-z' 'A-Z' — conversão de maiúsculas e minúsculas (bash 4+)
  • ${var//search/replace} em vez de echo "$var" | sed 's/search/replace/' — substituição simples
  • [[ "$var" =~ pattern ]] em vez de echo "$var" | grep -q pattern — correspondência de expressão regular
  • read -r line < file em vez de line=$(head -n1 file) — leitura da primeira linha

Nenhum desses comandos internos cria um processo filho. A economia é pequena por chamada, mas se acumula drasticamente dentro de laços.

#!/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"

Como evitar subshells dentro de laços

A substituição de comando dentro de um laço multiplica o custo de criação de processos pelo número de iterações. Um laço executado 500 vezes com uma chamada a $(date) cria 500 processos filhos apenas para obter marcas de tempo.

Estratégias para reduzir a sobrecarga dos laços:

  • Mova os comandos invariáveis para fora do laço (calcule uma vez e reutilize)
  • Prefira a expansão aritmética $(( expr )) — ela é um comando interno, não uma criação de processo
  • Use printf em vez de chamar date quando apenas a formatação for necessária
  • Agrupe chamadas externas: colete os dados primeiro e processe-os uma vez fora do laço
#!/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
)

Subshells de canalização e a armadilha do escopo de variáveis

No bash (diferentemente de ksh/zsh), cada comando em uma canalização é executado em seu próprio subshell. Isso significa que as variáveis definidas dentro de uma canalização são perdidas quando a canalização termina.

Isso é tanto um erro de correção quanto um problema de desempenho — você pode canalizar para while read esperando coletar dados e só descobrir depois que a variável está vazia.

Duas soluções:

  • Use a substituição de processo while read line; do ...; done < <(command) — o laço while é executado no shell atual, não em um subshell
  • Use a opção lastpipe (shopt -s lastpipe) — faz com que o último segmento da canalização seja executado no shell atual (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

Medição do custo de subshells com um microbenchmark

É fácil demonstrar a sobrecarga dos subshells com um pequeno teste de desempenho. Compare uma operação aritmética feita por meio de $(( )) (comando interno) com a mesma operação canalizada por meio de expr (processo externo).

Os resultados em um computador Linux típico mostram que 10.000 chamadas a expr levam cerca de 5 segundos, enquanto a mesma quantidade de chamadas a $(( )) leva menos de 0,1 segundo — uma diferença de 50 vezes para uma saída idêntica.

Esse padrão de teste também é útil quando você quer medir qualquer otimização: execute ambas as versões N vezes em um laço e compare com 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"
)

Uso de cadeias here para evitar canalizações com echo

Um padrão comum é echo "$var" | command para fornecer uma variável como entrada padrão. Isso cria dois processos (echo + command) e uma canalização. Uma cadeia here (<<<) obtém o mesmo resultado com apenas um processo — o comando externo lê de um buffer temporário gerenciado pelo núcleo.

  • grep pattern <<< "$var" — um processo, sem canalização
  • read -r field1 field2 <<< "$line" — divide uma variável sem ferramenta externa
  • wc -w <<< "$sentence" — conta as palavras de uma variável

Cadeias here são especialmente úteis dentro de laços intensivos, nos quais cada criação de processo é relevante.

#!/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[@]}"

Refatoração prática: antes e depois

Vamos analisar um script realista que processa um arquivo de registro e aplicar tudo o que aprendemos. A versão original encadeia cat, grep, awk e tr por meio de canalizações. A versão refatorada reduz a quantidade de processos de 8 para 2.

Principais alterações:

  • Remoção de cat — grep lê o arquivo diretamente
  • Substituição de tr '[:lower:]' '[:upper:]' por ${var^^}
  • Substituição de echo "$line" | grep -q por [[ $line =~ ]]
  • Uso de read -r com substituição de processo em vez de um laço while canalizado

Após a refatoração, execute time ./script.sh novamente para confirmar a melhoria. Sempre faça medições — não presuma.

#!/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)
)

Verificação de conhecimentos: escopo de subshells

Teste sua compreensão sobre subshells de canalizações e sobre como evitar a perda de alterações em variáveis feitas dentro de um pipe.

Recapitulação da lição: faça o perfil primeiro, crie menos processos

Nesta lição, você aprendeu a identificar e eliminar as fontes mais comuns de criação desnecessária de processos em scripts Bash.

Principais aprendizados:

  • Use time e bash -x enriquecido com PS4 para medir antes de otimizar
  • cat inútil é o antipadrão mais difundido — passe os nomes dos arquivos diretamente aos comandos que os aceitam
  • Substitua echo "$var" | command por uma string aqui (command <<< "$var") ou por um comando interno
  • Expansões de parâmetros (${var^^}, ${var//s/r}, ${#var}) substituem muitas chamadas a tr, sed e wc
  • Subshells de canalizações absorvem as mutações de variáveis — use substituição de processos ou shopt -s lastpipe
  • Mova as chamadas de comandos invariantes para fora dos loops; prefira a aritmética $(( )) em vez de expr

A regra prática é: meça primeiro, substitua comandos externos por comandos internos sempre que possível e confirme a melhoria com uma segunda medição.

Perguntas Frequentes

A aula “Criação de perfis de desempenho e prevenção de subshells inúteis” é grátis?

Sim — o texto completo de “Criação de perfis de desempenho e prevenção de subshells inúteis” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DevOps Bootcamp, atualize para CoddyKit PRO. O curso de DevOps Bootcamp inclui 4 aulas no total.

O que vou aprender em “Criação de perfis de desempenho e prevenção de subshells inúteis”?

Meça o tempo dos scripts e substitua padrões que criam muitos processos, como cadeias de cat e grep, por alternativas integradas. Você pratica DevOps Bootcamp com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DevOps Bootcamp?

Nenhuma experiência prévia é necessária. DevOps Bootcamp no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 1 de 4.

Quanto tempo leva a aula “Criação de perfis de desempenho e prevenção de subshells inúteis”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DevOps Bootcamp?

Sim. Cada aula de DevOps Bootcamp inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Criação de perfis de desempenho e prevenção de subshells inúteis
  2. Paralelismo com xargs -P e tarefas em segundo plano
  3. Orquestração de cargas de trabalho com GNU parallel
  4. Pipelines de transmissão e pipes nomeados para maior vazão
← Voltar para DevOps Bootcamp