Linux-kommandolinjen og Bash-scripting på ekspertniveau · Lektion

Profilering af scripts og undgåelse af overflødige subshells

Mål scriptets køretid, og erstat fork-tunge mønstre som cat-grep-kæder med indbyggede alternativer.

Lektion 1 af 413 trin

Profilering af scripts og undgåelse af overflødige subshells er en gratis Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektion på CoddyKit. Dette er lektion 1 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Linux-kommandolinjen og Bash-scripting på ekspertniveau, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset indeholder 4 lektioner i alt.

Hvorfor scriptets ydeevne er vigtig

Bash-scripts, der kører langsomt, spilder CI-tid, blokerer cron-job og frustrerer brugere. Det meste langsommelighed skyldes ikke kompleks logik — men unødvendige procesforgreninger: hver ekstern kommando, du kalder, starter en ny underordnet proces.

I denne lektion lærer du at:

  • Måle, hvor tiden faktisk bruges, med time og bash -x
  • Identificere mønstre med mange forgreninger, f.eks. unødvendig brug af cat
  • Erstatte eksterne kommandoer med hurtigere indbyggede shell-kommandoer
  • Bruge subshells bevidst og undgå dem, når de ikke tilfører værdi

Målet er at skrive scripts, der udfører det samme arbejde med færre underordnede processer og mindre forløbet tid.

Tidsmåling af et script med den indbyggede kommando time

Det enkleste profileringsværktøj er den indbyggede shell-kommando time. Sæt den foran en vilkårlig kommando eller pipeline for at få tre målinger:

  • real — forløbet tid på væguret (det, du faktisk venter på)
  • user — CPU-tid brugt på kode i brugerrummet
  • sys — CPU-tid brugt i kernen (systemkald, I/O)

En stor forskel mellem real og user+sys betyder normalt, at scriptet venter på I/O eller starter mange underordnede processer. Kør først time omkring hele scriptet for at bekræfte, at der er et problem, før du optimerer noget.

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

Sporing af kørsel med bash -x og PS4

bash -x udskriver hver kommando, før den kører — dette er kørselssporing. Det viser, hvilke linjer der udføres oftest, og om eksterne programmer kaldes flere gange, end du forventede.

Som standard sættes + foran hver sporet linje. Du kan udvide dette præfiks ved hjælp af PS4 og inkludere tidsstempler, så sporingen bliver til en enkel profiler:

  • PS4 udvides før hver sporede kommando
  • Inkludering af $EPOCHREALTIME (bash 5+) eller $(date +%s%N) giver opløsning i nanosekunder
  • Omdirigér standardfejl til en fil, og efterbehandl den for at finde langsomme afsnit
#!/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.

Hvad er en unødvendig subshell?

En subshell er en underordnet kopi af den aktuelle shell-proces. Den oprettes af:

  • Kommandosubstitution: $(command)
  • Gruppering med parenteser: ( commands )
  • Pipeline til en shell-konstruktion: cmd | while read ...

Subshells er nødvendige, når du reelt har brug for isolering eller en pipeline. De bliver unødvendige, når du kun bruger dem til at kalde et eksternt program, som shellen selv kunne håndtere, eller når du uden grund pakker en indbygget kommando ind i et ekstra forgreningslag.

Hver subshell-forgrening koster cirka 1–5 ms på et moderne Linux-system. I en løkke, der kører 10.000 gange, tilføjer 1000 unødvendige subshells 1–5 sekunders ren overhead.

Det klassiske uhensigtsmæssige mønster: unødvendig brug af cat

cat file | grep pattern er det mest berømte uhensigtsmæssige mønster med mange forgreninger. Det starter to processer (cat + grep), der er forbundet med en pipe, selvom grep alene kan læse filen direkte.

Løsningen er enkel: giv filnavnet direkte til den kommando, der forstår filer. Det kaldes inputomdirigering, når værktøjet ikke accepterer filnavne, eller blot at udelade cat, når det gør.

  • Langsom: cat file | grep pattern — 2 processer, 1 pipe
  • Hurtig: grep pattern file — 1 proces, ingen pipe
  • Også hurtig: grep pattern < file — 1 proces, 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.

Erstatning af eksterne kommandoer med indbyggede shell-kommandoer

