آرایه‌ها در مقابل اشاره‌گرها در سطح سخت‌افزار

در C، آرایه‌ها و اشاره‌گرها اغلب قابل‌تعویض به‌نظر می‌رسند، و بسیاری از برنامه‌نویسان با آن‌ها به‌عنوان یک چیز واحد رفتار می‌کنند. با این حال در سطح سخت‌افزار، آن‌ها به توالی‌های دستوری قابل‌توجهی متفاوت با ویژگی‌های کارایی متفاوت کامپایل می‌شوند. این مقاله این دو رویکرد را با استفاده از اسمبلی RISC-V مقایسه می‌کند تا دقیقاً نشان دهد چرا کد مبتنی بر اشاره‌گر اغلب سریع‌تر است.

آرایه در مقابل اشاره‌گرحساب اشاره‌گرمحاسبه آدرس RISC-V

~3 دقیقه مطالعه · آخرین به‌روزرسانی ۱۵ شهریور ۱۴۰۵

چرا این مقایسه اهمیت دارد

در کد سطح بالای C، پیمایش یک آرایه با استفاده از نمادگذاری ایندکس و پیمایش آن با استفاده از اشاره‌گر، هر دو طبیعی به‌نظر می‌رسند و اغلب نتیجه منطقی یکسانی تولید می‌کنند. اما یک کامپایلر هرکدام از این سبک‌ها را به الگوی متمایزی از دستورات RISC-V ترجمه می‌کند، و کار سخت‌افزاری حاصل یکسان نیست. درک این تفاوت منبع رایجی از بینش درباره اینکه چرا سبک‌های کدنویسی خاصی در عمل سریع‌تر اجرا می‌شوند است.

رویکرد ایندکس‌گذاری آرایه

یک حلقه ساده را در نظر بگیرید که هر عنصر یک آرایه را با استفاده از نمادگذاری ایندکس پاک می‌کند:

void clear1(long array[], long size) {
    for (long i = 0; i < size; i += 1) {
        array[i] = 0;
    }
}

برای ترجمه array[i] به یک دسترسی حافظه، پردازنده باید در هر تکرار حلقه، آدرس حافظه را با ضرب کردن ایندکس i در اندازه هر عنصر مجدداً محاسبه کند، سپس آن نتیجه را به آدرس پایه آرایه اضافه کند:

Loop:
bge i, size, Exit
slli t0, i, 3
add t1, array, t0
sd x0, 0(t1)
addi i, i, 1
jal x0, Loop
Exit:

توجه کنید به دستورات اضافی slli (شیفت به چپ، که اینجا برای ضرب در ۸ بایت استفاده شده) و add که در هر تکرار صرفاً برای محاسبه آدرس از روی ایندکس مورد نیازند.

رویکرد مبتنی بر اشاره‌گر

حالا همان منطق را با استفاده از حساب اشاره‌گر به‌جای ایندکس‌گذاری در نظر بگیرید:

void clear2(long *array, long size) {
    long *p;
    for (p = &array[0]; p < &array[size]; p = p + 1) {
        *p = 0;
    }
}

اینجا، کامپایلر می‌تواند حلقه‌ای تولید کند که در آن خود اشاره‌گر مستقیماً در هر تکرار به‌اندازه یک مقدار ثابت افزایش می‌یابد، بدون اینکه هرگز نیاز باشد آدرسی از روی ایندکس دوباره محاسبه شود:

Loop:
bge p, end_p, Exit
sd x0, 0(p)
addi p, p, 8
jal x0, Loop
Exit:

این نسخه مرحله ضرب را کاملاً حذف می‌کند، چون اشاره‌گر از قبل یک آدرس حافظه واقعی را نگه می‌دارد و فقط به یک جمع ثابت ساده برای رفتن به عنصر بعدی نیاز دارد.

چرا این تفاوت وجود دارد

یک ایندکس آرایه صرفاً یک عدد است که باید هر بار که استفاده می‌شود به یک آدرس تبدیل شود، که نیازمند ضرب در اندازه عنصر در هر دسترسی است. یک اشاره‌گر، در مقابل، از قبل یک آدرس است، پس پیش‌بردن آن به عنصر بعدی فقط نیازمند اضافه کردن یک‌باره اندازه ثابت عنصر است، نه محاسبه مجدد یک حاصل‌ضرب از ابتدا.

نتیجه عملی

