مهندسی پیشرفته کامپیوتر و طراحی سیستم
معماری پیشرفته پردازنده
خط لوله: فراتر از خط مونتاژ
احتمالاً با مفهوم خط لوله (Pipelining) به عنوان یک خط مونتاژ برای دستورالعملها آشنا هستید. هر دستورالعمل از مراحلی مانند واکشی (Fetch)، رمزگشایی (Decode)، اجرا (Execute) و نوشتن نتیجه (Write-back) عبور میکند. در یک پردازنده ساده، دستور بعدی تنها پس از اتمام کامل دستور قبلی شروع میشود. اما در یک پردازنده خط لولهای، این مراحل با هم همپوشانی دارند و چندین دستورالعمل به طور همزمان در مراحل مختلف پردازش قرار میگیرند. این کار توان عملیاتی (Throughput) پردازنده را به شدت افزایش میدهد.
اما این مدل ایدهآل همیشه به راحتی کار نمیکند. سه نوع «خطر» یا Hazard میتوانند این جریان روان را مختل کنند:
| نوع خطر (Hazard) | توضیح | مثال |
|---|---|---|
| ساختاری (Structural) | زمانی رخ میدهد که دو دستورالعمل به طور همزمان به یک منبع سختافزاری نیاز داشته باشند. | دو دستور همزمان بخواهند به حافظه دسترسی پیدا کنند. |
| داده (Data) | دستورالعمل بعدی به نتیجه دستورالعمل قبلی که هنوز آماده نشده، وابسته است. | ADD R1, R2, R3 (R1 = R2 + R3) و بلافاصله SUB R4, R1, R5 (R4 = R1 - R5). دستور SUB به مقدار جدید R1 نیاز دارد. |
| کنترل (Control) | پردازنده نمیداند دستور بعدی که باید واکشی شود کدام است. این مشکل معمولاً با دستورات پرش یا شاخه (Branch) پیش میآید. | یک دستور if-else که بسته به نتیجه شرط، مسیر اجرا را تغییر میدهد. |
برای مقابله با خطرات داده، پردازندههای مدرن از تکنیکی به نام پیشفرستی (Forwarding) یا بایپس (Bypassing) استفاده میکنند. به جای اینکه منتظر بمانند تا نتیجه یک دستورالعمل در رجیستر نوشته شود، سختافزار نتیجه را مستقیماً از خروجی واحد اجرا (ALU) به ورودی ALU برای دستورالعمل بعدی ارسال میکند. این کار از توقفهای غیرضروری (Stalls) جلوگیری میکند. اگر پیشفرستی ممکن نباشد، پردازنده مجبور میشود یک یا چند چرخه ساعت متوقف شود تا داده مورد نیاز آماده گردد.
پیشبینی آینده: هنر پیشبینی شاخه
خطرات کنترلی یکی از بزرگترین موانع برای کارایی خط لوله هستند. وقتی پردازنده به یک دستور شرطی (مانند if) میرسد، باید تصمیم بگیرد که آیا شاخه (Branch) را دنبال کند یا به دستورالعمل بعدی ادامه دهد. تصمیم اشتباه به معنای واکشی و پردازش دستورالعملهای نادرست است که باید از خط لوله پاک شوند (Pipeline Flush) و این کار چرخههای ساعت ارزشمندی را هدر میدهد.
برای حل این مشکل، پردازندهها از یک واحد سختافزاری تخصصی به نام واحد پیشبینی شاخه (Branch Prediction Unit - BPU) استفاده میکنند. این واحد تاریخچه شاخههای قبلی را ثبت میکند تا رفتار آینده آنها را حدس بزند. الگوریتمهای مختلفی برای این کار وجود دارد:
- پیشبینی ایستا (Static Prediction): یک قانون ساده و ثابت. مثلاً «همیشه فرض کن شاخه انجام نمیشود» یا «همیشه فرض کن شاخههای رو به عقب (مانند انتهای حلقهها) انجام میشوند».
- پیشبینی پویا (Dynamic Prediction): این روشها رفتار شاخه را در زمان اجرا یاد میگیرند. یک پیشبینیکننده ساده ۱ بیتی، آخرین نتیجه شاخه را به خاطر میسپارد. اما این روش در انتهای حلقهها که یک بار شاخه انجام نمیشود، دو بار اشتباه میکند. برای بهبود، از پیشبینیکنندههای اشباع ۲ بیتی (2-bit Saturating Counters) استفاده میشود. این پیشبینیکنندهها تنها پس از دو پیشبینی غلط متوالی، نظر خود را تغییر میدهند و در نتیجه عملکرد بسیار بهتری دارند.
| وضعیت فعلی | نتیجه واقعی شاخه | وضعیت بعدی | پیشبینی |
|---|---|---|---|
| 00 (Strongly Not Taken) | Not Taken | 00 | Not Taken |
| 00 (Strongly Not Taken) | Taken | 01 | Not Taken |
| 01 (Weakly Not Taken) | Not Taken | 00 | Not Taken |
| 01 (Weakly Not Taken) | Taken | 10 | Not Taken |
| 10 (Weakly Taken) | Not Taken | 01 | Taken |
| 10 (Weakly Taken) | Taken | 11 | Taken |
| 11 (Strongly Taken) | Not Taken | 10 | Taken |
| 11 (Strongly Taken) | Taken | 11 | Taken |
هنگامی که BPU یک شاخه را پیشبینی میکند، پردازنده به صورت (Speculatively) شروع به اجرای دستورالعملهای مسیر پیشبینی شده میکند. این کار به پردازنده اجازه میدهد تا خط لوله را پر نگه دارد. اگر پیشبینی درست باشد، نتایج نهایی میشوند. اگر اشتباه باشد، تمام عملیات حدسی لغو شده و خط لوله با دستورالعملهای مسیر صحیح پر میشود. این تکنیک، با وجود ریسک، یک عامل کلیدی در عملکرد پردازندههای مدرن است.
دو فلسفه، یک هدف: RISC در برابر CISC
پردازندهها را میتوان بر اساس «مجموعه دستورالعمل» (Instruction Set Architecture - ISA) آنها به دو دسته اصلی تقسیم کرد: CISC (کامپیوتر با مجموعه دستورالعملهای پیچیده) و RISC (کامپیوتر با مجموعه دستورالعملهای کاهشیافته). این دو، فلسفههای متفاوتی در طراحی پردازنده را نمایندگی میکنند.
CISC، که معماری x86 اینتل و AMD نمونه بارز آن است، بر این ایده استوار است که سختافزار باید کارهای پیچیده را انجام دهد. دستورالعملهای CISC میتوانند عملیات چندمرحلهای مانند خواندن از حافظه، انجام یک محاسبه و نوشتن نتیجه در حافظه را تنها در یک دستور انجام دهند. این باعث میشود کد کامپایل شده کوتاهتر و فشردهتر باشد، که در دوران حافظههای گرانقیمت یک مزیت بزرگ بود. با این حال، این پیچیدگی به معنای نیاز به مدارهای رمزگشایی پیچیدهتر و دشواری در پیادهسازی خط لوله کارآمد است.
RISC، که معماری ARM (مورد استفاده در تقریباً تمام گوشیهای هوشمند) پیشگام آن است، رویکرد مخالفی دارد. فلسفه RISC این است که با سادهسازی دستورالعملها میتوان سختافزار سادهتر و سریعتری ساخت. دستورالعملهای RISC ثابت، کوتاه و ساده هستند و هر کدام معمولاً تنها یک کار را انجام میدهند (مثلاً بارگذاری داده، ذخیره داده یا یک محاسبه). عملیات پیچیدهتر با ترکیب این دستورالعملهای ساده ساخته میشوند. این سادگی، طراحی خط لولههای عمیق و کارآمد و همچنین دستیابی به فرکانسهای ساعت بالاتر را آسانتر میکند.
جالب اینجاست که مرز بین این دو در حال محو شدن است. پردازندههای مدرن x86، که در ظاهر CISC هستند، در داخل دستورالعملهای پیچیده را به دنبالهای از عملیاتهای سادهتر شبیه به RISC به نام میکرو-عملیات (micro-ops) تجزیه میکنند. سپس این میکرو-عملیاتها در یک موتور اجرایی بسیار پیشرفته و شبیه به RISC، به صورت خارج از نوبت (Out-of-Order) اجرا میشوند. از سوی دیگر، معماری ARM نیز برای بهبود عملکرد، دستورالعملهای پیچیدهتری را اضافه کرده است.
مصالحه اصلی این است: CISC پیچیدگی را به سختافزار منتقل میکند تا کد فشردهتر شود، در حالی که RISC سختافزار را ساده نگه میدارد و این وظیفه را به کامپایلر واگذار میکند تا با تولید دستورالعملهای بیشتر، همان کار را انجام دهد.
معماری سوپراسکالر و اجرای خارج از نوبت
چگونه میتوان در هر چرخه ساعت بیش از یک دستورالعمل را اجرا کرد؟ پاسخ در معماری سوپراسکالر (Superscalar) نهفته است. یک پردازنده سوپراسکالر دارای چندین واحد اجرایی موازی است (مثلاً چندین واحد حساب و منطق، چندین واحد ممیز شناور و غیره). این به پردازنده اجازه میدهد تا دستورالعملهای مستقلی را که به یکدیگر وابستگی دادهای ندارند، به طور همزمان اجرا کند و به این ترتیب موازیسازی در سطح دستورالعمل (Instruction-Level Parallelism - ILP) را به حداکثر برساند.
اما برای اینکه این واحدهای اجرایی همیشه مشغول به کار باشند، پردازنده به یک جریان ثابت از دستورالعملهای مستقل نیاز دارد. اینجا است که تکنیک اجرای خارج از نوبت (Out-of-Order Execution - OoOE) وارد میشود. به جای اجرای دستورالعملها به همان ترتیبی که در برنامه نوشته شدهاند، یک موتور OoOE پنجرهای از دستورالعملهای آینده را بررسی میکند. این موتور دستورالعملهایی را که دادههای مورد نیازشان آماده است، پیدا کرده و آنها را برای اجرا به واحدهای اجرایی آزاد ارسال میکند، حتی اگر از دستورالعملهای قبلی که هنوز منتظر داده هستند، جلوتر باشند.
یک بافر ترتیبدهی مجدد (Re-order Buffer - ROB) اطمینان حاصل میکند که نتایج نهایی به همان ترتیبی که در برنامه اصلی انتظار میرود، به رجیسترها و حافظه نوشته شوند تا رفتار برنامه حفظ شود. این ترکیب از معماری سوپراسکالر و OoOE، هسته اصلی تقریباً تمام پردازندههای با عملکرد بالا، از x86 گرفته تا ARM، را تشکیل میدهد.
این تکنیکهای پیشرفته—خط لولههای عمیق، پیشبینی شاخه، اجرای حدسی و معماری سوپراسکالر—به پردازندههای مدرن اجازه میدهند تا با وجود محدودیتهای فیزیکی، عملکردی فراتر از اجرای ساده و متوالی دستورالعملها داشته باشند. درک این مفاهیم برای فهمیدن اینکه چرا برخی کدها سریعتر از بقیه اجرا میشوند و چگونه میتوان نرمافزار را برای سختافزار مدرن بهینه کرد، ضروری است.
تکنیک «پیشفرستی» (Forwarding) در خط لوله پردازنده برای حل کدام نوع از «خطر» (Hazard) به کار میرود؟
چرا پیشبینیکنندههای اشباع ۲ بیتی (2-bit Saturating Counters) در پیشبینی شاخه، عملکرد بهتری نسبت به پیشبینیکنندههای ۱ بیتی دارند؟

