مستوى المقال: محترف — يفترض إنك بتكتب بلغة فيها بنى مثل C أو C++ أو Rust أو Go، وإنك بتتعامل مع مصفوفات كبيرة من الـ structs.
ليه الـ struct بياخد ذاكرة أكتر من مجموع حقوله؟ Struct Padding بالتفصيل
لو عندك مصفوفة فيها 100 مليون struct، ترتيب الحقول جوّه الـ struct لوحده ممكن يفرّق 800 ميجابايت في الرام من غير ما تغيّر بايت واحد من بياناتك. المقال ده بيوريك ليه بيحصل ده، وإزاي تقيسه، وإزاي تصلحه بإعادة ترتيب سطرين.
المشكلة باختصار
خد الـ struct ده: char a (1 بايت) + int b (4) + char c (1) + double d (8). مجموع الحقول 14 بايت. تتوقع الـ struct ياخد 14. اطبع sizeof هتلاقيه 24. فين الـ 10 بايت الزيادة؟ دي مش بياناتك، دي حشو (padding) المترجم بيحطه عشان يحافظ على المحاذاة.
ليه المترجم بيحط حشو؟ قاعدة المحاذاة (Alignment)
تخيّل رفّ كتب مقسوم لخانات، كل خانة 4 سنتيمتر. القاعدة: أي كتاب لازم يبدأ من أول خانة، مينفعش يبدأ من نص خانة. لو عندك كتاب صغير احتل نص خانة، الكتاب اللي بعده لازم يستنى لأول الخانة الجاية، والفراغ اللي في النص بيفضل ضايع. ده بالظبط اللي بيحصل في الذاكرة.
علميًا: المعالج بيقرا الذاكرة في وحدات محاذاة. الـ int حجمه 4 بايت ولازم عنوانه يكون من مضاعفات 4. الـ double حجمه 8 ولازم عنوانه من مضاعفات 8. المترجم لمّا يحط char a في العنوان 0، الـ int b اللي بعده مينفعش يقعد في العنوان 1، لازم يستنى لعنوان 4. فبيتحط حشو في 1 و2 و3. نفس الحكاية قبل الـ double. والـ struct نفسه حجمه لازم يكون من مضاعفات أكبر حقل فيه (8 هنا)، عشان لو عملت منه مصفوفة يفضل كل عنصر محاذي صح.
الافتراض هنا إنك على معمارية x86-64 بواجهة System V ABI (لينكس وmacOS). الأرقام بتختلف على معماريات تانية، بس المبدأ واحد.
قيس بنفسك: كود C بيطبع الحجم
مش محتاج تصدّقني. جرّب الكود ده بنفسك:
#include <stdio.h>
#include <stddef.h>
struct Bad { char a; int b; char c; double d; };
struct Good { double d; int b; char a; char c; };
int main(void) {
printf("Bad size = %zu\n", sizeof(struct Bad));
printf("Good size = %zu\n", sizeof(struct Good));
printf("Bad.b offset = %zu\n", offsetof(struct Bad, b));
printf("Bad.d offset = %zu\n", offsetof(struct Bad, d));
return 0;
}المخرج بيأكد الكلام: Bad بياخد 24 وGood بياخد 16. لاحظ إن b بادئ من العنوان 4 مش 1، وده الدليل المباشر على الحشو.
الحل: أعد ترتيب الحقول من الأكبر للأصغر
الترتيب مش عشوائي. رتّب حقولك تنازليًا حسب حجمها: الـ double الأول، بعده الـ int، بعده الـ char. كده كل حقل بيقع طبيعي في مكانه المحاذي من غير فجوات في النص، والحشو الوحيد اللي بيفضل هو في الآخر عشان حجم الـ struct يكمل لمضاعف 8.
بإعادة الترتيب بس نزلنا من 24 لـ 16 بايت، يعني وفّرنا 33% من حجم الـ struct، والبيانات هي هي بالظبط. صفر تغيير في المنطق، صفر خطر.
الأثر الحقيقي: مش بس ذاكرة، ده أداء كمان
لو عندك مصفوفة بـ 100 مليون عنصر: بترتيب Bad هتاخد 2.4 جيجابايت، وبترتيب Good هتاخد 1.6 جيجا. وفّرت 800 ميجا من الرام على تغيير سطرين.
الأهم من الرام هو الكاش. سطر الكاش (cache line) في المعالجات الحديثة 64 بايت. الـ struct بحجم 16 بايت بيدخل منه 4 عناصر في كل سطر كاش، بينما بحجم 24 بايت بيدخل 2.67 عنصر بس. يعني لمّا تلف على المصفوفة، النسخة الأصغر بتجيب بيانات أكتر في كل قراءة كاش، والحلقة بتقل فيها الـ cache misses بحوالي الثلث. في حلقات ثقيلة على بيانات كبيرة، ده فرق ملموس في الزمن مش مجرد توفير ذاكرة.
الـ trade-offs وما يجب الانتباه له
ترتيب الحقول للكفاءة بيكسبك ذاكرة وأداء كاش. بتخسر إيه؟ حاجتين: الأول إن الترتيب المنطقي اللي بيريّح القارئ ممكن يتضحّى بيه لصالح الترتيب حسب الحجم، فالكود بيبقى أقل وضوحًا. التاني إنك ممكن تتغرّي تستخدم __attribute__((packed)) عشان تشيل الحشو خالص. متعملش ده كحل افتراضي: الـ packed بيخلي الوصول لحقل غير محاذي، وده على بعض المعماريات بيبطّئ الوصول أضعافًا أو بيعمل crash مباشرة (على ARM مثلًا). الـ packed لحالات التسلسل الثنائي والبروتوكولات بس، مش لتوفير رام عام.
القاعدة العملية: رتّب الحقول تنازليًا بالحجم أولًا. ده بيوفر أغلب الحشو بصفر مخاطرة. سيب الـ packed لآخر حل ولحالة محددة.
متى لا تشغل بالك بهذا؟
لو عندك عدد قليل من الـ structs (عشرات أو مئات)، الفرق بين 16 و24 بايت غير محسوس، وضوح الكود أهم بكتير. كمان لو شغّال بلغة زي Rust فالمترجم افتراضيًا بيعيد ترتيب الحقول لك تلقائيًا (إلا لو حطّيت #[repr(C)]). ولو في Go، الـ compiler مش بيعيد الترتيب فبتحتاج ترتّب يدوي، بس برضه بس لو الأعداد كبيرة. باختصار: التحسين ده يستاهل بس لمّا يكون عندك مصفوفات ضخمة أو حلقات حسّاسة للأداء.
الخطوة التالية
افتح أكبر struct في مشروعك، واطبع sizeof بتاعه ومجموع أحجام حقوله. لو فيه فرق، رتّب الحقول تنازليًا بالحجم وقيس تاني. لو بتستخدم Clang، شغّل clang -Wpadded -Xclang -fdump-record-layouts وهيوريك بالظبط كل بايت حشو فين. ابدأ بالـ struct اللي بيتعمل منه أكبر مصفوفة، ده اللي هيوفّرلك أكتر.
المصادر
- Eric S. Raymond — "The Lost Art of Structure Packing":
catb.org/esr/structure-packing/ - Ulrich Drepper — "What Every Programmer Should Know About Memory" (2007)، أقسام المحاذاة والكاش.
- cppreference — Object alignment و
_Alignof/alignas:en.cppreference.com/w/c/language/object - GCC Manual —
__attribute__((packed))وaligned. - Intel 64 and IA-32 Optimization Reference Manual — Data Alignment.