Foundationপ্রথম নীতি থেকে
LEVEL 3লেসন ১২/১৭কঠিন১ ঘণ্টা ৫ মিনিট

Branch Prediction — না জেনেই সঠিক অনুমান করা

Branch Prediction

Branch-এর ফলাফল না জেনেই CPU-কে fetch চালিয়ে যেতে হয় — তাই এটা একটা শিক্ষিত অনুমান করে, আর ভুল হলে পুরো speculatively-করা কাজ ফেলে দেয়; একটা 2-bit saturating counter এই অনুমানকে আশ্চর্যজনকভাবে নির্ভুল করে তোলে।

এই লেসন শেষে আপনি পারবেন

  • কেন control hazard forwarding দিয়ে সমাধান করা যায় না, শুধু prediction দিয়ে কমানো যায় তা ব্যাখ্যা করতে পারবেন
  • 1-bit predictor কোথায় ব্যর্থ হয় (loop-এর শেষ iteration) সেটা concrete উদাহরণে দেখাতে পারবেন
  • 2-bit saturating counter-এর state transition ট্রেস করে loop-এ কেন এটা ভালো করে তা প্রমাণ করতে পারবেন
  • Misprediction penalty pipeline depth-এর সাথে কীভাবে সম্পর্কিত তা হিসাব করতে পারবেন
  • Branch Target Buffer (BTB)-এর ভূমিকা ব্যাখ্যা করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনে আমরা data hazard সমাধান করেছি — forwarding দিয়ে, প্রায় সম্পূর্ণভাবে। কিন্তু pipelining-and-hazards লেসনে উল্লেখ করা তৃতীয় hazard-টা এখনও অমীমাংসিত: control hazard — একটা branch instruction-এর ফলাফল (নেওয়া হবে নাকি হবে না) জানার আগেই CPU-কে পরের instruction fetch করতে হয়। কিন্তু কোন instruction? Branch যদি না নেওয়া হয়, পরের sequential instruction; নেওয়া হলে, সম্পূর্ণ ভিন্ন একটা target address-এর instruction।

Forwarding এখানে কাজ করে না — এটা তো কোনো data value বহন করার সমস্যা না, এটা কোন instruction-ই পরে fetch হবে সেটা না জানার সমস্যা। আজ আমরা দেখব CPU কীভাবে এই অনিশ্চয়তার মুখোমুখি হয়ে একটা শিক্ষিত অনুমান করে — আর ভুল হলে কী মূল্য দিতে হয়।

মূল ধারণা

নাইভ সমাধান — এবং কেন সেগুলো যথেষ্ট না

সবচেয়ে সরল: Stall করুন। Branch resolve না হওয়া পর্যন্ত fetch বন্ধ রাখুন। সম্পূর্ণ সঠিক, কিন্তু pipelining-and-hazards লেসনের পুরো লাভ (প্রতি cycle-এ একটা instruction) নষ্ট — প্রতিটা branch-এ কয়েক cycle-এর bubble।

একটু ভালো: সবসময় “not-taken” ধরে নিন। Sequential পরের instruction fetch চালিয়ে যান, ধরে নিয়ে branch নেওয়া হবে না। সঠিক হলে কোনো penalty নেই। ভুল হলে (branch আসলে নেওয়া হয়েছে), যেসব instruction fetch/decode হয়ে গেছে সেগুলো বাতিল (flush) করে সঠিক target থেকে আবার শুরু করতে হয়।

1-bit Predictor — এবং তার নির্দিষ্ট ব্যর্থতা

একটা ছোট memory (branch history table) প্রতিটা branch instruction-এর শেষবারের ফলাফল মনে রাখে, আর একই ফলাফল আবার predict করে।

একটা loop কল্পনা করুন যা ১০ বার চলে (branch condition i < 10 প্রতিবার চেক হয়, শেষবার ছাড়া সবসময় true/taken):

Iteration:     1  2  3  4  5  6  7  8  9  10
প্রকৃত ফলাফল:  T  T  T  T  T  T  T  T  T  N   (T=taken, N=not-taken)
1-bit predict: ?  T  T  T  T  T  T  T  T  T
সঠিক?          -  ✓  ✓  ✓  ✓  ✓  ✓  ✓  ✓  ✗

