0Pricing
Linux Command Line & Bash Scripting Mastery · درس

تحليل أداء السكربتات وتجنّب subshells غير الضرورية

قِس زمن تنفيذ السكربت واستبدل الأنماط كثيرة التفريع مثل سلاسل cat وgrep ببدائل مضمّنة

تحليل أداء السكربتات وتجنّب subshells غير الضرورية درس مجاني في Linux Command Line & Bash Scripting Mastery على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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 — وقت وحدة المعالجة المركزية المستهلك في تعليمات مساحة المستخدم
  • sys — وقت وحدة المعالجة المركزية المستهلك في النواة (استدعاءات النظام والإدخال والإخراج)

تعني الفجوة الكبيرة بين real وuser+sys عادةً أن النص البرمجي ينتظر عمليات الإدخال والإخراج أو ينشئ عددًا كبيرًا من العمليات الفرعية. شغّلوا 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 مرة، تضيف 1000 صدفة فرعية غير مفيدة من 1 إلى 5 ثوانٍ من الحمل الزائد الخالص.

النمط المضاد الكلاسيكي: الاستخدام غير المفيد لـ cat

يُعد cat file | grep pattern أشهر نمط مضاد كثير إنشاء العمليات. فهو ينشئ عمليتين (cat وgrep) متصلتين عبر أنبوب، في حين يستطيع grep وحده قراءة الملف مباشرة.

الإصلاح بسيط: مرّر اسم الملف مباشرةً إلى الأمر الذي يفهم الملفات. ويُسمى ذلك إعادة توجيه الإدخال عندما لا تقبل الأداة أسماء الملفات، أو يقتصر ببساطة على حذف cat عندما تقبلها.

  • بطيء: cat file | grep pattern — عمليتان وأنبوب واحد
  • سريع: grep pattern file — عملية واحدة ومن دون أنبوب
  • سريع أيضًا: grep pattern < file — عملية واحدة وإعادة توجيه 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 نموذجي أن 10,000 استدعاء لـ expr تستغرق نحو 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 لإجراء القياس قبل التحسين
  • يُعد Useless cat أكثر الأنماط المضادة انتشارًا — مرّروا أسماء الملفات مباشرةً إلى الأوامر التي تقبلها
  • استبدلوا echo "$var" | command بسلسلة هنا (command <<< "$var") أو بأحد الأوامر المضمّنة
  • تستبدل توسعات المعلمات (${var^^} و${var//s/r} و${#var}) العديد من استدعاءات tr وsed وwc
  • تبتلع الصدفات الفرعية في خطوط الأنابيب تغييرات المتغيرات — استخدموا استبدال العمليات أو shopt -s lastpipe
  • انقلوا استدعاءات الأوامر الثابتة إلى خارج الحلقات، وفضّلوا العمليات الحسابية باستخدام $(( )) على expr

القاعدة العامة: أجروا القياس أولًا، واستبدلوا الأوامر الخارجية بالأوامر المضمّنة حيثما أمكن، وتحققوا من التحسن بإجراء قياس ثانٍ.

الأسئلة الشائعة

هل درس «تحليل أداء السكربتات وتجنّب subshells غير الضرورية» مجاني؟

نعم — نص درس «تحليل أداء السكربتات وتجنّب subshells غير الضرورية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Linux Command Line & Bash Scripting Mastery، انتقل إلى CoddyKit PRO. تتضمن دورة Linux Command Line & Bash Scripting Mastery 4 دروس في المجموع.

ماذا ستتعلم في «تحليل أداء السكربتات وتجنّب subshells غير الضرورية»؟

قِس زمن تنفيذ السكربت واستبدل الأنماط كثيرة التفريع مثل سلاسل cat وgrep ببدائل مضمّنة تتمرن على Linux Command Line & Bash Scripting Mastery مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Linux Command Line & Bash Scripting Mastery؟

لا تُشترط خبرة سابقة. Linux Command Line & Bash Scripting Mastery على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «تحليل أداء السكربتات وتجنّب subshells غير الضرورية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Linux Command Line & Bash Scripting Mastery هذا؟

نعم. كل درس في Linux Command Line & Bash Scripting Mastery يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تحليل أداء السكربتات وتجنّب subshells غير الضرورية
  2. التوازي باستخدام xargs -P والمهام الخلفية
  3. تنسيق أحمال العمل باستخدام GNU parallel
  4. خطوط الأنابيب المتدفقة والأنابيب المسماة لزيادة الإنتاجية
← العودة إلى Linux Command Line & Bash Scripting Mastery