Bemästra Linux-kommandoraden och Bash-skriptning · Lektion

Profilera skript och undvik onödiga subshells

Mät skriptets körtid och ersätt gaffelintensiva mönster, som kedjor med cat och grep, med inbyggda alternativ.

Lektion 1 av 413 steg

Profilera skript och undvik onödiga subshells är en gratis lektion i Bemästra Linux-kommandoraden och Bash-skriptning på CoddyKit. Detta är lektion 1 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Bemästra Linux-kommandoraden och Bash-skriptning, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bemästra Linux-kommandoraden och Bash-skriptning innehåller totalt 4 lektioner.

Varför skriptprestanda är viktig

Bash-skript som körs långsamt slösar CI-tid, blockerar cron-jobb och frustrerar användare. Det mesta av långsamheten beror inte på komplex logik – den beror på onödiga processavknoppningar: varje externt kommando du anropar startar en ny barnprocess.

I den här lektionen lär du dig att:

  • Mäta vart tiden faktiskt tar vägen med time och bash -x
  • Identifiera mönster som skapar många avknoppningar, till exempel onödig användning av cat
  • Ersätta externa kommandon med snabbare inbyggda skal-kommandon
  • Använda subshells medvetet och undvika dem när de inte tillför något

Målet är att skriva skript som utför samma arbete med färre barnprocesser och kortare väggklocktid.

Tidsmätning av ett skript med det inbyggda kommandot time

Det enklaste profileringsverktyget är det inbyggda skal-kommandot time. Sätt det före valfritt kommando eller pipeline för att få tre mätvärden:

  • real – förfluten väggklocktid (det du faktiskt väntar på)
  • user – CPU-tid som används av kod i användarrymden
  • sys – CPU-tid som används i kärnan (systemanrop, I/O)

En stor skillnad mellan real och user+sys betyder vanligtvis att skriptet väntar på I/O eller startar många barnprocesser. Kör time runt hela skriptet först för att bekräfta att det finns ett problem innan du optimerar något.

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

Spåra körningen med bash -x och PS4

bash -x skriver ut varje kommando innan det körs – detta kallas körningsspårning. Det visar vilka rader som körs oftast och om externa program anropas fler gånger än du förväntade dig.

Som standard inleds varje spårad rad med +. Du kan utöka prefixet med PS4 och lägga till tidsstämplar, vilket förvandlar spårningen till en enkel profilerare:

  • PS4 expanderas före varje spårat kommando
  • Om du inkluderar $EPOCHREALTIME (bash 5+) eller $(date +%s%N) får du upplösning på nanosekundnivå
  • Omdirigera standardfel till en fil och efterbearbeta den för att hitta långsamma avsnitt
#!/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.

Vad är ett onödigt subshell?

Ett subshell är en underordnad kopia av den aktuella skalprocessen. Det skapas av:

  • Kommandosubstitution: $(command)
  • Gruppering med parenteser: ( commands )
  • Rörledning till en skal-konstruktion: cmd | while read ...

Subshells är nödvändiga när du verkligen behöver isolering eller en pipeline. De blir onödiga när du endast använder dem för att anropa ett externt program som skalet självt kunde hantera, eller när du utan anledning lägger ett inbyggt kommando i ytterligare ett lager av avknoppning.

Varje avknoppning av ett subshell kostar cirka 1–5 ms på ett modernt Linux-system. I en loop som körs 10 000 gånger lägger 1 000 onödiga subshells till 1–5 sekunder ren overhead.

Det klassiska antimönstret: onödig användning av cat

cat file | grep pattern är det mest kända antimönstret som skapar onödiga avknoppningar. Det startar två processer (cat + grep) som kopplas samman med en pipe, trots att grep kan läsa filen direkt på egen hand.

Lösningen är enkel: skicka filnamnet direkt till kommandot som kan hantera filer. När verktyget inte accepterar filnamn kallas detta indataomdirigering; annars utelämnar du helt enkelt cat.

  • Långsamt: cat file | grep pattern – 2 processer, 1 pipe
  • Snabbt: grep pattern file – 1 process, ingen pipe
  • Också snabbt: grep pattern < file – 1 process, stdin-omdirigering (ingen pipe-buffer)
#!/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.

Ersätta externa kommandon med inbyggda skal-kommandon

