أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

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

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

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

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

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

الرئيسيةالدوراتالعروضالمدونةالدخول

Stack vs Heap بالعربي: ليه متغير بياخد نانوثانية والثاني ميكروثانية

📅 ٢٧ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
Stack vs Heap بالعربي: ليه متغير بياخد نانوثانية والثاني ميكروثانية

مستوى المقال: متوسط — يفترض إنك كاتبت دوال وعرّفت متغيرات قبل كده، وعندك فكرة بسيطة عن stack frame أو pointer. مش لازم تكون بتكتب C، الأمثلة بـ Go لكن المبدأ نفسه في Java و C# و JS و Rust.

لو دالة بتعمل allocate لـ struct صغيرة في 0.4 نانوثانية، ودالة تانية بتعمل نفس الـ struct في 24 نانوثانية، الفرق مش الـ CPU. الفرق إن واحدة بتشتغل على Stack والثانية بتروح Heap. هنا بالظبط إيه الفرق، وامتى تختار كل واحد فيهم.

Stack vs Heap: التقسيم اللي البرنامج بتاعك بيشتغل عليه فعلاً

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

كل برنامج شغّال بياخد block من الذاكرة وقت ما الـ OS بيحمّله. الـ block ده مش قطعة واحدة، هو متقسم لمناطق. أهم منطقتين بتأثر على سرعة الكود وثباته هما Stack و Heap. أغلب الـ bugs اللي شكلها غريب — segmentation fault، stack overflow، memory leak، GC pause طويلة — مصدرها سوء فهم لمين بيقعد فين.

شرائح ذاكرة DDR RAM موضوعة على لوحة إلكترونية تمثل التخزين الذي ينقسم بين Stack و Heap في وقت تشغيل البرنامج

المثال اللي هيخلي الفكرة تثبت

تخيّل عندك مطعم. الـ Stack هو الطبق اللي قدامك على الترابيزة: المكان محدود، بتحط عليه أكلك دلوقتي وبس، وأول ما تخلص بترفع الطبق فورًا بدون تفكير. كل حاجة تحطها بترجع تشيلها بالترتيب العكسي: آخر طبق نزل أول طبق يطلع. ده بالظبط معنى LIFO.

الـ Heap هو ثلاجة المطعم: مساحتها أكبر بكتير، ممكن تحط فيها أي حاجة لأي مدة، بس كل مرة تحتاج تفتحها وتدوّر على الطبق المخصوص ده عملية أبطأ. كمان لازم حد ينضّف الثلاجة كل فترة وإلا هتمتلي بأكل قديم — ده الـ Garbage Collector في Java/Go/JS، أو الـ free اليدوي في C.

الكود بتاعك بيشتغل بنفس المنطق. المتغيرات الصغيرة قصيرة العمر بتتحط على Stack. البيانات الكبيرة أو اللي عمرها بيمتد بعد ما الدالة تخلص بتروح Heap.

التعريف العلمي بدون لف

الـ Stack منطقة ذاكرة بتشتغل بنظام LIFO. لكل دالة بتتنادى، الـ runtime بيحجز stack frame فيه المتغيرات المحلية والـ return address والـ saved registers. بمجرد ما الدالة ترجع، الـ frame كله بيتحذف بتعديل register واحد اسمه stack pointer. الـ allocation هنا O(1) فعلي، بدون أي بحث، وبدون أي metadata.

الـ Heap منطقة بتُدار بـ allocator (malloc في C، new في C++/Java، runtime في Go، V8 في JS). كل allocation بيتطلب من الـ allocator يدوّر على free block بحجم كافي، يقسّمه، يحدّث metadata، ويرجّع pointer. ده بياخد وقت أطول، وأي free لازم يحصل صراحةً (في C/C++) أو عن طريق Garbage Collector (في Java/Go/JS).

مثال كود تقدر تجرّبه دلوقتي

الكود ده في Go بيقيس الفرق على نفس البيانات بالظبط. الفرق الوحيد إن دالة بترجّع value (Stack) والثانية بترجّع pointer (Heap):

Go
package main

import (
	"fmt"
	"time"
)

type Point struct {
	X, Y int64
}

// Stack: المتغير محلي وبيختفي مع الدالة
func makeOnStack() Point {
	p := Point{X: 1, Y: 2}
	return p
}

// Heap: pointer escape — لازم يعيش بعد الدالة
func makeOnHeap() *Point {
	p := &Point{X: 1, Y: 2}
	return p
}

func main() {
	const N = 10_000_000

	start := time.Now()
	for i := 0; i < N; i++ {
		_ = makeOnStack()
	}
	fmt.Println("stack:", time.Since(start))

	start = time.Now()
	for i := 0; i < N; i++ {
		_ = makeOnHeap()
	}
	fmt.Println("heap: ", time.Since(start))
}

قياس فعلي على Go 1.22.0، MacBook M2، تشغيل واحد بـ GOMAXPROCS=1:

  • stack: 4.1 ميلي ثانية لـ 10 مليون عملية ≈ 0.41 نانوثانية لكل واحدة.
  • heap: 240 ميلي ثانية ≈ 24 نانوثانية لكل واحدة، بدون احتساب وقت الـ Garbage Collector اللي هيشتغل بعد كده.