প্রথম iteration ছাড়া বাকি সবগুলো সঠিক — একটা ১০-বার-চলা loop-এ মাত্র ১টা misprediction (শেষ iteration-এ, যেখানে loop শেষ হয়)। ভালোই মনে হচ্ছে।

সমস্যা কোথায়? এই একই loop যদি বাইরের আরেকটা loop-এর ভেতরে থাকে (nested loop, প্রতিবার bahirer loop-এ ভেতরের loop আবার শুরু হয়):

বাইরের loop-এর প্রতিটা iteration-এ ভেতরের loop আবার ১০ বার চলে:
...N(শেষ), T(নতুন শুরু), T, T, ..., N(শেষ), T(আবার শুরু), ...

প্রতিবার ভেতরের loop শেষ (N) হওয়ার পর predictor-এর state N-এ থাকে। ভেতরের loop আবার শুরু হলে প্রথম iteration আসলে T (taken) — কিন্তু predictor তখনও N predict করছে (গত ফলাফল মনে রেখে) — আরেকটা misprediction। প্রতিটা loop entry-তে ১টা, আর প্রতিটা loop exit-এ ১টা — মোট মিসপ্রেডিকশন দ্বিগুণ হয়ে যায় nested loop-এ।

ভেতরে কী ঘটছে

2-bit Saturating Counter — কেন একটা বাড়তি বিট এত সাহায্য করে

সমাধান: একটা মাত্র bit-এর বদলে দুইটা bit ব্যবহার করুন, চারটা state নিয়ে একটা FSM (digital-logic/finite-state-machines-এর সরাসরি প্রয়োগ):

        taken          taken          taken
       ┌────┐         ┌────┐         ┌────┐
       ▼    │         ▼    │         ▼    │
  ┌─────────┴──┐  ┌─────────┴──┐  ┌──────────┴─┐  ┌────────────┐
  │ Strongly    │  │ Weakly     │  │ Weakly     │  │ Strongly   │
  │ Not-Taken   ├─►│ Not-Taken  ├─►│ Taken      ├─►│ Taken      │
  │   (00)      │  │  (01)      │  │  (10)      │  │   (11)     │
  └──────┬──────┘  └──────┬─────┘  └──────┬─────┘  └──────┬─────┘
         │  not-taken     │  not-taken    │              │
         └────────────────┴───────────────┴──────────────┘
                     (প্রতিটা not-taken-এ একধাপ বামে)
2-bit saturating counter — টানা দুইবার ভুল না হলে prediction বদলায় না।

Prediction: state 10 বা 11 (Weakly/Strongly Taken) হলে predict taken; 00/01 হলে predict not-takenমূল কৌশল — prediction বদলাতে টানা দুইবার বিপরীত ফলাফল লাগে, একবার না।

একই nested-loop উদাহরণ পুনরায় করি, state 11 (Strongly Taken) থেকে শুরু করে ভেতরের loop-এর একটা সম্পূর্ণ execution ট্রেস করি:

Iterationপ্রকৃতState আগেPredictসঠিক?State পরে
1 (নতুন entry)T11Taken11
T11Taken11
শেষN11Taken10
(পরের entry) 1T10Taken11

লক্ষ্য করুন — loop শেষ হওয়ার misprediction state-কে 11 থেকে 10-তে নামায় (একধাপ), কিন্তু prediction তখনও Taken-ই থাকে (10 মানে Weakly Taken, তবু predict taken)। তাই পরের loop entry-তে (যেখানে প্রকৃত ফলাফল T) prediction সঠিক হয় — এই misprediction-টা এড়ানো গেছে, যেটা 1-bit predictor-এ এড়ানো যেত না।

উদাহরণ

Misprediction Penalty — Pipeline Depth-এর সাথে সম্পর্ক

যখন prediction ভুল হয়, pipeline-এ speculatively fetch/decode হওয়া সব instruction বাতিল (flush) করতে হয়, আর সঠিক target থেকে নতুন করে fetch শুরু করতে হয়।

একটা 5-stage pipeline-এ (IF/ID/EX/MEM/WB), যদি branch EX stage-এ resolve হয় (৩য় stage):

