عنوان مقاله اشاره بسیار کوتاه و مختصری به نحوه کار cpu میکنه. بریم که موشکافانه بررسیش کنیم.
خب همونطور که میدونید (یا نمیدونید) کلا cpu ها عجول هستن و مثل خیلی از ماها از صبر کردن متنفرن، این موضوع باعث یک موضوع دیگه میشه که قراره در این نوشته دقیق بهش بپردازیم.
بطور کلی عملکرد و سرعت cpu به شدت سریعه و پردازنده های امروزی میتونن در هر ثانیه چندین میلیارد چرخه انجام بدن. در هر چرخه هم ممکنه که چندین دستور با هم در حال اجرا شدن باشن. دقیقا مثل خط تولید یک کارخونه. یعنی در یک لحظه قطعه یک در حال تراشکاری هست، قطعه دو در حال رنگ شدن، قطعه سه در حال بستهبندی و… تا به اینجا که همچی خوب پیش رفته، ولی دقیقا نقطهایه که به یک مشکل اساسی میخوریم، یعنی Branch Hazard.
اینجاست که میرسیم به یکی از مهمترین چالشهای طراحی پردازندهها: Branch Hazard یا همون «خطر انشعاب».
این مشکل دقیقاً زمانی رخ میده که CPU مجبور بشه تصمیم بگیره در ادامهٔ pipeline چه دستوری رو اجرا کنه، اما نتیجهٔ یک شرط یا مقایسه که به اون وابسته هستیم هنوز آماده نیست.
به مثال زیر نگاه کنید:
load x; if x > 10: jump to A; else: jump to B; A: DoA(); B: DoB();
اینجا CPU یک دردسر جدی داره.
چرا؟
چون تا زمانی که مقدار x از حافظه خونده نشده باشه و مقایسهٔ x > 10 انجام نشه، CPU نمیدونه مسیر A اجرا میشه یا B.
حالا تصور کنید CPU بخواد صبر کنه تا نتیجهٔ load آماده بشه.
چی میشه؟
کل مسیر Fetch
کل مسیر Decode
کل مسیر Execute
و تمام stageهای بعدی
همگی باید خالی بمونن تا این شرط مشخص بشه.
این یعنی توقف کامل خط تولید؛ درست مثل اینکه کارخونه ناگهان دست از کار بکشه چون نمیدونه قطعه باید به کدوم بخش بره.
و این توقف دقیقاً همون Pipeline Stall معروفه.
چون هر stall به معنای از دست رفتن ۱۰ تا ۲۰ چرخهٔ پردازنده است.
و پردازندهها عاشق ایناند که هیچوقت بیکار نشن.
تصور کن یه CPU مدرن با ۵ گیگاهرتز فرکانس = پنج میلیارد چرخه در ثانیه.
از دست دادن فقط 20 چرخه = تقریباً 4ns هدر رفت
اما در یک حلقهٔ سنگین این اتفاق بارها و بارها رخ میدهد و میتونه performance رو نصف کنه.
اینجاست که پردازندهها یک رفتار فوقالعاده هوشمندانه نشون میدن:
حدس میزنن!
قبل از اینکه نتیجهٔ واقعی آماده بشه، CPU تصمیم میگیره:
احتمالاً مسیر A اجرا میشه یا B؟
اگر حدس درست باشه → همهچیز بدون هیچ توقفی جلو میره.
اگر اشتباه باشه → کل pipeline باید flush بشه و دوباره با مسیر درست پر بشه.
این اشتباه رو بهش میگن:
Branch Misprediction
و هزینهاش ممکنه 15 تا 30 cycle باشه.
خب علاوه بر هوشمندی cpu ما هم میتونیم کمکش کنیم تا تصمیمات بهتری بگیره، دراینجا چندین پیشنهاد و اصل مطرح میشه که خیلی میتونه کمک کننده باشه:
۱. مسیر رایج باید اول نوشته بشه
بد:
if unlikelyCondition { slowPath() } else { fastPath() }
خوب:
if likelyCondition { fastPath() } else { slowPath() }
۲. پرهیز از شرط های تودرتو
این کد به شدت کند خواهد بود:
if a { if b { if c { ... } } }
ولی این کد مناسب است:
if !a || !b || !c { return }
۳. استفاده از map بجای switch در شرایط خاص
بجای:
switch opcode { case 1: f1() case 2: f2() case 3: f3() ... }
از:
handlers := [...]func(){f0, f1, f2, f3} handlers[opcode]()
استفاده کنید.
۴. شرط های وابسته به یک مقدار رو پشت سر هم بررسی کنید
بد:
if x > 0 { if x < 10 { ... } }
خوب:
if x > 0 && x < 10 { ... }
۵. پرهیز از ایجاد branching درحلقه هایی که زیاد اجرا میشن
وقتی حلقه خیلی تکرار میشه، branching داخل اون دشمن Performance هست.
بد:
for i := 0; i < n; i++ { if arr[i] > 10 { ... } }
خوب: پیشفیلتر کردن یا pre-computation
filtered := make([]int, 0, n) for _, v := range arr { if v > 10 { filtered = append(filtered, v) } } for _, v := range filtered { ... }
ممنون که تا اینجا همراه بودید :)
