Scripts profileren en overbodige subshells vermijden
Meet de uitvoertijd van scripts en vervang fork-intensieve patronen, zoals cat-grep-ketens, door ingebouwde alternatieven.
Scripts profileren en overbodige subshells vermijden is een gratis DevOps-bootcamp-les op CoddyKit. Dit is les 1 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject DevOps-bootcamp. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus DevOps-bootcamp bevat in totaal 4 lessen.
Waarom scriptprestaties belangrijk zijn
Langzaam uitgevoerde Bash-scripts verspillen CI-tijd, blokkeren cron-taken en frustreren gebruikers. De meeste traagheid ontstaat niet door complexe logica, maar door onnodige procesforks: elke externe opdracht die u aanroept, start een nieuw onderliggend proces.
In deze les leert u:
- Met
timeenbash -xmeten waar de tijd werkelijk naartoe gaat - Fork-intensieve antipatronen herkennen, zoals onnodig gebruik van cat
- Externe opdrachten vervangen door snellere ingebouwde shellopdrachten
- Subshells bewust gebruiken en vermijden wanneer ze geen meerwaarde bieden
Het doel is scripts te schrijven die hetzelfde werk uitvoeren met minder onderliggende processen en minder verstreken tijd.
Een script timen met de ingebouwde opdracht time
Het eenvoudigste hulpmiddel voor profilering is de ingebouwde shellopdracht time. Zet deze voor een willekeurige opdracht of pijplijn om drie metingen te krijgen:
- real — verstreken kloktijd (de tijd die u daadwerkelijk moet wachten)
- user — processortijd die is besteed aan code in de gebruikersruimte
- sys — processortijd die is besteed in de kernel (systeemaanroepen, I/O)
Een groot verschil tussen real en user+sys betekent meestal dat het script op I/O wacht of veel onderliggende processen start. Voer time eerst rond het hele script uit om te bevestigen dat er een probleem is voordat u iets optimaliseert.
#!/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.012sUitvoering volgen met bash -x en PS4
bash -x toont elke opdracht voordat deze wordt uitgevoerd — dit is uitvoeringsregistratie. U ziet welke regels het vaakst worden uitgevoerd en of externe programma's vaker worden aangeroepen dan u verwachtte.
Standaard krijgt elke gevolgde regel het voorvoegsel +. U kunt dit voorvoegsel uitbreiden met PS4 door tijdstempels toe te voegen. Zo wordt het volgen een lichtgewicht profiler:
PS4wordt uitgebreid vóór elke gevolgde opdracht- Door
$EPOCHREALTIME(bash 5+) of$(date +%s%N)op te nemen, krijgt u een resolutie van nanoseconden - Leid stderr om naar een bestand en verwerk dit achteraf om trage gedeelten te vinden
#!/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.Wat is een onnodige subshell?
Een subshell is een onderliggende kopie van het huidige shellproces. Deze wordt gemaakt door:
- Opdrachtvervanging:
$(command) - Groepering met haakjes:
( commands ) - Doorsturen naar een shellconstructie:
cmd | while read ...
Subshells zijn nodig wanneer u echt isolatie of een pijplijn nodig hebt. Ze zijn onnodig wanneer u ze alleen gebruikt om een extern programma aan te roepen dat de shell zelf kan afhandelen, of wanneer u zonder reden een ingebouwde opdracht in een extra forklaag verpakt.
Elke subshell-fork kost ongeveer 1–5 ms op een modern Linux-systeem. In een lus die 10.000 keer wordt uitgevoerd, voegen 1000 onnodige subshells 1–5 seconden pure overhead toe.
Het klassieke antipatroon: onnodig gebruik van cat
cat file | grep pattern is het bekendste fork-intensieve antipatroon. Het start twee processen (cat + grep) die via een pijp zijn verbonden, terwijl grep het bestand rechtstreeks kan lezen.
De oplossing is eenvoudig: geef de bestandsnaam rechtstreeks door aan de opdracht die bestanden begrijpt. Dit heet invoeromleiding wanneer het hulpprogramma geen bestandsnamen accepteert, of simpelweg het weglaten van cat wanneer dat wel het geval is.
- Langzaam:
cat file | grep pattern— 2 processen, 1 pijp - Snel:
grep pattern file— 1 proces, geen pijp - Ook snel:
grep pattern < file— 1 proces, stdin-omleiding (geen pijpbuffer)
#!/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.Externe opdrachten vervangen door ingebouwde shellopdrachten
Veel transformaties in één regel hebben een ingebouwd equivalent waarmee u een fork volledig vermijdt. Vergelijk deze veelgebruikte vervangingen:
echo ${#var}in plaats vanecho "$var" | wc -c— tekenreekslengte${var^^}en${var,,}in plaats vanecho "$var" | tr 'a-z' 'A-Z'— hoofdletterconversie (bash 4+)${var//search/replace}in plaats vanecho "$var" | sed 's/search/replace/'— eenvoudige vervanging[[ "$var" =~ pattern ]]in plaats vanecho "$var" | grep -q pattern— overeenkomst met een reguliere expressieread -r line < filein plaats vanline=$(head -n1 file)— eerste regel lezen
Geen van deze ingebouwde opdrachten fork't een onderliggend proces. De besparing per aanroep is klein, maar loopt binnen lussen sterk op.
#!/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"Subshells in lussen vermijden
Opdrachtvervanging binnen een lus vermenigvuldigt de fork-kosten met het aantal iteraties. Een lus die 500 keer wordt uitgevoerd en één aanroep van $(date) bevat, start alleen voor de tijdstempels al 500 onderliggende processen.
Strategieën om de overhead van lussen te verminderen:
- Verplaats onveranderlijke opdrachten buiten de lus (bereken ze één keer en hergebruik het resultaat)
- Geef de voorkeur aan rekenkundige expansie
$(( expr ))— dit is een ingebouwde functie en geen fork - Gebruik
printfin plaats vandatewanneer alleen opmaak nodig is - Bundel externe aanroepen: verzamel eerst de gegevens en verwerk ze één keer buiten de lus
#!/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 in pijpen en de valkuil van variabelenbereik
In bash (anders dan in ksh/zsh) wordt elke opdracht in een pijplijn in zijn eigen subshell uitgevoerd. Dit betekent dat variabelen die binnen een pijp worden ingesteld verloren gaan nadat de pijp is voltooid.
Dit is zowel een fout in de werking als een prestatieprobleem — u kunt gegevens naar while read doorsturen in de verwachting ze te verzamelen, om daarna te ontdekken dat de variabele leeg is.
Twee oplossingen:
- Gebruik procesvervanging
while read line; do ...; done < <(command)— de while-lus wordt in de huidige shell uitgevoerd, niet in een subshell - Gebruik de optie lastpipe (
shopt -s lastpipe) — hiermee wordt het laatste segment van de pijplijn in de huidige shell uitgevoerd (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 5Subshellkosten meten met een microbenchmark
U kunt de overhead van subshells eenvoudig aantonen met een kleine benchmark. Vergelijk een rekenkundige bewerking via $(( )) (ingebouwd) met dezelfde bewerking die via expr (extern proces) wordt doorgestuurd.
Resultaten op een doorsnee Linux-systeem laten zien dat 10.000 aanroepen van expr ongeveer 5 seconden duren, terwijl hetzelfde aantal aanroepen van $(( )) minder dan 0,1 seconde duurt — een verschil van 50 keer bij identieke uitvoer.
Dit benchmarkpatroon is ook nuttig wanneer u een optimalisatie wilt meten: voer beide versies N keer uit in een lus en vergelijk ze met 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-strings gebruiken om echo-pijpen te vermijden
Een veelgebruikt patroon is echo "$var" | command om een variabele als stdin door te geven. Dit fork't twee processen (echo + command) en maakt een pijp. Een here-string (<<<) bereikt hetzelfde resultaat met slechts één proces — de externe opdracht leest uit een tijdelijke buffer die door de kernel wordt beheerd.
grep pattern <<< "$var"— één proces, geen pijpread -r field1 field2 <<< "$line"— een variabele splitsen zonder extern hulpprogrammawc -w <<< "$sentence"— het aantal woorden in een variabele tellen
Here-strings zijn vooral waardevol in krappe lussen waarin elke fork meetelt.
#!/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[@]}"Praktische herstructurering: voor en na
We bekijken een realistisch script dat een logbestand verwerkt en passen alles toe wat we hebben geleerd. De oorspronkelijke versie koppelt cat, grep, awk en tr met pijpen. De herstructureerde versie verlaagt het aantal processen van 8 naar 2.
Belangrijkste wijzigingen:
catverwijderd —grepleest het bestand rechtstreekstr '[:lower:]' '[:upper:]'vervangen door${var^^}echo "$line" | grep -qvervangen door[[ $line =~ ]]read -rgebruikt met procesvervanging in plaats van een while-lus via een pijp
Voer na de herstructurering opnieuw time ./script.sh uit om de verbetering te bevestigen. Meet altijd; ga niet uit van aannames.
#!/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)
)Kennischeck: bereik van subshells
Test je begrip van subshells in pipelines en van manieren om te voorkomen dat variabelewijzigingen die binnen een pipe zijn aangebracht verloren gaan.
Samenvatting van de les: profileer eerst, fork minder
In deze les heb je geleerd hoe je de meest voorkomende bronnen van onnodige procescreatie in bash-scripts kunt herkennen en elimineren.
Belangrijkste punten:
- Gebruik
timeenbash -xmetPS4-verrijking om eerst te meten voordat je optimaliseert - Overbodige cat is het meest voorkomende antipatroon — geef bestandsnamen rechtstreeks door aan opdrachten die deze accepteren
- Vervang
echo "$var" | commanddoor een here-string (command <<< "$var") of een ingebouwde opdracht - Parameterexpansies (
${var^^},${var//s/r},${#var}) vervangen veel aanroepen vantr,sedenwc - Subshells in pipelines slikken variabelewijzigingen in — gebruik proces-substitutie of
shopt -s lastpipe - Verplaats aanroepen van onveranderlijke opdrachten buiten lussen; geef de voorkeur aan rekenkundige bewerkingen met
$(( ))bovenexpr
De vuistregel: meet eerst, vervang externe opdrachten waar mogelijk door ingebouwde opdrachten en controleer de verbetering met een tweede meting.
Leer DevOps-bootcamp met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 142
- Lessen
- 568
Veelgestelde vragen
Is de les “Scripts profileren en overbodige subshells vermijden” gratis?
Ja — je kunt hier op het web alle 3 lessen van het leerpad DevOps-bootcamp, waaronder “Scripts profileren en overbodige subshells vermijden”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus DevOps-bootcamp bevat in totaal 4 lessen.
Wat leer ik in “Scripts profileren en overbodige subshells vermijden”?
Meet de uitvoertijd van scripts en vervang fork-intensieve patronen, zoals cat-grep-ketens, door ingebouwde alternatieven. Je oefent met DevOps-bootcamp door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met DevOps-bootcamp te beginnen?
Ervaring vooraf is niet nodig. DevOps-bootcamp op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 1 van 4.
Hoe lang duurt de les “Scripts profileren en overbodige subshells vermijden”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over DevOps-bootcamp?
Ja. Elke les over DevOps-bootcamp bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Scripts profileren en overbodige subshells vermijden
- Parallelisme met xargs -P en achtergrondtaken
- Workloads orkestreren met GNU parallel
- Streaming-pipelines en named pipes voor doorvoer