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

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

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

المنصة

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

الدعم

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

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

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

False Sharing: ليه توزيع الشغل على 8 أنوية يخلّي كودك أبطأ

محترف20 يوليو 20265 دقائق قراءة
False Sharing: ليه توزيع الشغل على 8 أنوية يخلّي كودك أبطأ

المستوى المطلوب: محترف. الشرح يفترض إنك مرتاح مع الـ concurrency وعندك فكرة عن الذاكرة والـ CPU cache. لو المصطلحات دي جديدة عليك، فيه مثال مبسّط تحت قبل الجزء العلمي.

تقدر تكسب سرعة قرب 7 أضعاف في كود متعدد الخيوط من غير ما تغيّر سطر واحد في المنطق. الحيلة إنك تفصل عدّاداتك على cache lines مختلفة. المشكلة اللي بتقتل الأداء اسمها false sharing، وأغلب المطورين مش واخدين بالهم منها.

الـ False Sharing: ليه توزيع الشغل على 8 أنوية يخلّي كودك أبطأ

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

بتوزّع شغل ثقيل على 8 أنوية وتتوقّع تقريبًا 8 أضعاف السرعة. بدل كده الكود بيطلع أبطأ من نواة واحدة أحيانًا. الـ CPU profiler بيقولك النواة مشغولة 100%، بس الشغل الحقيقي بطيء. ده مش عطل في المعالج. ده تنافس خفي على نفس سطر الذاكرة.

مخطط لنواتي معالج تكتبان على متغيرين مختلفين داخل نفس الـ cache line بحجم 64 بايت مع سهم invalidate أحمر يوضح تنافس MESI

مثال بسيط قبل الكلام العلمي

تخيّل مكتب فيه موظفين، كل واحد قاعد على مكتب لوحده وشغال على ورقته. لو كل واحد على ورقة منفصلة، الاتنين بيشتغلوا بالتوازي بدون مشاكل. دلوقتي حط الورقتين على نفس الصفحة الواحدة في نفس الدفتر. كل مرة موظف عايز يكتب، لازم ياخد الدفتر كله من زميله ويرجّعه بعدها. النتيجة: بيقضّوا وقتهم في تمرير الدفتر بدل ما يكتبوا.

ده بالظبط اللي بيحصل في الـ false sharing. المعالج مبيتعاملش مع بايت واحد لوحده. بيتعامل مع "صفحة" اسمها cache line.

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

المعالج مبينقلش بايت واحد بين الذاكرة والكاش. بينقل بلوك ثابت اسمه cache line، حجمه 64 بايت على أغلب معالجات x86 و ARM الحديثة. لو متغيرين مختلفين وقعوا جوّه نفس الـ 64 بايت، بيبقوا في نفس السطر فعليًا.

هنا بيدخل بروتوكول تماسك الكاش (cache coherence)، زي MESI. لمّا نواة تكتب على متغيرها، البروتوكول بيعلّم نسخة السطر في باقي الأنوية بإنها باطلة (invalidate). النواة التانية لمّا تيجي تكتب على متغيرها هي، بتلاقي السطر بطل، فتضطر تجيبه من جديد. النتيجة إن السطر بيفضل يتنطط بين الأنوية طول الوقت. الاسم "false" لأن الأنوية مش بتشارك نفس المتغير أصلًا؛ هي بس اتصادف إنها في نفس السطر.

القياس بالكود: تجربة تقيسها بنفسك

الكود ده بيشغّل 8 goroutines، كل واحدة بتزوّد عدّادها الخاص 100 مليون مرة. النسخة الأولى بتحط العدّادات متلاصقة (نفس الـ cache line تقريبًا). النسخة التانية بتضيف حشو (padding) عشان كل عدّاد يحتل سطر لوحده.

Go
package main

import (
	"fmt"
	"sync"
	"time"
)

const N = 100_000_000

type Bad struct{ v int64 }              // 8 بايت: العدّادات متلاصقة
type Good struct{ v int64; _ [56]byte } // 64 بايت: كل عدّاد على cache line لوحده

func bench[T any](items []T, inc func(*T)) time.Duration {
	var wg sync.WaitGroup
	t := time.Now()
	for i := range items {
		wg.Add(1)
		go func(p *T) {
			defer wg.Done()
			for j := 0; j < N; j++ {
				inc(p)
			}
		}(&items[i])
	}
	wg.Wait()
	return time.Since(t)
}

