أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالمناهج والباقات
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

Escape Analysis في Go للمحترف: ليه المتغير بيهرب للـ Heap ويبطّئ كودك

محترف١٥ يونيو ٢٠٢٦5 دقائق قراءة
Escape Analysis في Go للمحترف: ليه المتغير بيهرب للـ Heap ويبطّئ كودك

Escape Analysis في Go: ليه متغير محلي بيهرب من الـ Stack للـ Heap

المستوى: محترف. المقال ده بيفترض إنك بتكتب Go فعلًا، عارف الفرق بين القيمة والمؤشر، وقريت output benchmark قبل كده. لو لسه مبتدئ، اقرأ المثال التشبيهي في الأول وكمّل عادي.

لو خدمة Go عندك بتعمل allocations أكتر من المتوقع والـ Garbage Collector بياخد نسبة CPU محسوسة، السبب الأكثر شيوعًا مش الكود الواضح. السبب إن متغيرات محلية بتهرب للـ Heap من غير ما تقصد. هتتعلم هنا تكشف الهروب بأمر واحد، وتقيس تكلفته بالأرقام، وتمنعه في الحالات اللي تستاهل.

المشكلة باختصار

في Go إنت مش بتقرر المتغير يتخزن فين. الكومبايلر هو اللي بيقرر: على الـ Stack ولا على الـ Heap. القرار ده اسمه Escape Analysis. لو الكومبايلر قرر إن متغير ممكن «يعيش» بعد انتهاء الدالة اللي عملته، بيحطه على الـ Heap. والـ Heap معناه شغل زيادة للـ GC، يعني CPU وlatency. المشكلة إنك مش شايف القرار ده في الكود، فبتدفع تمنه من غير ما تعرف.

لوحة دوائر إلكترونية ورقائق ذاكرة ترمز لتخصيص المتغيرات بين الـ Stack والـ Heap في لغة Go

المفهوم الأول بمثال بسيط

تخيّل مكتبك في الشغل. الورقة اللي بتكتب عليها حسبة سريعة وترميها لما تخلص المهمة دي هي الـ Stack: مكانها معروف، وبتتفضّى تلقائيًا أول ما تقوم من المكتب. بسيطة ورخيصة.

دلوقتي تخيّل ورقة لازم زميلك ياخدها بعد ما تمشي. مينفعش ترميها مع مكتبك، فبتحطها في مخزن مشترك في الشركة. المخزن ده هو الـ Heap. مرن، بيستحمل أي حاجة تعيش طويل، بس محتاج حارس (الـ GC) يلف عليه كل فترة يشيل اللي محدش محتاجه. الحارس ده شغله مش ببلاش.

السؤال اللي Escape Analysis بيجاوب عليه: «هل في حد هيحتاج الورقة دي بعد ما الموظف يمشي؟» لو آه، الورقة بتهرب (escapes) للمخزن.

التعريف الدقيق

Escape Analysis هو تحليل بيعمله الكومبايلر وقت الـ build، بيتتبّع كل قيمة ويحدد هل عمرها الافتراضي محصور جوه الدالة (lifetime محلي) ولا ممكن يتجاوزها (escapes). لو الـ lifetime محلي، القيمة بتتحط على الـ Stack frame بتاع الدالة، وبتختفي تلقائيًا عند الـ return بدون أي تدخّل من الـ GC. لو القيمة ممكن يتم الوصول لها بعد انتهاء الدالة، الكومبايلر مجبور يحطها على الـ Heap. توثيق Go الرسمي بيقول الكلام ده صراحةً: «لو الكومبايلر مقدرش يثبت إن المتغير مش بيتم الرجوع له بعد انتهاء الدالة، بيخصصه على الـ Heap».

ازاي تشوف القرار بنفسك

الكومبايلر بيطبع قراراته لو طلبت منه بـ -gcflags="-m". خد المثال ده:

Go
package main

// مش بيهرب: القيمة بترجع by value، عمرها بينتهي مع الدالة
func sumLocal() int {
    x := 42
    return x
}

// بيهرب: بنرجّع مؤشر لمتغير محلي، فلازم يعيش بعد الدالة
func newCounter() *int {
    c := 0
    return &c
}

func main() {
    _ = sumLocal()
    _ = newCounter()
}

شغّل التحليل:

