محاذاة عناوين الذاكرة

من ويكي
اذهب إلى: تصفح، ابحث

يقرأ المعالج الذاكرة عادةً من عناوين "بمضاعفات" نوع البيانات التي يقرأها، مثل بايت أو بايتين أو 4 بايت أو 8 بايت .. إلخ، فهم هذا الشيء مهم عند تصميم البروتوكولات وصيغ الملفات وفهم سبب البايتات الإضافية غير المستخدمة في بعض تلك الصيغ والبروتوكولات، وستفهم بعض المفاهيم مثل المحاذاة alignment والحشو padding، هذا المقال ليس خاص بـC أو لغة معينة، لكنني سأستخدم C لعرض المشكلة، نظراً لأن اللغات الأخرى تخفي عنك هذه المشكلة.

لو قلنا أنك تريد قراءة نوع بيانات حجمه يساوي 4 بايت، أي 32 بت، فالمعالج يستطيع قراءة العناوين التالية (العمود الأيسر للعنوان، والأيمن للقيم):

0x00000000 XX XX XX XX
0x00000004 XX XX XX XX
0x00000008 XX XX XX XX
0x0000000D XX XX XX XX
...

لو أراد المعالج قراءة 4 بايت، عند العنوان 0x00000004، فلاتوجد لديه مشكلة، لأن العنوان في هذه الحالة عنوان "محاذى" aligned على 4 بايت، أي من مضاعفات 4 بايت والتي تساوي حجم هذا النوع.

لكن المشكلة ستظهر لو أراد المعالج قراءة أربع بايت من عنوان غير محاذى misaligned مثل 0x00000005، المعالجات هنا تختلف في تصرفها، فبعض المعالجات عالية الأداء مثل SPARC والمعالجات الرسومية ومعالجات الـ SIMD لاتدعم الوصول غير المحاذى لما يضيفه من تعقيد على تصميم المعالج وكذلك لما يسبب من تقليل أداء البرنامج، لذا تقرر عدم دعمه، سيعيط المعالج استثناء عند محاولة قراءة 4 بايت من عنوان غير محاذى على 4 بايت ثم سيوقف نظام التشغيل برنامجك.

بعض المعالجات، خصوصاً المعالجات المعقدة مثل x86 و x86-64، تدعم الوصول غير المحاذى، لكنها تحتاج لقراءة الذاكرة مرتين، فحتى تقرأ 4 بايت من العنوان 0x00000005:

0x00000004 XX 00 00 00
0x00000008 00 XX XX XX

فسيقرأ المعالج أولاً 4 بايت (حجم النوع) من العنوان 0x00000004، ثم يأخذ آخر ثلاثة بايتات، ويقرأ 4 بايت مرة أخرى من العنوان 0x00000008، وتأخذ أول بايت، ثم يضم تلك البايتات لبعض مكون الأربعة بايتات التي أردت قراءتها.

ستلاحظ هنا أن المعالج احتاج لإجراء دورتين قراءة من الذاكرة، قد لاتبدو مشكلة، لكن تكرار قراءة العناوين غير المحاذاة يقلل أداء البرنامج كثيراً، فالمعالج الذي يعمل بسرعة يحتاج لوقت طويل، حسب معاييره، حتى يقرأ البيانات من الذاكرة، خصوصاً في حال لم تكن متوفرة في ذاكرة التخزين المؤقت cache، وسيحتاج لإجراء عمليتين قراءة أو عمليتين كتابة لكل عنوان غير محاذى، بعض المعالجات كما قلت لاتدعم الوصول غير المحاذى أساساً، لذا سينهار برنامجك لو أردت تشغيله على تلك المعماريات.

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

لو قرأت بالتفصيل عن C، فستجد أن C تسمح لك بتحويل int * إلى char *، في الحقيقة أنه يمكنك تحويل أي مؤشر إلى char *، وتضمن لك أنه لاتوجد مشاكل، إلا أنها لاتضمن لك أن برنامجك سيعمل لو قمت بالعكس، أي حولت char * إلى int *، فمجرد محاولتك لقراءة العدد الذي يشير له int * قد ينهار برنامجك، لأن عنوان char * كان غير محاذى ومعالجك لايدعم الوصول غير المحاذى.

الحالة الأخرى التي ستلاحظ أثر المحاذاة بها، أنه عندما ننشئ بنية كالتالي[١]:

#include <stdio.h>

struct MyStruct
{
  char c;   /* 1 */
  int i;    /* 4 */
};

int main(void)
{
  printf("%d\n", sizeof(struct MyStruct));
  return 0;
}

لو لاحظت ستجد أن مجموع أحجام عناصر البنية MyStruct يساوي 5، إلا أن حجم البنية ككل يساوي 8، سبب هذا أن هناك ثلاث بايتات أضيفت بين c و i لجعل العنصر i محاذى على 4 بايت:

cc ?? ?? ??
ii ii ii ii

تسمى هذه العملية بالحشو padding، حتى لو عكس الترتيب، فالمترجم قد يضيف حشوه كي يضمن في حالة مثلاً أنشأت متغيرين من البنية:

struct MyStruct a;
struct MyStruct b;

فـ b.i ستكون محاذاة ولاتقع في هذه المشكلة:

ii ii ii ii      محاذاة صحيحة a.i
cc ii ii ii      محاذاة خاطئة b.i
ii cc

أغلب المترجمات تسمح لك بإلغاء المحاذاة وتغييرها، مثلاً لإلغاء المحاذاة في مترجم gcc و clang يمكنك استخدام الإضافة __attribute__((packed)):

#include <stdio.h>

struct MyStruct
{
  char c;   /* 1 */
  int i;    /* 4 */
} __attribute__((packed));

int main(void)
{
  printf("%d\n", sizeof(struct MyStruct));
  return 0;
}

وستعطيك أن حجم البنية يساوي 5، إلا أنني لا أنصحك باستخدامها لأن برنامجك لن يعمل على كل المعالجات بالإضافة لتأثيرها على الأداء.

بالنسبة لتغيير المحاذاة، فيمكن استخدام __attribute__((aligned(N)))، حيث أن N تمثل المحاذاة التي تريدها، مثلاً هنا:

#include <stdio.h>

struct MyStruct
{
  char c;
  int i __attribute__((aligned(8))); 
};

int main(void)
{
  printf("%d\n", sizeof(struct MyStruct));
  return 0;
}

عندي حجم البنية أصبح 16 وذلك لمحاذاة i على 8 بايت:

cc ?? ?? ?? ?? ?? ?? ??
ii ii ii ii ?? ?? ?? ??
  1. حجم المتغيرات في C يختلف باختلاف المترجمات، الشيء الوحيد المتفق عليه أن حجم char دائماً 1، إلا أن باقي أنواع البيانات مثل int قد يختلف، فقد يكون 2 أو 4 أو 8 وغيرها، حسب المترجم.