تولید کد میانی (Intermediate Code) در طراحی کامپایلر – راهنمای جامع
یک کد منبع را میتوان به صورت مستقیم به کد ماشین مقصد ترجمه کرد. در این صورت شاید این سؤال پیش بیاید که دلیل ترجمه کد منبع به یک کد میانی که در ادامه باید به کد مقصد ترجمه شود، چیست؟ در ادامه دلایل تولید کد میانی توضیح داده شده است.
اگر کامپایلر زبان منبع را بدون گزینهای برای تولید کد میانی به زبان ماشین مقصد ترجمه کند، در این صورت برای هر ماشین جدید میبایست یک کامپایلر بومی جدید استفاده شود.
کد میانی نیاز به کامپایلر جدید را برای هر ماشین منحصر به فرد جدید حذف میکند و بخش تحلیلی کد را برای همه کامپایلرها یکسان خواهد بود.
بخش دوم کامپایلر که بخش سنتز است بر اساس ماشین مقصد تغییر مییابد.
بدین ترتیب اعمال تغییرات کد منبع برای بهینهسازی عملکرد از طریق بهکارگیری تکنیکهای بهینهسازی کد روی کد میانی آسانتر خواهد بود.
بازنمایی میانی
کدهای میانی را میتوان به چندین روش نمایش داد که هر کدام مزایای خاص خود را دارند:
- نمایش سطح بالا (High Level IR) – نمایش سطح بالای کد میانی بسیار به خود زبان نزدیک است. این نمایش را به سادگی میتوان از روی کد منبع ایجاد کرد و امکان تغییرات کد برای بهینهسازی عملکردی به طور آسانی وجود دارد. اما برای بهینهسازی ماشین مقصد این روش ترجیح کمتری دارد.
- نمایش سطح پایین (Low Level IR) – این نوع از نمایش به ماشین مقصد نزدیکتر است و برای ثبت و تخصیص حافظه، انتخاب مجموعه دستورالعملها و غیره مناسب است. این روش برای بهینهسازیهای وابسته هبه ماشین خوب است.
کد میانی میتواند وابسته به زبان (مانند Byte Code برای جاوا) یا مستقل از زبان (کد سه آدرسی) باشد.
کد سه آدرسی
تولید کننده کد میانی ورودی را از فاز پیش قبلی که تحلیلگر معنایی است به شکل درخت نحویِ حاشیهنویسی شده دریافت میکند. سپس این درخت نحوی را میتوان به بازنمایی خطی یعنی نمادگذاری پسوندی تبدیل کرد. کد میانی معمولاً کدی مستقل از ماشین است. از این رو تولید کننده کد فرض میکند که تعداد نامحدودی از ثباتهای ذخیرهسازی حافظه برای تولید کد دارد.
مثال
a = b + c * d;
تولید کننده کد میانی تلاش میکند تا این عبارت را به زیرعبارتهایی تجزیه کند و سپس کد متناظر را تولید کند:
r1 = c * d; r2 = b + r1; a = r2
در برنامه مقصد r به عنوان یک ثبات استفاده میشود.
کد سه آدرسی در بیشینه حالت خود، سه موقعیت حافظه برای محاسبه عبارت دارد. یک کد سه آدرسی میتواند به دو شکل نمایش یابد: چهارتایی و سهتایی
چهارتایی
هر دستورالعمل در بازنمایی چهارتایی به چهار فیلد تقسیم میشود: عملگر، آرگومان 1، آرگومان 2 و نتیجه. مثال فوق را میتوان مانند زیر به شکل چهارتایی نمایش داد:
Op | arg1 | arg2 | نتیجه |
* | c | d | r1 |
+ | b | r1 | r2 |
+ | r2 | r1 | r3 |
= | r3 | a |
سهتایی
هر دستورالعمل در روش نمایش سهتایی سه فیلد دارد: عملگر، آرگومان 1 و آرگومان 2. نتایج زیرعبارتهای مربوطه بر اساس موقعیت عبارت مشخص میشوند. سهتاییها نشان دهنده مشابهت با DAG و درخت نحوی هستند. این درختها هنگام بازنمایی عبارتها، معادل DAG هستند.
Op | arg1 | arg2 |
* | c | d |
+ | b | (0) |
+ | (1) | (0) |
= | (2) |
سهتاییها هنگام بهینهسازی با مشکل عدم تحرکپذیری کد مواجه هستند، چون نتایج وابسته به موقعیت هستند و تغییر دادن ترتیب موقعیتهای یک عبارت میتواند باعث بروز مشکلاتی شود.
سهتایی غیرمستقیم
این بازنمایی در واقع شیوه بهینهای از بازنمایی سهتایی است. در این روش به جای استفاده از موقعیت برای ذخیرهسازی نتایج از اشارهگرها استفاده میشود. بدین ترتیب امکان بهینهسازی و تغییر موقعیت آزادانه زیرعبارتها برای تولید کد بهینه وجود دارد.
اعلانها
یک متغیر یا روال باید پیش از استفاده اعلان شود. اعلان شامل تخصیص فضایی در حافظه و مدخل نوع و نام در جدول نام است. یک برنامه میتواند طوری کدنویسی شده و طراحی شده باشد که ساختار ماشین مقصد را به خاطر خود بسپارد؛ اما ممکن است همواره این امکان برای تبدیل دقیق کد منبع به زبان مقصدش وجود نداشته باشد.
اگر کل یک برنامه به عنوان مجموعهای از روالها و زیر روالها انتخاب شود، میتوان همه نامها را به صورت بومی روال اعلان کرد. بدین ترتیب تخصیص حافظه به روشی محافظهکارانه صورت میگیرد و نامها در حافظه در جملهای که در برنامه اعلان شدهاند، تخصیص مییابند. ما از متغیر offset استفاده میکنیم و آن را صفر قرار میدهیم {offset = 0} که نشان دهنده آدرس پایه است.
زبان برنامهنویسی منبع و معماری ماشین مقصد ممکن است در مورد نامهایی که ذخیرهسازی میکنند تفاوتهایی داشته باشند و از این رو آدرسدهی نسبی مورد استفاده قرار میگیرد. هنگامی که نام اول با شروع از موقعیت حافظه صفر یعنی {offset = 0} در حافظه تخصیص مییابد، نام بعدی که در ادامه اعلان شده است باید در موقعیت بعد از موقعیت اول حافظه تخصیص یابد.
مثال
برای نمونه زبان برنامهنویسی C را در نظر بگیرید که یک متغیر Integer با 2 بایت حافظه تخصیص مییابد و متغیر float 4 بایت حافظه میگیرد.
int a; float b; Allocation process: {offset = 0} int a; id.type = int id.width = 2 offset = offset + id.width {offset = 2} float b; id.type = float id.width = 4 offset = offset + id.width {offset = 6}
برای وارد کردن این جزییات در یک جدول نماد، از یک روال به نام enter میتوان استفاده کرد. این متد میتواند ساختاری مانند زیر داشته باشد:
enter(name, type, offset)
این روال یک مدخل در نماد جدول برای متغیر name ایجاد میکند که مجموعه نوع و offset آدرس نسبی خاصی را در حوزه دادهای خودش دارد.
اگر به این نوشته علاقهمند بودید، موارد زیر نیز احتمالاً مورد توجه شما قرار خواهند گرفت:
- آموزش طراحی کامپایلر
- کامپایلر، طراحی و معماری آن — به زبان ساده
- مجموعه آموزشهای برنامهنویسی
- تحلیل نحوی (Syntax Analysis) در طراحی کامپایلر — راهنمای جامع
- دروس مهندسی کامپیوتر
- آموزش تجزیه انتقال (کاهش) در طراحی کامپایلر
==