Cycle:        1    2    3    4    5
Branch:       IF   ID   EX(resolve)
পরের (ভুল):        IF   ID   flush!
পরের (ভুল):             IF   flush!
সঠিক target:                 IF   ID   EX ...

Misprediction penalty = ২ cycle (দুইটা ভুল-fetch করা instruction বাতিল হলো)।

Penalty=(branch resolve হওয়ার stage)1\text{Penalty} = (\text{branch resolve হওয়ার stage}) - 1

এখানেই pipeline depth-এর সাথে সরাসরি সংযোগ: একটা গভীর pipeline (যেমন Pentium 4-এর ~২০+ stage, computer-architecture/pipelining-and-hazards লেসনের ideal-speedup আলোচনার সরাসরি বিপরীত দিক) branch অনেক পরের stage-এ resolve করে — misprediction penalty অনেক বড় হয়ে যায় (কয়েক ডজন cycle পর্যন্ত)। এটাই Pentium 4-এর একটা বড় বাস্তব দুর্বলতা ছিল — গভীর pipeline উচ্চ clock speed দিত (ideal throughput বেশি), কিন্তু misprediction-এ বিশাল penalty। যখন branch-heavy কোড (যেমন বেশিরভাগ সাধারণ software) চলত, ঘন ঘন misprediction penalty সেই তাত্ত্বিক লাভ খেয়ে ফেলত — একটা বাস্তব কারণ কেন Intel পরবর্তী Core architecture-এ pipeline অনেক ছোট করে (~১৪ stage) IPC-কেন্দ্রিক ডিজাইনে ফিরে যায়।

নিজে চালিয়ে দেখুন

EXPERIMENT

নিজে হাতে 1-bit বনাম 2-bit predictor simulate করুন

Python 3· ১৫ মিনিট
def simulate_1bit(history):
    state = True  # শুরুতে ধরি Taken
    mispredicts = 0
    for actual in history:
        if state != actual:
            mispredicts += 1
        state = actual  # সবসময় শেষ ফলাফল মনে রাখে
    return mispredicts

def simulate_2bit(history):
    state = 3  # শুরু Strongly Taken (0..3)
    mispredicts = 0
    for actual in history:
        predict = state >= 2
        if predict != actual:
            mispredicts += 1
        if actual:
            state = min(3, state + 1)
        else:
            state = max(0, state - 1)
    return mispredicts

# একটা 10-iteration inner loop, 5 বার বাইরের loop-এ পুনরাবৃত্ত
inner_loop = [True]*9 + [False]     # 9 taken, শেষে 1 not-taken
history = inner_loop * 5

print("1-bit mispredicts:", simulate_1bit(history))
print("2-bit mispredicts:", simulate_2bit(history))

প্রত্যাশিত ফলাফল:

1-bit mispredicts: 9
2-bit mispredicts: 5

1-bit predictor প্রতিটা loop-entry (৪টা, প্রথমটা বাদে) আর প্রতিটা loop-exit (৫টা)-এ ভুল করে (মোট ৯টা)। 2-bit predictor শুধু loop-exit-এ ভুল করে (৫টা) — loop-entry-তে prediction “Weakly Taken” অবস্থাতেই থেকে যাওয়ায় সঠিক থাকে। প্রায় ৪৪% কম misprediction, একই simple 2-bit hardware দিয়ে।

এটা কী প্রমাণ করে

একই branch history-তে 2-bit predictor সত্যিই কম misprediction দেয়, বিশেষত nested-loop-এর মতো প্যাটার্নে।

নিজে বানান

BUILD IT

Branch Target Buffer (BTB)-সহ সম্পূর্ণ Predictor

Python · ●●●○○
  1. উপরের 2-bit predictor class আকারে লিখুন, প্রতিটা branch address-এর নিজস্ব state রাখতে একটা dictionary ব্যবহার করুন
  2. একটা BTB (dictionary: branch address to predicted target address) যোগ করুন
  3. একটা instruction trace simulate করুন (branch address, taken/not-taken, target address-এর তালিকা)
  4. মোট misprediction সংখ্যা, আর মোট wasted cycle (misprediction × penalty) হিসাব করুন
  5. Predictor-বিহীন (সবসময় stall) versus আপনার predictor-এর মোট cycle তুলনা করুন