Mange transformationer på én linje har en indbygget variant, som helt undgår en forgrening. Sammenlign disse almindelige erstatninger:

  • echo ${#var} i stedet for echo "$var" | wc -c — strenglængde
  • ${var^^} og ${var,,} i stedet for echo "$var" | tr 'a-z' 'A-Z' — konvertering af store og små bogstaver (bash 4+)
  • ${var//search/replace} i stedet for echo "$var" | sed 's/search/replace/' — simpel erstatning
  • [[ "$var" =~ pattern ]] i stedet for echo "$var" | grep -q pattern — match af regulært udtryk
  • read -r line < file i stedet for line=$(head -n1 file) — læsning af første linje

Ingen af disse indbyggede kommandoer forgrener en underordnet proces. Besparelsen er lille pr. kald, men bliver markant større inde i løkker.

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

Undgåelse af subshells i løkker

Kommandosubstitution inde i en løkke multiplicerer forgreningsomkostningen med antallet af gentagelser. En løkke, der kører 500 gange med ét $(date)-kald, starter 500 underordnede processer kun for at hente tidsstempler.

Strategier til at reducere overhead i løkker:

  • Flyt uforanderlige kommandoer ud af løkken (beregn én gang, genbrug resultatet)
  • Foretræk aritmetisk udvidelse $(( expr )) — det er indbygget og kræver ingen forgrening
  • Brug printf i stedet for at kalde date, når du kun har brug for formatering
  • Saml eksterne kald i grupper: indsaml data først, og behandl dem én gang uden for løkken
#!/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
)

Pipeline-subshells og faldgruben med variabelomfang

I bash (i modsætning til ksh/zsh) kører hver kommando i en pipeline i sin egen subshell. Det betyder, at variabler, der sættes inde i en pipe, går tabt, når pipen er færdig.

Dette er både en korrekthedsfejl og et ydeevneproblem — du sender måske data til while read i forventning om at indsamle dem, men opdager bagefter, at variablen er tom.

To løsninger:

  • Brug proces-substitution while read line; do ...; done < <(command) — while-løkken kører i den aktuelle shell, ikke i en subshell
  • Brug indstillingen lastpipe (shopt -s lastpipe) — får det sidste pipeline-segment til at køre i den aktuelle shell (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åling af subshell-omkostninger med et mikrobenchmark

Det er nemt at påvise overhead fra subshells med et lille benchmark. Sammenlign en aritmetisk operation udført via $(( )) (indbygget) med den samme operation sendt gennem expr (ekstern proces).

Resultater på en typisk Linux-maskine viser, at 10.000 kald til expr tager cirka 5 sekunder, mens det samme antal kald til $(( )) tager under 0,1 sekund — en forskel på 50 gange for identisk output.

Dette benchmarkmønster er også nyttigt, når du vil måle en optimering: kør begge versioner N gange i en løkke, og sammenlign 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"
)

Brug af here-strings til at undgå echo-pipes

Et almindeligt mønster er echo "$var" | command for at sende en variabel ind via stdin. Det forgrener to processer (echo + command) og opretter en pipe. En here-string (<<<) giver samme resultat med kun én proces — den eksterne kommando læser fra en midlertidig buffer, der administreres af kernen.

  • grep pattern <<< "$var" — én proces, ingen pipe
  • read -r field1 field2 <<< "$line" — opdel en variabel uden et eksternt værktøj
  • wc -w <<< "$sentence" — ordoptælling fra en variabel

Here-strings er især værdifulde i stramme løkker, hvor hver forgrening har betydning.

#!/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 refaktorering: før og efter

Lad os gennemgå et realistisk script, der behandler en logfil, og anvende alt det, vi har lært. Den oprindelige version kæder cat, grep, awk og tr sammen med pipes. Den refaktorerede version reducerer antallet af processer fra 8 til 2.

Vigtige ændringer:

  • Fjernede cat — grep læser filen direkte
  • Erstattede tr '[:lower:]' '[:upper:]' med ${var^^}
  • Erstattede echo "$line" | grep -q med [[ $line =~ ]]
  • Brugte read -r med proces-substitution i stedet for en while-løkke med pipe

Kør time ./script.sh igen efter refaktoreringen for at bekræfte forbedringen. Mål altid — antag ikke.

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

Videnstjek: Omfanget af underskaller

Test din forståelse af pipelines i underskaller, og hvordan du undgår at miste ændringer af variabler, der foretages inde i en pipe.

Opsummering af lektionen: Profilér først, forgren færre processer

I denne lektion har du lært at identificere og eliminere de mest almindelige kilder til unødvendig oprettelse af processer i bash-scripts.

Vigtigste pointer:

  • Brug time og bash -x beriget med PS4 til at måle, før du optimerer
  • Useless cat er det mest udbredte anti-mønster — giv filnavne direkte til kommandoer, der accepterer dem
  • Erstat echo "$var" | command med en here-string (command <<< "$var") eller en indbygget kommando
  • Parameterudvidelser (${var^^}, ${var//s/r}, ${#var}) erstatter mange kald til tr, sed og wc
  • Underskaller i pipelines opsluger ændringer af variabler — brug proces-substitution eller shopt -s lastpipe
  • Flyt uforanderlige kommandokald uden for løkker, og foretræk aritmetik med $(( )) frem for expr

Tommelfingerreglen er: mål først, erstat eksterne kommandoer med indbyggede kommandoer, hvor det er muligt, og bekræft forbedringen med en ny måling.

Gratis at komme i gang

Lær Bash med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
88

Ofte stillede spørgsmål

Er lektionen “Profilering af scripts og undgåelse af overflødige subshells” gratis?

Ja — hele teksten til “Profilering af scripts og undgåelse af overflødige subshells” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset, skal du opgradere til CoddyKit PRO. Linux-kommandolinjen og Bash-scripting på ekspertniveau-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Profilering af scripts og undgåelse af overflødige subshells”?

Mål scriptets køretid, og erstat fork-tunge mønstre som cat-grep-kæder med indbyggede alternativer. Du øver dig i Linux-kommandolinjen og Bash-scripting på ekspertniveau med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Linux-kommandolinjen og Bash-scripting på ekspertniveau?

Der kræves ingen tidligere erfaring. Linux-kommandolinjen og Bash-scripting på ekspertniveau på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 1 af 4.

Hvor lang tid tager lektionen “Profilering af scripts og undgåelse af overflødige subshells”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektion?

Ja. Alle Linux-kommandolinjen og Bash-scripting på ekspertniveau-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Profilering af scripts og undgåelse af overflødige subshells
  2. Parallelisme med xargs -P og baggrundsjob
  3. Orkestrering af arbejdsbelastninger med GNU parallel
  4. Streaming-pipelines og navngivne pipes for høj kapacitet
← Tilbage til Linux-kommandolinjen og Bash-scripting på ekspertniveau