يتطلب مستوى: محترف
Struct Field Alignment في Go: نزّل ذاكرة 100 مليون كائن بنسبة 50%
لو خدمة في الإنتاج بتحتفظ بـ 100 مليون struct في الذاكرة وبتاكل 4.8 جيجابايت، إعادة ترتيب 3 fields بتنزّل الرقم لـ 2.4 جيجا بدون لمس أي logic. السبب اسمه struct padding، وهو سلوك معتمد من المعالج نفسه مش من Go.
المشكلة باختصار
الـ Go compiler بيضيف bytes فاضية بين fields في الـ struct علشان كل field يبدأ على عنوان متوافق مع حجمه. لما الـ fields متلخبطة، الـ padding بيتراكم بدون فايدة. مثال مباشر: struct فيها bool ثم int64 ثم bool بتاخد 24 بايت. نفس الـ fields بترتيب int64 ثم bool ثم bool بتاخد 16 بايت. الفرق 33% في كل instance، ولما الـ struct أكبر النسبة بتوصل لـ 50%.
ليه الـ padding بيحصل أصلاً — مثال للمبتدئ
تخيل درج مطبخ بيتعبي بأكواب. كل كوب لازم ياخد خانة كاملة بطول 8 سم حتى لو هو طوله 1 سم. لو حطيت كوب صغير ثم كبير ثم صغير، الكبير محتاج خانته الكاملة، فبتفضل مساحة فاضية حوالين الصغار. لكن لو لمّيت كل الكبار في ناحية واحدة والصغار جنب بعض، المساحة الضايعة بتقل.
المعالج بيشتغل بنفس المنطق بالظبط. بيقرأ الذاكرة في وحدات word size — 8 بايت على معالج 64-bit. وبيتطلب إن أي field 8 بايت يبدأ في عنوان قابل للقسمة على 8، أي field 4 بايت يبدأ على عنوان قابل للقسمة على 4، وهكذا. ده اللي اسمه natural alignment.
الشرح العلمي الدقيق
على معماري x86_64 و ARM64 الـ CPU بيقرأ الذاكرة في وحدات بـ cache line حجمها 64 بايت. لو int64 بدأت عند offset 1 بدل 0، المعالج هيحتاج عمليتين قراءة بدل واحدة لجلبها — مرة لجلب البايتات من 0 لـ 7، ومرة تانية للبايتات من 8 لـ 15. ده بيكسّر الـ cache locality وبيضاعف زمن الوصول من ~4 cycles لـ ~10 cycles.
الافتراض هنا: انت شغال على معماري حديث (x86_64 من 2010 وبعدها، أو ARM64). على معماري قديم زي ARMv5 الـ unaligned access بيرمي SIGBUS فعلياً، مش بس بيبطّأ. Go بيضمن natural alignment افتراضياً علشان الكود يشتغل على كل المعماريات اللي بيدعمها.
المثال التنفيذي — قبل وبعد
package main
import (
"fmt"
"unsafe"
)
// ترتيب سيء — bool بين int64 بيخلق padding
type SessionBad struct {
IsActive bool // 1 بايت + 7 padding
UserID int64 // 8 بايت
IsVerified bool // 1 بايت + 7 padding tail
}
// ترتيب كفء — int64 الأول، bool في الآخر
type SessionGood struct {
UserID int64 // 8 بايت
IsActive bool // 1 بايت
IsVerified bool // 1 بايت + 6 padding tail
}
func main() {
fmt.Println(unsafe.Sizeof(SessionBad{})) // يطبع 24
fmt.Println(unsafe.Sizeof(SessionGood{})) // يطبع 16
}