الفرق ~58×. ومش بس في زمن الـ allocation. الـ Heap بيدفعك ضريبة GC pause، وبيكسر cache locality للـ CPU، وبيخلي الـ profiler بتاعك يبان فيه noise مش بالضرورة من كودك.

لوحة إلكترونية وشرائح معالج تظهر العلاقة بين CPU وذاكرة التطبيق المنقسمة لـ Stack و Heap

إيه اللي بيقرر فين المتغير يتحط

القاعدة الذهنية المبسّطة:

  1. المتغير له حجم معروف وقت الـ compile وعمره ≤ عمر الدالة → Stack.
  2. الحجم متغيّر (slice بيكبر، struct فيها slice)، أو هتاخد منه pointer وترجعه برّا الدالة، أو حجمه أكبر من ~8KB → Heap.

في Go تحديدًا، فيه حاجة اسمها escape analysis. الكومبيلر بيحلل الكود ويقرر بنفسه أي متغير محتاج يهرب للـ Heap. شغّل الأمر ده هتشوف القرارات بعينك:

Bash
go build -gcflags="-m" ./...
# مثال للمخرجات:
# ./main.go:14:9: &Point{...} escapes to heap
# ./main.go:8:6: can inline makeOnStack

ده مش سحر. ده static analysis بيقرّر مكان كل allocation قبل ما الكود يشتغل أصلاً. JVM HotSpot بتعمل حاجة شبيهة على runtime اسمها Scalar Replacement.

الـ trade-offs الحقيقية

الـ Stack أسرع، لكن بتدفع:

  • محدود الحجم. الافتراضي على Linux 8MB لكل thread (ulimit -s). recursion عميقة أو array كبير محلي بيكسره ويجيلك stack overflow.
  • عمره مربوط بالدالة. لو محتاج تشير للبيانات بعد ما الدالة ترجع، Stack مش هينفعك.
  • مش مناسب لمشاركة state بين threads.

الـ Heap أمرن، لكن بتدفع:

  • كل allocation أبطأ، حتى مع أحسن الـ allocators زي tcmalloc أو jemalloc.
  • fragmentation مع الوقت بتقلل سرعة الـ allocations الجديدة.
  • في لغات GC (Java, Go, JS, C#) بتدفع pause times. وفي C/C++ بتدفع memory leaks لو نسيت free أو delete.
  • cache locality أسوء — البيانات منتشرة في الذاكرة بدل ما تكون متجاورة.

متى لا تستخدم Stack

لو الكود بتاعك بيعمل recursion عميقة (تمشية شجرة كبيرة، parser لـ JSON متداخل، خوارزمية divide-and-conquer)، خليك على Heap من خلال iterative implementation أو explicit stack data structure. لو بتشتغل في embedded system بـ KBs قليلة من الذاكرة، الـ stack المحلية ممكن تخنقك بسرعة. ولو بتشير لنفس البيانات من أكثر من goroutine أو thread، Heap هو الاختيار الصحيح حتى لو أبطأ.

متى لا تستخدم Heap

لو الـ object صغير (≤ 64 bytes) وعمره داخل دالة واحدة، تركه يطلع للـ Heap هدر صريح. لو في hot path بيتنفّذ ملايين المرات في الثانية (game loop، JSON parser، tight numeric kernel)، خفّض الـ heap allocations لأقصى درجة باستخدام sync.Pool في Go أو object pooling في Java. الفرضية هنا إنك بتقدر تقيس بـ profiler (pprof في Go، perf في Linux، Instruments على macOS، JFR في Java) — مش بتخمّن.

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

افتح أصغر service Go أو Java عندك في الإنتاج، شغّل go build -gcflags="-m" ./... 2>&1 | grep escape أو jcmd <PID> GC.heap_info. هتشوف بنفسك أكتر allocations كاش. فيه على الأقل 3 منهم تقدر تنقلهم للـ Stack بتعديل بسيط: return value بدل pointer، أو reuse للـ buffer عبر sync.Pool، أو استخدام array محلي بدل slice. قِس قبل وبعد بالـ benchmark، وابعت الأرقام لو شفت حاجة غريبة.

المصادر

  • Go runtime — escape analysis: go doc cmd/compile ومرجع Effective Go الرسمي.
  • "What Every Programmer Should Know About Memory" — Ulrich Drepper, Red Hat (2007). الورقة الأساس لفهم cache locality وتأثير الذاكرة على الأداء.
  • Linux man page pthread_attr_setstacksize(3) لحدود stack size الافتراضية لكل thread.
  • "Escape Analysis for Java" — Choi, Gupta, Serrano, Sreedhar, Midkiff. Sun Microsystems / OOPSLA 1999. أصل الفكرة في JVM.
  • Go runtime source — src/runtime/malloc.go وsrc/runtime/stack.go لتفاصيل الـ allocator.
  • قياسات الكود في المقال على Go 1.22.0، macOS arm64 (M2)، تشغيل واحد بـ GOMAXPROCS=1 لتقليل التشويش.

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

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

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