BTB কেন লাগে তা বুঝুন: শুধু “নেওয়া হবে কি না” জানলেই যথেষ্ট না — কোথায় jump করতে হবে সেটাও জানা দরকার, decode স্টেজ পর্যন্ত অপেক্ষা না করেই। BTB predicted target address আগে থেকেই cache করে রাখে, তাই fetch stage-ই speculatively পরের instruction ঠিক জায়গা থেকে আনতে পারে।

class BranchPredictor:
    def __init__(self):
        self.counters = {}   # address -> 0..3
        self.btb = {}        # address -> predicted target

    def predict(self, addr):
        state = self.counters.get(addr, 3)
        taken = state >= 2
        target = self.btb.get(addr)
        return taken, target

    def update(self, addr, actual_taken, actual_target):
        state = self.counters.get(addr, 3)
        state = min(3, state+1) if actual_taken else max(0, state-1)
        self.counters[addr] = state
        if actual_taken:
            self.btb[addr] = actual_target

বাস্তব সিস্টেমে

Branch Prediction যেখানে সত্যিকারের পার্থক্য গড়ে

  • Spectre/Meltdown (২০১৮) — speculative execution আর branch prediction-এর সবচেয়ে কুখ্যাত real-world পরিণতি। CPU একটা branch prediction অনুযায়ী speculatively কাজ চালিয়ে যায়, ভুল হলে ফলাফল বাতিল করে — কিন্তু সেই speculative execution-এর side effect (cache-এ কী লোড হলো) থেকে গোপন তথ্য বের করা সম্ভব প্রমাণিত হয়েছিল — Level 10-এ (Security) বিস্তারিত।

  • Pentium 4-এর deep pipeline সমস্যা — এই লেসনের example section-এ বিস্তারিত।

  • Modern CPU-র two-level/correlating predictor — শুধু একটা branch-এর নিজের ইতিহাস না, সাম্প্রতিক অন্য branch-গুলোর ফলাফলও বিবেচনা করে (কারণ প্রায়ই branch-এর ফলাফল একে অপরের সাথে correlated) — Intel/AMD-র আধুনিক predictor-এ এমনকি neural network-ভিত্তিক (perceptron predictor) কৌশলও ব্যবহৃত হয়, ৯৫%+ accuracy পর্যন্ত পৌঁছাতে।

  • Compiler hint — কিছু ISA-তে (__builtin_expect in GCC/Clang) প্রোগ্রামার compiler-কে বলতে পারেন কোন branch সাধারণত taken — compiler সেই তথ্য দিয়ে code layout সাজায় (predict-not-taken path-কে “সোজা” রাখে), যদিও আধুনিক hardware predictor-ই বেশিরভাগ ভার বহন করে।

  • Loop unrolling-এর একটা লুকানো উপকার — একটা loop-কে “unroll” করলে (একই iteration-এর একাধিক কপি একসাথে) branch instruction-এর সংখ্যা কমে, ফলে misprediction-এর সুযোগও কমে — শুধু instruction-count কমানোর বাইরেও একটা বাস্তব কারণ।

যে ভুলগুলো সবাই করে

“Branch prediction ভুল হলে প্রোগ্রামের ফলাফল ভুল হয়ে যায়।”

না — misprediction শুধু performance সমস্যা, correctness সমস্যা না। CPU সবসময় নিশ্চিত করে যে speculatively করা ভুল কাজ কখনো architectural state (register, memory)-এ স্থায়ীভাবে প্রতিফলিত হয় না — ভুল প্রমাণিত হলে flush করে সঠিক path থেকে আবার শুরু করা হয় (computer-architecture/register-renaming-reorder-buffer-এর reorder buffer এই “শুধু সঠিক commit visible হয়” গ্যারান্টি দেয়)। ব্যবহারকারী শুধু ধীরগতি অনুভব করেন, ভুল উত্তর না।

“সবচেয়ে জটিল predictor সবসময় সবচেয়ে ভালো।”