Många transformationer som normalt skrivs på en rad har en inbyggd motsvarighet som helt undviker en avknoppning. Jämför dessa vanliga ersättningar:

  • echo ${#var} i stället för echo "$var" | wc -c – stränglängd
  • ${var^^} och ${var,,} i stället för echo "$var" | tr 'a-z' 'A-Z' – ändra skiftläge (bash 4+)
  • ${var//search/replace} i stället för echo "$var" | sed 's/search/replace/' – enkel ersättning
  • [[ "$var" =~ pattern ]] i stället för echo "$var" | grep -q pattern – matchning med reguljära uttryck
  • read -r line < file i stället för line=$(head -n1 file) – läsa den första raden

Inget av dessa inbyggda kommandon startar en barnprocess. Besparingen är liten per anrop men blir mycket stor inuti loopar.

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

Undvika subshells i loopar

Kommandosubstitution i en loop multiplicerar avknoppningskostnaden med antalet iterationer. En loop som körs 500 gånger och innehåller ett anrop till $(date) startar 500 barnprocesser enbart för tidsstämplar.

Strategier för att minska overhead i loopar:

  • Flytta oföränderliga kommandon utanför loopen (beräkna en gång och återanvänd)
  • Föredra aritmetisk expansion $(( expr )) – det är ett inbyggt kommando, inte en avknoppning
  • Använd printf i stället för att anropa date när du bara behöver formatering
  • Samla externa anrop i grupper: samla först in data och bearbeta den en gång utanför loopen
#!/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 i pipes och fallgropen med variabelomfattning

I bash (till skillnad från ksh/zsh) körs varje kommando i en pipeline i sitt eget subshell. Det innebär att variabler som sätts inuti en pipe försvinner när pipen är klar.

Detta är både ett korrekthetsfel och ett prestandaproblem – du kanske skickar data till while read i förväntan att samla in den, men upptäcker sedan att variabeln är tom.

Två lösningar:

  • Använd processubstitution while read line; do ...; done < <(command) – while-loopen körs i det aktuella skalet, inte i ett subshell
  • Använd alternativet lastpipe (shopt -s lastpipe) – gör att det sista pipeline-segmentet körs i det aktuella skalet (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

Mäta kostnaden för subshells med ett mikrobenchmark

Det är enkelt att visa overheaden för subshells med ett litet benchmark. Jämför en aritmetisk operation som utförs via $(( )) (inbyggt kommando) med samma operation som skickas genom expr (externt process).

Resultat på en typisk Linux-dator visar att 10 000 anrop till expr tar cirka 5 sekunder, medan samma antal anrop med $(( )) tar under 0,1 sekunder – en 50 gånger större skillnad för identiskt resultat.

Det här benchmarkmönstret är också användbart när du vill mäta en optimering: kör båda versionerna N gånger i en loop och jämför med 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"
)

Använda here-strings för att undvika echo-pipes

Ett vanligt mönster är echo "$var" | command för att skicka en variabel till stdin. Det skapar två processer (echo + command) och en pipe. En here-string (<<<) ger samma resultat med endast en process – det externa kommandot läser från en temporär buffer som hanteras av kärnan.

  • grep pattern <<< "$var" – en process, ingen pipe
  • read -r field1 field2 <<< "$line" – dela upp en variabel utan ett externt verktyg
  • wc -w <<< "$sentence" – räkna ord i en variabel

Here-strings är särskilt värdefulla i täta loopar där varje avknoppning spelar roll.

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

Praktisk omstrukturering: före och efter

Vi går igenom ett realistiskt skript som bearbetar en loggfil och tillämpar allt vi har lärt oss. Originalversionen kedjar ihop cat, grep, awk och tr med pipes. Den omstrukturerade versionen minskar antalet processer från 8 till 2.

Viktiga ändringar:

  • Tog bort cat – grep läser filen direkt
  • Ersatte tr '[:lower:]' '[:upper:]' med ${var^^}
  • Ersatte echo "$line" | grep -q med [[ $line =~ ]]
  • Använde read -r med processubstitution i stället för en while-loop med pipe

Kör time ./script.sh igen efter omstruktureringen för att bekräfta förbättringen. Mät alltid – anta inte.

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

Kunskapskontroll: Omfång för subshell

Testa er förståelse av subshells i pipelines och hur ni undviker att förlora variabeländringar som görs inuti en pipe.

Lektionssammanfattning: Profilera först, forka mindre

I den här lektionen lärde ni er att identifiera och eliminera de vanligaste källorna till onödig processkapning i bash-skript.

Viktiga lärdomar:

  • Använd time och bash -x berikat med PS4 för att mäta innan ni optimerar
  • Onödig cat är det vanligaste antipatternet – skicka filnamn direkt till kommandon som accepterar dem
  • Ersätt echo "$var" | command med en here-string (command <<< "$var") eller ett builtin-kommando
  • Parameterexpansioner (${var^^}, ${var//s/r}, ${#var}) ersätter många anrop till tr, sed och wc
  • Subshells i pipelines slukar variabeländringar – använd process substitution eller shopt -s lastpipe
  • Flytta invarianta kommandoanrop utanför loopar och föredra aritmetik med $(( )) framför expr

Tumregeln är: mät först, ersätt externa kommandon med builtin-kommandon när det är möjligt och verifiera förbättringen med en andra mätning.

Gratis att börja

Lär dig Bash med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
88

Vanliga frågor

Är lektionen ”Profilera skript och undvik onödiga subshells” gratis?

Ja – hela texten till ”Profilera skript och undvik onödiga subshells” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Bemästra Linux-kommandoraden och Bash-skriptning, kan Ni uppgradera till CoddyKit PRO. Kursen i Bemästra Linux-kommandoraden och Bash-skriptning innehåller totalt 4 lektioner.

Vad lär jag mig i ”Profilera skript och undvik onödiga subshells”?

Mät skriptets körtid och ersätt gaffelintensiva mönster, som kedjor med cat och grep, med inbyggda alternativ. Ni övar på Bemästra Linux-kommandoraden och Bash-skriptning med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bemästra Linux-kommandoraden och Bash-skriptning?

Du behöver inga förkunskaper. Utbildningen i Bemästra Linux-kommandoraden och Bash-skriptning på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 1 av 4.

Hur lång tid tar lektionen ”Profilera skript och undvik onödiga subshells”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bemästra Linux-kommandoraden och Bash-skriptning-lektionen?

Ja. Varje Bemästra Linux-kommandoraden och Bash-skriptning-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Profilera skript och undvik onödiga subshells
  2. Parallellism med xargs -P och bakgrundsjobb
  3. Orkestrera arbetsbelastningar med GNU parallel
  4. Strömmande pipelines och namngivna pipes för genomströmning
← Tillbaka till Bemästra Linux-kommandoraden och Bash-skriptning