func main() {
	bad := make([]Bad, 8)
	good := make([]Good, 8)
	fmt.Println("bad :", bench(bad, func(c *Bad) { c.v++ }))
	fmt.Println("good:", bench(good, func(c *Good) { c.v++ }))
}

لاحظ إن مفيش data race هنا: كل goroutine بتكتب على عنصرها هي بس، مكانها مختلف في الذاكرة. الفرق الوحيد بين النسختين هو الـ padding. النتيجة على معالج بـ 8 أنوية فعلية (Go 1.22):

بدون padding: حوالي 1.31 ثانية. مع padding: حوالي 0.18 ثانية. الفرق قرب 7.3 أضعاف بنفس المنطق تمامًا. الأرقام بتختلف حسب المعالج وعدد الأنوية والـ compiler، بس الاتجاه ثابت: كل ما زادت الأنوية اللي بتكتب على نفس السطر، زاد التنطيط وبطؤ الكود.

سيناريو واقعي

الافتراض إن عندك خدمة بتجمّع مقاييس (metrics) وبتزوّد عدّاد لكل نواة عشان تتجنّب القفل (lock). لو حطّيت الـ 16 عدّاد في مصفوفة int64 متلاصقة، هتلاقي throughput العدّ أقل من ما تتوقّع تحت الضغط، والـ profiler بيوريك وقت ضايع في الكتابة على الذاكرة. ده نفس الفخ اللي وقعت فيه مكتبات معروفة قبل ما تضيف padding صريح لعدّاداتها.

الحل: افصل الأسطر بالـ padding

  1. حدد الحقول اللي بتتكتب من خيوط مختلفة بكثافة.
  2. حط كل حقل ساخن في struct لوحده، وكمّله لـ 64 بايت بحشو.
  3. في Go استخدم _ [56]byte بعد int64. في Java استخدم @Contended. في C/C++ استخدم alignas(64).
  4. قِس قبل وبعد. لو الفرق كبير، الـ false sharing كان موجود فعلًا.

الـ trade-offs

الـ padding مش ببلاش. بتكسب السرعة، بتخسر الذاكرة. في المثال فوق، الـ 8 عدّادات كبرت من 64 بايت لـ 512 بايت، أي 8 أضعاف. لـ 8 عدّادات ده لا يُذكر. لكن لو عندك مليون عنصر وكل واحد فيه lock مبطّن، الزيادة بتبقى مئات الميجابايت. الـ trade-off هنا: padding للحقول الساخنة القليلة، مش لكل حاجة. وكمان الـ 64 بايت مش مضمونة على كل معمارية؛ بعض المعالجات بتجيب سطرين مع بعض (prefetch pair) فتحتاج محاذاة 128 بايت.

متى لا تستخدم هذه الطريقة

ما تشغّلش بالك بالـ false sharing في الحالات دي: لو الداتا read-mostly، لأن المشكلة بتظهر مع الكتابة مش القراءة. لو الكود single-threaded أصلًا. لو التنافس منخفض والحقل بيتكتب نادرًا. وأهم حالة: لو الخيوط بتكتب على نفس المتغير فعلًا (true sharing)، هنا الـ padding مش هيحل حاجة؛ المشكلة في التصميم نفسه ومحتاج تقلّل الكتابة المشتركة أو تجمّعها.

المصادر

  • Intel — Avoiding and Identifying False Sharing Among Threads: intel.com
  • False sharing — Wikipedia: en.wikipedia.org/wiki/False_sharing
  • Ulrich Drepper — What Every Programmer Should Know About Memory (حجم الـ cache line و MESI): akkadia.org/drepper/cpumemory.pdf
  • Go — توثيق حزمة sync: pkg.go.dev/sync

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

افتح أكثر struct بيتكتب عليه من خيوط متعددة في كودك، وشوف حجم حقوله الساخنة. لو حقلين ساخنين واقعين في أقل من 64 بايت، افصلهم بـ padding وقِس الفرق بـ benchmark بسيط زي اللي فوق. لو الأداء اتحسّن، يبقى كان عندك false sharing مدفون طول الوقت.

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

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

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