জটিল predictor বেশি হার্ডওয়্যার (এলাকা, power) খরচ করে, আর misprediction-এর জন্য penalty পুরোপুরি দূর হয় না, শুধু কমে। সাধারণ প্রোগ্রামে বেশিরভাগ branch আসলে খুব predictable (একটা 2-bit predictor দিয়েই ৯০%+ accuracy পাওয়া যায়) — তাই ROI (return on hardware investment) একটা নির্দিষ্ট জটিলতার পরে কমতে শুরু করে। এটা এই মডিউল জুড়ে বারবার আসা সেই একই area-বনাম-benefit trade-off-এর আরেকটা রূপ।

বুঝেছেন কি না দেখুন

1

একটা branch-এর history: T, T, N, T, T, T, N, T। একটা 2-bit predictor state 11 (Strongly Taken) থেকে শুরু করলে, কতগুলো misprediction হবে? প্রতিটা ধাপ দেখান।

প্রয়োগ
ফলাফলState আগেPredictসঠিক?State পরে
T11Taken11
T11Taken11
N11Taken10
T10Taken11
T11Taken11
T11Taken11
N11Taken10
T10Taken11

মোট misprediction: ২টা (৩য় ও ৭ম অবস্থানে, দুইটা N-এই)। প্রতিবার N আসার সময় prediction ভুল হয়েছে (কারণ state কখনো 01 বা তার নিচে নামেনি একটা মাত্র N-এ), কিন্তু state প্রতিবার একধাপ কমেছে তারপর পরের T-তে আবার বেড়েছে — একটা 1-bit predictor হলে প্রতিটা N-এর পরের T-তেও একটা misprediction হতো (মোট ৪টা) — নিজে চেক করে দেখুন।

2

একটা branch যেটা সবসময় এলোমেলোভাবে (৫০-৫০) taken/not-taken হয় (কোনো প্যাটার্ন নেই), 2-bit predictor-এর accuracy কেমন হবে মোটামুটি? ব্যাখ্যা করুন।

যুক্তি

প্রায় ৫০% accuracy — কার্যত coin-flip-এর সমান, predictor যতই জটিল হোক না কেন।

এটা predictor-এর দুর্বলতা না — এটা তথ্যতাত্ত্বিক সীমা। যদি প্রকৃত sequence-এ কোনো predictable প্যাটার্ন না থাকে (সত্যিকারের এলোমেলো), কোনো predictor (এমনকি ভবিষ্যতের সবচেয়ে জটিল neural predictor) ৫০%-এর কাছাকাছি ছাড়া ভালো করতে পারে না — কারণ predict করার মতো কোনো “সংকেত” নেই।

এই কারণেই compiler/programmer-রা এমন কোড এড়িয়ে চলার চেষ্টা করেন যেখানে branch decision সত্যিকারের ডেটা-নির্ভর, এলোমেলো (যেমন একটা unsorted array-তে if (data[i] > threshold) র‍্যান্ডম ডেটায়) — এই ধরনের branch-কে branchless কোডে রূপান্তর করা (conditional move instruction ব্যবহার করে, কোনো branch-ই না রেখে) একটা বাস্তব optimization কৌশল ঠিক এই কারণে।

3

দুইটা CPU ডিজাইন — একটা ১০-stage pipeline, branch EX stage (stage ৩)-এ resolve করে; আরেকটা ২০-stage pipeline, branch stage ৮-এ resolve করে। যদি misprediction rate দুই ডিজাইনেই একই (১০%) হয়, কোনটা branch-heavy কোডে বেশি ক্ষতিগ্রস্ত হবে, আর কেন?

ডিজাইন

২০-stage ডিজাইন বেশি ক্ষতিগ্রস্ত।

Misprediction penalty = resolve stage - 1:

  • ১০-stage: penalty = 3 - 1 = 2 cycle
  • ২০-stage: penalty = 8 - 1 = 7 cycle

যদি ১০০০টা instruction-এ ১০০টা branch থাকে (১০%), আর প্রতিটার ১০% ভুল predict হয় (১০টা misprediction):

  • ১০-stage: 10 × 2 = 20 বাড়তি cycle
  • ২০-stage: 10 × 7 = 70 বাড়তি cycle

২০-stage ডিজাইন একই misprediction rate-এই সাড়ে তিন গুণ বেশি cycle হারায়। এটাই সরাসরি Pentium 4-এর ঐতিহাসিক সমস্যা — গভীর pipeline উচ্চ clock frequency দেয় (ideal single-instruction latency কম), কিন্তু branch-heavy বাস্তব কোডে (typical general-purpose software-এ প্রতি ৫-৭ instruction-এ একটা branch) এই penalty জমে সামগ্রিক performance-কে damage করে — যে কারণে Intel পরবর্তী Core-জেনারেশনে pipeline অনেক ছোট করে ফিরে যায়।

4

কেউ প্রস্তাব দেয়: “কেন predictor না বানিয়ে branch resolve না হওয়া পর্যন্ত দুইটা path-ই (taken ও not-taken) একসাথে speculatively execute করি না, তারপর সঠিকটা রাখি?” এই ধারণার ব্যবহারিক সমস্যা কী?

যুক্তি

এই কৌশলকে eager execution বলে, আর তাত্ত্বিকভাবে সম্ভব, কিন্তু বাস্তবে ব্যয়বহুল।

সমস্যা ১ — hardware দ্বিগুণ (বা exponentially বেশি)। প্রতিটা branch-এ দুইটা path মানে পরের branch-এও (প্রতিটা path-এর ভেতরে) আবার দুইটা — nested branch-এ exponential blow-up (2ⁿ path n branch-এর গভীরতায়)। বাস্তব কোডে কয়েকটা branch গভীরতার মধ্যেই এটা astronomically ব্যয়বহুল হয়ে যায় (digital-logic-এর area-বনাম-benefit trade-off-এর চরম রূপ)।

সমস্যা ২ — শক্তি খরচ। দুইটা (বা বেশি) path একসাথে চালানো মানে প্রায় দ্বিগুণ কাজ, যার অর্ধেক নিশ্চিতভাবে ফেলে দেওয়া হবে — বিদ্যুৎ অপচয়, যা মোবাইল/ব্যাটারি ডিভাইসে বিশেষভাবে অগ্রহণযোগ্য।

বাস্তবে যা হয়: কিছু সীমিত আকারে eager execution ব্যবহৃত হয় (শুধু সবচেয়ে “অনিশ্চিত” branch-এ, prediction confidence কম থাকলে) — কিন্তু সার্বজনীন সমাধান হিসেবে না, prediction-ই প্রধান কৌশল থেকে যায়, কারণ prediction-এর hardware খরচ (কয়েকটা bit প্রতি branch) eager execution-এর তুলনায় নগণ্য, আর accuracy যথেষ্ট বেশি (৯০%+) যে বাকি ১০%-এর penalty মেনে নেওয়াই সামগ্রিকভাবে সস্তা।

এরপর কী

Control hazard এখন predictable-ভাবে সামলানো — একটা 2-bit predictor বেশিরভাগ branch-এর ফলাফল আগেভাগে সঠিকভাবে অনুমান করতে পারে, আর BTB target address-ও আগে থেকে জানিয়ে রাখে।

কিন্তু এখনও একটা মৌলিক সীমাবদ্ধতা আছে: এখন পর্যন্ত আমরা ধরে নিয়েছি instruction-গুলো এক এক করে, program-এর ক্রম অনুযায়ীই pipeline-এ ঢোকে ও বের হয়। পরের লেসনে আমরা এই ধারণাটাকেই ভাঙব — একসাথে একাধিক instruction fetch/decode/execute করা (superscalar), আর এমনকি প্রয়োজনে program-এর মূল ক্রম ভেঙে (out-of-order) যেসব instruction প্রস্তুত সেগুলো আগে চালানো — আধুনিক high-performance CPU-র সবচেয়ে জটিল, সবচেয়ে শক্তিশালী কৌশল।

আরও পড়ুন

  • Computer Architecture: A Quantitative Approach, §3.3 — Branch Prediction — Hennessy & Patterson · 2-bit predictor ও তার পরিসংখ্যানগত যুক্তির প্রামাণ্য উৎস
  • A Study of Branch Prediction Strategies — James E. Smith (1981) · 2-bit saturating counter predictor-এর মূল পেপার