การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น
วัดเวลาทำงานของสคริปต์ และแทนที่รูปแบบที่สร้างกระบวนการจำนวนมาก เช่น การต่อ cat-grep ด้วยทางเลือกในตัว
การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น เป็นบทเรียน Linux Command Line & Bash Scripting Mastery ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 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 ที่ใช้ในเคอร์เนล (การเรียกระบบ, I/O)
ช่องว่างขนาดใหญ่ระหว่าง real กับ user+sys มักหมายความว่าสคริปต์กำลังรอ I/O หรือสร้างกระบวนการลูกจำนวนมาก ให้เรียกใช้ 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)จะให้ความละเอียดระดับนาโนวินาที - เปลี่ยนเส้นทาง stderr ไปยังไฟล์ แล้วประมวลผลภายหลังเพื่อค้นหาส่วนที่ทำงานช้า
#!/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 ครั้ง เชลล์ย่อยที่ไม่จำเป็น 1,000 ครั้งจะเพิ่มเวลาเหนือศีรษะล้วน ๆ อีก 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 กระบวนการ, เปลี่ยนเส้นทาง stdin (ไม่มีบัฟเฟอร์ไปป์)
#!/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 ทั่วไปแสดงให้เห็นว่า การเรียก expr 10,000 ครั้งใช้เวลาประมาณ 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"
)การใช้สตริงตรงเพื่อหลีกเลี่ยงไปป์ของ echo
รูปแบบที่พบบ่อยคือ echo "$var" | command เพื่อส่งตัวแปรเป็น stdin วิธีนี้แยกสองกระบวนการ (echo + command) และสร้างไปป์หนึ่งชุด สตริงตรง (<<<) ให้ผลลัพธ์เดียวกันด้วยกระบวนการเดียว โดยคำสั่งภายนอกจะอ่านข้อมูลจากบัฟเฟอร์ชั่วคราวที่เคอร์เนลจัดการ
grep pattern <<< "$var"— หนึ่งกระบวนการ ไม่มีไปป์read -r field1 field2 <<< "$line"— แยกตัวแปรโดยไม่ใช้เครื่องมือภายนอกwc -w <<< "$sentence"— นับคำจากตัวแปร
สตริงตรงมีประโยชน์เป็นพิเศษภายในลูปที่ทำงานถี่ ซึ่งการแยกกระบวนการทุกครั้งมีผล
#!/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และbash -xที่เพิ่มข้อมูลด้วยPS4เพื่อวัดผลก่อนปรับปรุงประสิทธิภาพ - การใช้ cat อย่างไร้ประโยชน์ เป็นรูปแบบการเขียนที่ควรหลีกเลี่ยงซึ่งพบได้แพร่หลายที่สุด — ส่งชื่อไฟล์ให้คำสั่งที่รองรับชื่อไฟล์โดยตรง
- แทนที่
echo "$var" | commandด้วย here-string (command <<< "$var") หรือคำสั่งในตัว - การขยายพารามิเตอร์ (
${var^^},${var//s/r},${#var}) ช่วยแทนการเรียกใช้tr,sedและwcได้หลายกรณี - เชลล์ย่อยในไปป์ไลน์กลืนการเปลี่ยนแปลงค่าตัวแปร — ให้ใช้การแทนที่โพรเซสหรือ
shopt -s lastpipe - ย้ายการเรียกใช้คำสั่งที่ไม่เปลี่ยนแปลงออกไปนอกลูป และเลือกใช้การคำนวณด้วย
$(( ))แทนexpr
หลักการโดยทั่วไปคือ วัดผลก่อน แทนที่คำสั่งภายนอกด้วยคำสั่งในตัวเมื่อทำได้ และตรวจสอบการปรับปรุงด้วยการวัดผลอีกครั้ง
คำถามที่พบบ่อย
บทเรียน “การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Linux Command Line & Bash Scripting Mastery ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น”
วัดเวลาทำงานของสคริปต์ และแทนที่รูปแบบที่สร้างกระบวนการจำนวนมาก เช่น การต่อ cat-grep ด้วยทางเลือกในตัว คุณปฏิบัติ Linux Command Line & Bash Scripting Mastery ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Linux Command Line & Bash Scripting Mastery หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Linux Command Line & Bash Scripting Mastery บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Linux Command Line & Bash Scripting Mastery นี้ได้ไหม
ได้ บทเรียน Linux Command Line & Bash Scripting Mastery ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การวิเคราะห์ประสิทธิภาพสคริปต์และการหลีกเลี่ยง subshell ที่ไม่จำเป็น
- การทำงานแบบขนานด้วย xargs -P และงานเบื้องหลัง
- การจัดการเวิร์กโหลดด้วย GNU parallel
- ไปป์ไลน์แบบสตรีมและไปป์ที่ตั้งชื่อเพื่อเพิ่มอัตรารับส่งข้อมูล