هذا المقال يتطلب مستوى محترف. لو لسه بتبدأ في لغة منخفضة المستوى زي Go أو C ومش متعوّد على مفاهيم الذاكرة، ابدأ بمقال أبسط الأول ثم ارجع لهنا.
Struct Padding: ليه الـ struct بتاخد 24 بايت وانت حاطط 17 بايت بس
لو عندك مصفوفة من 10 مليون struct بتاكل 240 ميجا رام، إعادة ترتيب الحقول جواها بتنزّل الاستهلاك لـ 160 ميجا. من غير ما تشيل ولا حقل واحد. الفرق كله سببه حاجة اسمها Struct Padding.
المشكلة باختصار
المبرمج بيفترض إن حجم الـ struct = مجموع أحجام حقوله. ده غلط. الكومبايلر بيحشر بايتات فاضية بين الحقول علشان يحاذيها في الذاكرة. النتيجة: struct فيه 10 بايت بيانات فعلية ممكن ياخد 24 بايت في الرام. على مصفوفة كبيرة، الـ 14 بايت الضايعة دي بتتضاعف ملايين المرات.
الفكرة بمثال قبل الكلام العلمي
تخيّل رف فيه خانات، كل خانة بتسع 8 صناديق بالظبط. عندك صندوق صغير (بايت واحد)، وصندوق كبير لازم يبدأ من أول خانة جديدة (8 بايت)، وصندوق صغير تاني.
لو رصّيتهم بالترتيب ده: صغير، ثم كبير، ثم صغير — هتحط الصغير الأول، وبعدين الكبير مش هينفع يبدأ بعده على طول لأنه لازم يبدأ من بداية خانة، فهتسيب فراغ. النتيجة رف مليان فجوات. لكن لو حطيت الكبير الأول ثم الصغيرين ورا بعض، هتملا الرف أحسن بكتير. نفس الصناديق، ترتيب مختلف، مساحة أقل.
القاعدة علميًا: المحاذاة (Alignment)
المعالج مابيقراش الذاكرة بايت بايت. بيقراها في كلمات (words) بحجم ثابت، غالبًا 8 بايت على معمارية 64-bit. علشان القراءة تتم في دورة واحدة، كل نوع لازم عنوانه يكون من مضاعفات حجمه. ده اسمه natural alignment.
boolوint8: محاذاة 1 — يقدروا يبدأوا في أي عنوان.int32: محاذاة 4 — لازم يبدأ من عنوان من مضاعفات 4.int64و pointer: محاذاة 8 — لازم يبدأ من عنوان من مضاعفات 8.
والـ struct نفسه حجمه بيتقرّب لأعلى ليبقى من مضاعفات أكبر محاذاة جواه (trailing padding)، علشان لو عملت منه مصفوفة يفضل كل عنصر محاذى صح.
الكود اللي بيوريك الفرق
الكود ده شغّال على Go 1.23 على معمارية amd64. بنقيس الحجم الفعلي بـ unsafe.Sizeof:
package main
import (
"fmt"
"unsafe"
)
// ترتيب سيء: صغير، كبير، صغير
type BadLayout struct {
a bool // 1 بايت + 7 padding علشان b يحاذي على 8
b int64 // 8 بايت
c bool // 1 بايت + 7 trailing padding
} // الإجمالي: 24 بايت
// ترتيب جيد: الكبير الأول ثم الصغار
type GoodLayout struct {
b int64 // 8 بايت
a bool // 1 بايت
c bool // 1 بايت + 6 trailing padding
} // الإجمالي: 16 بايت
func main() {
fmt.Println("BadLayout :", unsafe.Sizeof(BadLayout{})) // 24
fmt.Println("GoodLayout:", unsafe.Sizeof(GoodLayout{})) // 16
}
البيانات الحقيقية في الحالتين 10 بايت (1 + 8 + 1). بس الترتيب السيء أضاف 14 بايت حشو، والترتيب الجيد أضاف 6 بايت بس. الفرق 24 → 16 بايت، أي توفير 33% بإعادة ترتيب سطرين.
السيناريو الواقعي: ليه ده مهم فعلاً
الـ 8 بايت دول مالهمش قيمة في struct واحد. بس افترض إن عندك خدمة بتحمّل 10 مليون سجل في الذاكرة (مثلاً جدول مستخدمين أو أحداث telemetry):
- الترتيب السيء: 24 بايت × 10,000,000 = 240 ميجابايت.
- الترتيب الجيد: 16 بايت × 10,000,000 = 160 ميجابايت.
توفير 80 ميجا في الرام من غير أي تغيير في المنطق. وفيه مكسب تاني أهم أحيانًا: كل struct أصغر يعني عناصر أكتر بتدخل في الـ CPU cache line (64 بايت)، فالمرور على المصفوفة بيبقى أسرع بسبب cache locality أحسن.
متلقّطش الحقول بإيدك: استخدم أداة
إعادة الترتيب يدوي بتغلط فيها وانت مرهق. Go فيه محلّل جاهز اسمه fieldalignment ضمن go vet:
# تثبيت الأداة
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
# فحص الحزمة وكشف الـ structs اللي ممكن تصغر
fieldalignment ./...
# إصلاح تلقائي لإعادة ترتيب الحقول (خد نسخة احتياطية قبلها)
fieldalignment -fix ./...
الأمر ده هيقولك بالظبط أنهي struct ممكن يتقلّص وبكام بايت، وهيعيد ترتيبه لك.
الـ trade-offs اللي لازم تعرفها
- الترتيب الأمثل للذاكرة ضد قابلية القراءة. ترتيب الحقول حسب الحجم بيكسب بايتات، بس بيخسر التجميع المنطقي (مثلاً كل حقول العنوان مع بعض). المكسب: ذاكرة أقل. الخسارة: كود أصعب في القراءة شوية.
- محاذاة مقابل أداء. لو شلت الحشو بالعافية (packed struct في C بـ
#pragma pack)، بتوفّر بايتات لكن بتخسر سرعة، لأن المعالج هيعمل قراءتين لكل حقل غير محاذى، وعلى بعض المعماريات بيتعطل أصلاً. - الافتراض هنا إن معماريتك 64-bit (amd64/arm64). الأحجام والمحاذاة بتختلف على 32-bit، فمتعتمدش على رقم محدد لو بتبني cross-platform.
متى متشغّلش بالك بالموضوع ده
لو عندك مئات أو آلاف الـ structs مش ملايين، التوفير هيبقى كيلوبايتات لا أحد يحس بيها. وقتها إعادة الترتيب مجرد تعقيد بدون عائد. ركّز على الـ padding بس لما الـ struct بيتكرر بكميات ضخمة في الذاكرة أو في hot path. وقبل أي تحسين، قِس بـ pprof الأول علشان تتأكد إن الذاكرة فعلاً مشكلة عندك.
الخطوة التالية
افتح أكبر struct بيتكرر في كودك، شغّل عليه fieldalignment ./...، وشوف لو بيقترح تصغير. لو الفرق معتبر مضروب في عدد العناصر عندك، طبّق -fix وقِس استهلاك الذاكرة قبل وبعد. لو مفيش فرق ملحوظ، يبقى مشكلتك مش هنا — كمّل لحاجة تانية.
المصادر
- قواعد الحجم والمحاذاة في Go — The Go Programming Language Specification, "Size and alignment guarantees": go.dev/ref/spec
- أداة fieldalignment — golang.org/x/tools: pkg.go.dev/.../fieldalignment
- Eric S. Raymond, "The Lost Art of Structure Packing": catb.org/esr/structure-packing
- Data structure alignment — Wikipedia: en.wikipedia.org/wiki/Data_structure_alignment
- توثيق حزمة unsafe في Go: pkg.go.dev/unsafe