Bash
$ go build -gcflags="-m" main.go
# command-line-arguments
./main.go:11:2: moved to heap: c
./main.go:6:6: can inline sumLocal
./main.go:11:6: can inline newCounter

السطر moved to heap: c هو الدليل. المتغير c هرب لأننا رجّعنا مؤشره. أما x في sumLocal فما ظهرش في القائمة، يعني فضل على الـ Stack. لو عايز سبب القرار بالتفصيل، استخدم -gcflags="-m -m".

شاشة كود ومخططات أداء تمثل قياس تخصيص الذاكرة في Go باستخدام go test benchmem و pprof

قِس الفرق بالأرقام

الكلام النظري مش كفاية. خلّيك تقيس بـ -benchmem:

Bash
$ go test -bench=. -benchmem
BenchmarkStack-8    1000000000    1.20 ns/op    0 B/op    0 allocs/op
BenchmarkHeap-8       48000000   25.1 ns/op    16 B/op    1 allocs/op

المسار اللي على الـ Stack: 1.2 نانوثانية، وصفر allocations. المسار اللي بيهرب للـ Heap: ~25 نانوثانية و1 allocation لكل عملية. الفرق هنا حوالي 20 ضعف في الزمن، غير شغل الـ GC اللي بيتراكم بعدين. الأرقام دي تقريبية وبتختلف حسب الـ CPU وحجم القيمة، بس الترتيب (order of magnitude) ثابت: الـ Heap أغلى بمراحل في المسارات الساخنة (hot paths).

4 أسباب شائعة بتخلّي المتغير يهرب

  1. إرجاع مؤشر لمتغير محلي — زي return &c فوق. الكلاسيكية.
  2. الـ interfaces والـ boxing — لمّا تمرر قيمة لـ interface{} (زي fmt.Println(x))، القيمة غالبًا بتهرب لأن الكومبايلر مش عارف نوعها وقت الترجمة.
  3. الـ closures اللي بتمسك متغير بالمرجع — لو دالة جوّانية بتعدّل متغير من برّه، المتغير ده بيعيش مع الـ closure وبيهرب.
  4. slices/maps بحجم مش معروف وقت الترجمة — لو الكومبايلر مش قادر يحسب الحجم، بيحط الـ backing array على الـ Heap.

الـ trade-offs — متبالغش

قبل ما تروح تطارد كل allocation، خد بالك من التكلفة:

  • بتكسب: سرعة في الـ hot path وضغط أقل على الـ GC. بتخسر: أحيانًا قراءة الكود، لمّا تتجنب المؤشرات بشكل مصطنع.
  • تجنّب الهروب بنسخ قيم كبيرة (structs ضخمة by value) ممكن يطلّع الـ Stack frame كبير ويعمل بطء أو حتى مشاكل، فمش كل «منع هروب» مكسب.
  • الافتراض هنا إنك بتشتغل على مسار بيتنفّذ ملايين المرات/الثانية. لو الدالة بتتنادى 10 مرات في الطلب، الفرق بين 1ns و25ns مش هيبان أصلًا.

متى لا تستخدم هذه التحسينات

متبدأش تحسّن الـ allocations في كود مش مثبت إنه bottleneck. اعمل profiling الأول بـ pprof (go test -memprofile). لو الـ allocations مش ظاهرة في أعلى الـ profile، سيبها. الوضوح أهم من نانوثانيات في 95% من الكود. كمان في الكود اللي بيتنفّذ مرة واحدة (startup, config loading) الهروب ملوش أي تأثير عملي.

المصادر

  • Go FAQ — Stack or heap allocation (التوثيق الرسمي لقرار التخصيص)
  • cmd/compile documentation (شرح أعلام الكومبايلر منها -m)
  • Go Optimization Guide — Stack Allocations and Escape Analysis
  • Understanding Allocations in Go: stack, heap, allocs/op (Pairs Engineering)
  • Dave Cheney — Five things that make Go fast (escape analysis)

الخطوة التالية

افتح أكتر دالة بتتنفّذ في الـ request path عندك، وشغّل عليها go build -gcflags="-m" ./... وفلتر على كلمة escapes أو moved to heap. لو لقيت هروب في loop بيلف ملايين المرات، حاول ترجع by value بدل المؤشر وأعد قياس الـ benchmark. لو الـ allocs/op نزلت لصفر والزمن قلّ، يبقى مسكت waste حقيقي.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة