المستوى المطلوب: محترف. الشرح يفترض إنك مرتاح مع الـ concurrency وعندك فكرة عن الذاكرة والـ CPU cache. لو المصطلحات دي جديدة عليك، فيه مثال مبسّط تحت قبل الجزء العلمي.
تقدر تكسب سرعة قرب 7 أضعاف في كود متعدد الخيوط من غير ما تغيّر سطر واحد في المنطق. الحيلة إنك تفصل عدّاداتك على cache lines مختلفة. المشكلة اللي بتقتل الأداء اسمها false sharing، وأغلب المطورين مش واخدين بالهم منها.
الـ False Sharing: ليه توزيع الشغل على 8 أنوية يخلّي كودك أبطأ
المشكلة باختصار
بتوزّع شغل ثقيل على 8 أنوية وتتوقّع تقريبًا 8 أضعاف السرعة. بدل كده الكود بيطلع أبطأ من نواة واحدة أحيانًا. الـ CPU profiler بيقولك النواة مشغولة 100%، بس الشغل الحقيقي بطيء. ده مش عطل في المعالج. ده تنافس خفي على نفس سطر الذاكرة.
مثال بسيط قبل الكلام العلمي
تخيّل مكتب فيه موظفين، كل واحد قاعد على مكتب لوحده وشغال على ورقته. لو كل واحد على ورقة منفصلة، الاتنين بيشتغلوا بالتوازي بدون مشاكل. دلوقتي حط الورقتين على نفس الصفحة الواحدة في نفس الدفتر. كل مرة موظف عايز يكتب، لازم ياخد الدفتر كله من زميله ويرجّعه بعدها. النتيجة: بيقضّوا وقتهم في تمرير الدفتر بدل ما يكتبوا.
ده بالظبط اللي بيحصل في الـ false sharing. المعالج مبيتعاملش مع بايت واحد لوحده. بيتعامل مع "صفحة" اسمها cache line.
التعريف العلمي الدقيق
المعالج مبينقلش بايت واحد بين الذاكرة والكاش. بينقل بلوك ثابت اسمه cache line، حجمه 64 بايت على أغلب معالجات x86 و ARM الحديثة. لو متغيرين مختلفين وقعوا جوّه نفس الـ 64 بايت، بيبقوا في نفس السطر فعليًا.
هنا بيدخل بروتوكول تماسك الكاش (cache coherence)، زي MESI. لمّا نواة تكتب على متغيرها، البروتوكول بيعلّم نسخة السطر في باقي الأنوية بإنها باطلة (invalidate). النواة التانية لمّا تيجي تكتب على متغيرها هي، بتلاقي السطر بطل، فتضطر تجيبه من جديد. النتيجة إن السطر بيفضل يتنطط بين الأنوية طول الوقت. الاسم "false" لأن الأنوية مش بتشارك نفس المتغير أصلًا؛ هي بس اتصادف إنها في نفس السطر.
القياس بالكود: تجربة تقيسها بنفسك
الكود ده بيشغّل 8 goroutines، كل واحدة بتزوّد عدّادها الخاص 100 مليون مرة. النسخة الأولى بتحط العدّادات متلاصقة (نفس الـ cache line تقريبًا). النسخة التانية بتضيف حشو (padding) عشان كل عدّاد يحتل سطر لوحده.
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++ }))
}