کامپایلرهای بهینه‌ساز امروزی اغلب قادرند به‌طور خودکار حلقه‌های مبتنی بر ایندکس را داخلاً به حلقه‌های مبتنی بر اشاره‌گر تبدیل کنند، فرآیندی به نام Strength Reduction، که می‌تواند این شکاف کارایی را بدون نیاز به بازنویسی دستی کد توسط برنامه‌نویس از بین ببرد. با این حال، درک تفاوت زیربنایی سخت‌افزاری توضیح می‌دهد چرا از نظر تاریخی، و در مواردی که کامپایلر نمی‌تواند این بهینه‌سازی را با امنیت انجام دهد، پیمایش مبتنی بر اشاره‌گر برای کد حساس به کارایی توصیه شده است.

نوشته و پژوهش‌شده توسط دکتر شاهین صیامی

مقالات مرتبط

خطرهای داده در پایپ‌لاین: Forwarding در مقابل Stalling

هم‌پوشانی اجرای دستورات یک مشکل جدی ایجاد می‌کند وقتی یک دستور به نتیجه‌ای نیاز دارد که دستور قبلی هنوز محاسبه آن را تمام نکرده است. این مقاله توضیح می‌دهد خطرهای داده چه هستند، forwarding چگونه بیشتر آن‌ها را بدون از دست دادن هیچ کارایی حل می‌کند، و چرا برخی موقعیت‌ها همچنان نیاز به توقف پایپ‌لاین دارند.

ادامه

تبدیل یک Datapath تک‌سیکلی به یک Datapath پایپ‌لاین‌شده

هم‌پوشانی اجرای دستورات به بیش از اجرای سریع‌تر همان سخت‌افزار تک‌سیکلی نیاز دارد؛ نیازمند جداسازی فیزیکی هر مرحله پایپ‌لاین با عناصر ذخیره‌سازی و تکرار منطق کنترلی در سراسر مراحل است. این مقاله توضیح می‌دهد رجیسترهای پایپ‌لاین چگونه وضعیت دستور را بین مراحل حفظ می‌کنند و سیگنال‌های کنترلی چگونه همراه با داده در سراسر پایپ‌لاین حرکت می‌کنند.

ادامه

مروری بر پایپ‌لاینینگ: هم‌پوشانی اجرای دستورات

یک پردازنده تک‌سیکلی مقدار عظیمی از زمان بیکاری سخت‌افزار را هدر می‌دهد چون هر دستور باید در طول کندترین دستور ممکن جا شود. این مقاله پایپ‌لاینینگ را به‌عنوان یک راه‌حل معرفی می‌کند، تشبیه کلاسیک خط تولید را توضیح می‌دهد، پایپ‌لاین استاندارد پنج‌مرحله‌ای را تجزیه می‌کند، و توضیح می‌دهد چرا پایپ‌لاینینگ توان عملیاتی دستورات را افزایش می‌دهد بدون اینکه هیچ دستور منفردی را سریع‌تر کند.

ادامه

طراحی منطق کنترلی برای یک پردازنده تک‌سیکلی

یک datapath به‌تنهایی بدون سیگنال‌های کنترلی که به آن بگویند برای هر دستور چه کاری انجام دهد، هیچ کاری نمی‌کند. این مقاله توضیح می‌دهد منطق کنترلی چگونه فیلدهای opcode و function یک دستور را می‌خواند تا سیگنال‌های دقیق مورد نیاز برای مسیریابی درست داده را تولید کند، و مرور می‌کند یک پیاده‌سازی کامل تک‌سیکلی چگونه انواع مختلف دستورات را اجرا می‌کند.

ادامه

ساخت یک Datapath: اتصال رجیسترها، حافظه، و ALU

یک datapath مدار فیزیکی است که داده را در طول اجرای یک دستور از میان پردازنده حرکت می‌دهد. این مقاله بلوک‌های سازنده اساسی سخت‌افزاری مورد نیاز برای fetch، decode، و اجرای دستورات را تجزیه می‌کند، و نشان می‌دهد آن‌ها چگونه به‌هم سیم‌کشی می‌شوند تا یک datapath پردازنده کارآمد، هرچند ساده‌شده، تشکیل دهند.

ادامه

مقدمه‌ای بر طراحی پردازنده و قوانین منطق دیجیتال

پیش از آنکه یک پردازنده بتواند ساخته شود، طراحان آن باید روی مجموعه‌ای مشترک از قوانین برای نحوه رفتار مدارهای دیجیتال در طول زمان توافق کنند. این مقاله توضیح می‌دهد ساخت یک پردازنده واقعاً چه چیزی را شامل می‌شود، دو دسته گسترده پیاده‌سازی که در این فصل پوشش داده می‌شود، و قراردادهای بنیادین طراحی منطقی که رفتار مدار را قابل‌پیش‌بینی می‌کنند.

ادامه