Cache Hierarchy — Inclusive, Exclusive, আর মাঝামাঝি
Cache Hierarchy: Inclusive vs Exclusive Design
একাধিক cache level (L1 private, L2 প্রায়ই private, L3 shared) একসাথে কাজ করার সময় একটা design সিদ্ধান্ত নিতে হয় — ইনক্লুসিভ (প্রতিটা outer level inner level-এর সব ধরে রাখে, coherence checking সহজ কিন্তু capacity নষ্ট), এক্সক্লুসিভ (প্রতিটা line ঠিক এক level-এ, বেশি effective capacity কিন্তু eviction জটিল), আর নন-ইনক্লুসিভ (বাস্তবসম্মত মাঝামাঝি) — এই সিদ্ধান্তই পরের লেসনের cache coherence protocol-এর কার্যকারিতাকে সরাসরি প্রভাবিত করে।
আগে এটা বুঝি
computer-architecture/cache-organization লেসন একটা একক cache level কীভাবে কাজ করে তা সম্পূর্ণভাবে ব্যাখ্যা করেছিল — একটা address কীভাবে tag/index/offset-এ ভাগ হয়, direct-mapped বনাম set-associative বনাম fully-associative। computer-architecture/memory-hierarchy লেসন তার আগে একটা latency table দিয়েছিল — register থেকে HDD পর্যন্ত প্রতিটা স্তরের আনুমানিক গতি। কিন্তু দুইটা লেসনই একটা প্রশ্ন পুরোপুরি এড়িয়ে গিয়েছিল, যেটা advanced-architecture/microarchitecture-overview-র শেষে আমরা ইচ্ছাকৃতভাবে খোলা রেখেছিলাম:
L1, L2, L3 — এই তিনটা level যখন একসাথে, একই CPU-তে, একই সময়ে কাজ করে, তাদের ভেতরের সম্পর্কটা ঠিক কী? একটা memory line যদি L1-এ থাকে, সেটা কি নিশ্চিতভাবে L2-তেও আছে? L3-এ? নাকি প্রতিটা level সম্পূর্ণ স্বাধীনভাবে নিজের জিনিস রাখে, একে অপরের থেকে আলাদা ডেটা নিয়ে?
এই প্রশ্নের উত্তরকে বলে inclusion policy (অন্তর্ভুক্তি নীতি) — আর এই লেসনের মূল দাবি: এই একটা design সিদ্ধান্ত শুধু “capacity efficiency”-র প্রশ্ন না। এটা সরাসরি নির্ধারণ করে দেয় পরের লেসনের বিষয় — cache coherence — কতটা সহজ বা কঠিন হবে বাস্তবায়ন করা, যখন একাধিক core একই memory শেয়ার করে। এই লেসনটাই সেই সেতু।
মূল ধারণা
বাস্তব multi-level hierarchy — সংখ্যায়
memory-hierarchy লেসনের latency table আমাদের একটা প্রথম ধারণা দিয়েছিল। এখন সেই সংখ্যাগুলোকে আরও বাস্তবসম্মত, নির্দিষ্ট করে দেখি — একটা আধুনিক multi-core CPU-র typical L1/L2/L3 বিন্যাস:
| Level | সাধারণত | typical আকার (per core, L3 ছাড়া) | typical latency | কার সাথে shared |
|---|---|---|---|---|
| L1 | split I-cache + D-cache | ৩২-৬৪ KB (প্রতিটা, তাই মোট ৬৪-১২৮ KB) | ~৪-৫ cycle | শুধু নিজের core |
| L2 | unified (instruction+data একসাথে) | ২৫৬ KB - ১ MB | ~১২-২০ cycle | সাধারণত শুধু নিজের core (কিছু ডিজাইনে cluster-shared) |
| L3 | unified | কয়েক থেকে কয়েক-দশ MB (মোট চিপে) | ~৪০-৭০ cycle | সব core-এর মধ্যে shared |
এই টেবিল memory-hierarchy লেসনের সংখ্যার সাথে সামঞ্জস্যপূর্ণ (L1 ~৪ cycle/~১.৩ ns, L2 ~১২ cycle/~৪ ns, L3 ~৪০ cycle/~১৩ ns), শুধু এখানে আকার-পরিসরটা আরও specific — আর সবচেয়ে গুরুত্বপূর্ণ নতুন তথ্য: L3 shared, L1/L2 সাধারণত private। এই একটা পার্থক্যই পরের লেসনের পুরো সমস্যার জন্ম দেবে — private cache মানে একই memory address-এর ভিন্ন ভিন্ন core-এর L1-এ ভিন্ন ভিন্ন copy থাকতে পারে, একই সময়ে।
বাস্তব চিপের নির্দিষ্ট সংখ্যা
| CPU | L1D (per core) | L2 (per core) | L3 (shared, মোট) |
|---|---|---|---|
| Intel Golden Cove | ~৪৮ KB | ~১.২৫-২ MB | ~৩০ MB (মডেলভেদে) |
| AMD Zen 4 | ৩২ KB | ১ MB | ৩২ MB (প্রতি CCD) |
| Apple M1 | ~১২৮ KB (অস্বাভাবিক বড়) | ~১২ MB (per cluster, shared) | কোনো traditional L3 নেই — বদলে একটা System Level Cache (SLC) |
Apple M1-এর গঠন বিশেষভাবে লক্ষণীয় — এটা L1/L2/L3-এর traditional তিন-স্তর মডেল হুবহু মানে না, বরং একটা ভিন্ন hierarchy shape ব্যবহার করে (realworld অংশে বিস্তারিত)। এটা এই লেসনের প্রথম ইঙ্গিত যে “L1/L2/L3” কোনো সার্বজনীন সত্য না, এটা একটা common pattern, যা থেকে বাস্তব ডিজাইনাররা প্রয়োজনমতো সরে যান।
Inclusion Policy — তিনটা সম্ভাব্য নকশা
এখন মূল প্রশ্নে আসি। একটা memory line যখন L1-এ আনা হয়, সেটা কি L2/L3-এও রাখা হয় (duplicate), নাকি সরিয়ে নেওয়া হয় (move), নাকি কোনো নিয়মই নেই?
১. Inclusive — বাইরের level সবসময় ভেতরেরটার superset
Inclusive hierarchy-তে একটা কড়া গ্যারান্টি থাকে: L1-এ যা কিছু আছে, তা অবশ্যই L2-তেও আছে; L2-তে যা কিছু আছে, তা অবশ্যই L3-তেও আছে।
অর্থাৎ একটা line L1-এ থাকলে, সেই একই ডেটার একটা কপি L2-তেও বসে আছে, আরেকটা কপি L3-তেও। ডেটা duplicate হচ্ছে একাধিক level জুড়ে।
সুবিধা: নিচের অংশে (hood) বিস্তারিত, কিন্তু সংক্ষেপে — কোনো একটা line কোনো core-এর private cache-এ আছে কি না তা জানতে পুরো hierarchy খোঁজার দরকার নেই, শুধু সবচেয়ে বাইরের (L3) level দেখলেই যথেষ্ট, কারণ inclusion গ্যারান্টি করে সেখানে না থাকলে ভেতরে কোথাও থাকতেই পারে না।
দাম: capacity নষ্ট। যদি L2 ১ MB আর L3 ৩২ MB হয়, L2-এর পুরো ১ MB-ই L3-তে আবার জায়গা নিচ্ছে — L3-এর কার্যকর, অনন্য (unique) capacity কমে যায় প্রতিটা core-এর L2-এর সমান পরিমাণে (একাধিক core থাকলে, প্রতিটার L2-এর সমষ্টি)।
২. Exclusive — প্রতিটা line ঠিক এক জায়গায়
Exclusive hierarchy-তে বিপরীত নিয়ম: একটা line ঠিক একটা level-এ থাকে, কখনোই একাধিক level-এ একসাথে না।
L1-এ একটা line থাকলে, সেই ডেটা L2/L3-তে নেই — তারা একে অপরের সম্পূর্ণ আলাদা, অ-পুনরাবৃত্ত অংশ ধরে রাখে।
সুবিধা: কোনো অপচয় নেই। L1+L2+L3-এর মোট capacity-ই কার্যকর, অনন্য ডেটার জন্য ব্যবহৃত হয় — উপরের উদাহরণে ১ MB + ৩২ MB = ৩৩ MB সবটাই ভিন্ন ডেটা ধরে রাখতে পারে।
দাম: eviction জটিল। L1 থেকে একটা line miss করে L2-তে পাওয়া গেলে (L1 miss, L2 hit), সেই line-টাকে শুধু কপি করলেই চলবে না (তাহলে দুই জায়গায় একসাথে থাকবে, exclusive নিয়ম ভেঙে যাবে) — সেটাকে L2 থেকে সরিয়ে (move) L1-এ আনতে হয়, L2-তে আর থাকবে না। এই “সরানো” প্রক্রিয়া (swap) বাস্তবায়ন করতে বাড়তি bus transaction আর নিয়ন্ত্রণ-যুক্তি (control logic) লাগে — inclusive-এ যেখানে “শুধু কপি রেখে দাও, বাদ দিয়ো না” যথেষ্ট ছিল।
৩. Non-inclusive — বাস্তবসম্মত মাঝামাঝি
Non-inclusive (পুরো নাম প্রায়ই “NINE” — Non-Inclusive Non-Exclusive) hierarchy-তে কোনো গ্যারান্টিই নেই — একটা line L1-এ থাকতে পারে, L2-তেও থাকতে পারে, দুইটাতেই থাকতে পারে, অথবা শুধু L2-তে থাকতে পারে — hardware কোনো কড়া নিয়ম বজায় রাখে না।
সুবিধা: inclusive-এর মতো জোর করে duplicate রাখতে হয় না (তাই কিছুটা capacity বাঁচে), আবার exclusive-এর মতো প্রতিটা miss-এ বাধ্যতামূলক move করতে হয় না (সরল replacement — miss হলে শুধু নতুন লাইন আনো, পুরনোটা থাকুক বা না থাকুক ভেতরের level-এ, কেউ পরোয়া করে না)।
দাম: coherence checking-এর জন্য inclusive-এর “শুধু L3 দেখলেই যথেষ্ট” সুবিধা হারিয়ে যায় (নিচের hood অংশে বিস্তারিত) — একটা আলাদা, explicit tracking কাঠামো (snoop filter/directory) লাগে সেই সুবিধা ফিরে পেতে।
INCLUSIVE EXCLUSIVE NON-INCLUSIVE
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ L3: A B C D E F │ │ L3: C D E F │ │ L3: A C D E F │
│ L2: A B C │◄─কপি │ L2: A B │◄─move │ L2: A B │ কোনো
│ L1: A │◄─কপি │ L1: A │◄─move │ L1: A │ গ্যারান্টি নেই
└─────────────────┘ └─────────────────┘ └─────────────────┘
A সব জায়গায় duplicate A শুধু L1-এ, অন্য কোথাও না A দুই জায়গায়, কিন্তু
L1⊆L2⊆L3 কড়াভাবে সত্য প্রতিটা line ঠিক এক জায়গায় সেটা নিয়মের অংশ না, কাকতালভেতরে কী ঘটছে
কেন inclusion policy coherence-এর জন্য এত গুরুত্বপূর্ণ
এই অংশটাই এই লেসনের কেন্দ্রীয় প্রকৌশলগত যুক্তি, আর পরের লেসনের (cache-coherence-mesi) ভিত্তি।
কল্পনা করুন একটা ৮-core চিপ, প্রতিটা core-এর নিজস্ব private L1+L2, একটা shared L3। Core A যখন memory address X-এ লিখতে চায়, coherence protocol-কে (পরের লেসনের বিষয়) নিশ্চিত করতে হবে অন্য কোনো core-এর private cache-এ X-এর পুরনো (stale) কপি নেই — থাকলে সেটা invalidate করতে হবে।
নিষ্পাপ (naive) সমাধান — সবাইকে জিজ্ঞেস করা
সবচেয়ে সরল উপায়: A প্রতিটা অন্য core-কে (B, C, D, E, F, G, H) জিজ্ঞেস করবে “তোমার L1/L2-তে X আছে কি?” — এটাকে বলে broadcast snooping। কিন্তু এটা core সংখ্যার সাথে সরাসরি স্কেল করে — ৮ core মানে ৭টা প্রশ্ন প্রতিটা লেখার জন্য, ৬৪ core মানে ৬৩টা প্রশ্ন। বাস্তব বড় multi-core/multi-socket সিস্টেমে এই broadcast ট্রাফিক দ্রুত অসহনীয় হয়ে ওঠে।
Inclusive L3-এর সমাধান — “L3 দেখলেই যথেষ্ট”
যদি L3 inclusive হয় ( সব core জুড়ে), তাহলে একটা চমৎকার logical শর্টকাট পাওয়া যায়:
যদি X L3-তে না থাকে, X নিশ্চিতভাবে কোনো core-এর L1/L2-তেও নেই।
কারণ — inclusion গ্যারান্টি করে L1/L2-তে যা কিছু আছে তার সবই L3-তে থাকতেই হবে; তাই বিপরীতক্রমে, L3-তে না থাকলে ভেতরে কোথাও থাকা যুক্তিগতভাবে অসম্ভব। Core A-কে তাই আর সবাইকে জিজ্ঞেস করতে হয় না — শুধু একবার L3 চেক করলেই যথেষ্ট।
আর যদি X L3-তে থাকে? তখনও পুরো broadcast লাগে না — বাস্তব inclusive ডিজাইনে প্রতিটা L3 line-এর সাথে অতিরিক্ত কিছু বিট রাখা হয়, যাকে বলে presence bits (বা core-valid bits) — প্রতিটা core-এর জন্য ১ বিট, বলে দেয় “এই core-এর private cache-এ এই line-এর কপি থাকতে পারে কি না”। A শুধু সেই presence bits পড়ে বলতে পারে ঠিক কোন কোন core-কে invalidate message পাঠাতে হবে — বাকিদের বিরক্ত করার দরকার নেই।
- Core A লিখতে চায় ঠিকানা X-এনিশ্চিত হতে হবে অন্য কোনো core-এর কাছে stale কপি নেই
- L3-এ X-এর entry চেকinclusion গ্যারান্টি: এখানে না থাকলে কোথাও নেই
- যদি L3-এ না থাকেসরাসরি নিশ্চিত — কোনো broadcast লাগবে না, A নিরাপদে লিখতে পারে
- যদি L3-এ থাকে — presence bits পড়াধরুন bits বলছে শুধু core C আর F-এর কাছে সম্ভাব্য কপি আছে
- শুধু C আর F-কে targeted invalidateবাকি ৫টা core-কে (B, D, E, G, H) কোনো বার্তাই পাঠানো হয় না
দামের আরেকটা দিক — Back-invalidation
Inclusion বজায় রাখতে গেলে একটা কম-আলোচিত কিন্তু বাস্তব দাম আছে। যদি L3 (capacity সীমিত থাকায়) একটা line evict করতে বাধ্য হয়, inclusion নিয়ম রক্ষা করতে সেই একই line-এর কপি যদি কোনো core-এর private L1/L2-তে থেকে থাকে, সেটাকেও জোর করে invalidate করতে হয় — এমনকি যদি সেই core এখনো সক্রিয়ভাবে line-টা ব্যবহার করছে! এটাকে বলে back-invalidation।
এটা একটা প্রকৃত পারফরম্যান্স-দাম — একটা core-এর জন্য এখনো “গরম” (hot, ঘন ঘন ব্যবহৃত) একটা L1 line শুধু এই কারণে হারিয়ে যেতে পারে যে L3 (যেটা অনেক বেশি core-এর ডেটা একসাথে ধরে রাখে, তাই বেশি চাপে থাকে) জায়গার প্রয়োজনে সেটা বাদ দিয়েছে। এটাই একটা বড় কারণ কেন কিছু আধুনিক server-grade ডিজাইন (realworld অংশে বিস্তারিত) কড়া inclusive থেকে সরে non-inclusive-এ চলে গেছে।
Victim Cache — একটা ছোট, চতুর মধ্যপন্থা
cache-organization লেসনের realworld অংশে সংক্ষেপে উল্লেখ করা হয়েছিল একটা victim cache-এর কথা — এখানে একটু বিস্তারিত করি। এটা একটা ছোট (সাধারণত মাত্র ৪-১৬ entry), fully-associative বাফার, যেটা L1 থেকে সদ্য evict হওয়া line-গুলো সাময়িকভাবে ধরে রাখে, তারা L2-তে পৌঁছানোর আগে বা পাশাপাশি।
উদ্দেশ্য: যদি L1 (প্রায়ই low-associativity বা এমনকি direct-mapped, latency budget-এর কারণে, cache-organization-এর আলোচনা মনে করুন) একটা conflict miss-এ কোনো line evict করে ফেলে, কিন্তু সেই line অচিরেই আবার দরকার পড়ে (যা conflict miss-এ প্রায়ই ঘটে, কারণ সমস্যাটা capacity না, শুধু index-সংঘর্ষ), victim cache-এ সেটা এখনো পাওয়া যেতে পারে — পুরো L2/L3 পর্যন্ত না গিয়েই। এটা exclusive hierarchy-র কিছুটা সুবিধা (recently-evicted ডেটা হারিয়ে না যাওয়া) দেয়, fully exclusive ডিজাইনের পূর্ণ জটিলতা ছাড়াই — শুধু একটা ছোট, সস্তা বাফারের মাধ্যমে।
Snoop Filter — inclusion ছাড়াই coherence-শর্টকাট ফিরে পাওয়া
Non-inclusive hierarchy inclusion-এর “L3 দেখলেই যথেষ্ট” সুবিধা হারায় (পরের লেসনের বিষয়, কিন্তু এখানেই ভিত্তিটা বোঝা দরকার)। কিন্তু coherence-checking সহজ রাখার চাহিদা তো থেকেই যায় — সমাধান একটা আলাদা, dedicated snoop filter (কখনো “directory” নামেও পরিচিত) কাঠামো।
Snoop filter মূলত inclusive L3-এর presence-bits ধারণাটাকে cache থেকে আলাদা করে ফেলে — এটা নিজে কোনো ডেটা রাখে না, শুধু “কোন address কোন core-এর private cache-এ থাকতে পারে” এই তথ্যটা একটা compact টেবিলে রাখে। ফলাফল: inclusion-এর capacity-দাম (ডেটা duplicate রাখা) ছাড়াই, coherence-checking-এর গতির সুবিধা (broadcast এড়ানো) ধরে রাখা যায় — দামটা এখন শুধু এই ছোট directory-র জন্য বাড়তি silicon area, পুরো L3-এর একটা অংশ উৎসর্গ করার বদলে।
উদাহরণ
সংখ্যায় — effective capacity-র পার্থক্য
ধরুন একটা ৮-core চিপ, প্রতিটা core-এর L2 = ১ MB, shared L3 = ৩২ MB।
Inclusive hierarchy-তে (worst-case ধরে, প্রতিটা core-এর পুরো L2 ভরা এবং সবই L3-তেও duplicate থাকতে বাধ্য):
লক্ষ্য করুন — L2-এর ডেটা L3-তেও আছে বলে, মোট unique capacity আসলে L3-এর আকারের সমানই থেকে যায় (৩২ MB), L2-এর ৮ MB নতুন কিছু যোগ করে না worst-case-এ, কারণ সেটা শুধু L3-এর একটা অংশের প্রতিফলন।
Exclusive hierarchy-তে (একই আকার ধরে, কোনো duplication নেই):
পার্থক্য: বাড়তি কার্যকর capacity, exclusive ডিজাইনে, ঠিক একই hardware আকারে — কোনো বাড়তি transistor ছাড়াই, শুধু নীতি পরিবর্তন করে। এটাই ঠিক সেই যুক্তি যা AMD তাদের Zen-গোত্রের L2/L3 সম্পর্ককে (মূলত exclusive/victim-ধাঁচের, realworld অংশে বিস্তারিত) ডিজাইন করার সময় ব্যবহার করেছে — বিশেষত 3D V-Cache-এর মতো ডিজাইনে, যেখানে L3-এর প্রতিটা অতিরিক্ত MB প্রকৃতপক্ষে নতুন, অনন্য ডেটা ধরে রাখা জরুরি, শুধু L2-এর প্রতিধ্বনি না।
একটা L1 miss-এর সম্পূর্ণ ট্রেস, inclusion policy-সহ
- CPU address X চায়L1 miss — X নেই L1-এ
- L2 lookupX পাওয়া গেল L2-তে (hit)
- INCLUSIVE হলেX-এর একটা কপি L1-এ বসানো হয়; L2-এর কপি থেকেই যায় — কোনো removal নেই
- EXCLUSIVE হলেX L2 থেকে সরিয়ে (move) L1-এ আনা হয় — L2-তে X আর থাকবে না, একটা বাড়তি bus/control transaction লাগে
- NON-INCLUSIVE হলেবাস্তবায়ন-নির্ভর — কপি রাখতেও পারে, নাও পারে; কোনো hardware গ্যারান্টি নেই কোনটা হবে
নিজে চালিয়ে দেখুন
নিজের মেশিনে L1/L2/L3-এর আসল সীমানা মাপুন — cache latency ladder
ধারণা: ক্রমান্বয়ে বড় হতে থাকা array-তে বারবার sequential access করে, প্রতিটা আকারে গড় per-access সময় মাপা — যেখানে array cache boundary পার হবে, সেখানেই latency-তে একটা স্পষ্ট “লাফ” দেখা যাবে।
import time
import array
def measure(size_kb, iterations=2_000_000):
n = (size_kb * 1024) // 8
buf = array.array('q', [0] * n)
stride = 8 # প্রতিটা int64 (৮ বাইট), একটানা sequential access
idx = 0
total = 0
t0 = time.perf_counter()
for _ in range(iterations):
total += buf[idx % n]
idx += 1
dt = time.perf_counter() - t0
return dt / iterations
sizes_kb = [8, 16, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768]
print(f"{'আকার (KB)':\<12}{'প্রতি-access সময় (ns)':\<20}")
for s in sizes_kb:
t = measure(s)
print(f"{s:\<12}{t * 1e9:.2f}")প্রত্যাশিত ফলাফলের ধরন (হাইপোথেটিক্যাল, একটা ৪৮KB L1D / ১.২৫MB L2 / ৩০MB shared L3 মেশিনে):
আকার (KB) প্রতি-access সময় (ns)
8 0.35
16 0.36
32 0.38
48 0.40 ← L1 সীমার কাছাকাছি
64 1.20 ← প্রথম লাফ: L1 → L2
128 1.35
512 1.40
1024 1.45 ← L2 সীমার কাছাকাছি
2048 4.10 ← দ্বিতীয় লাফ: L2 → L3
16384 4.50
32768 5.20 ← L3 সীমার কাছাকাছি
65536 18.00 ← তৃতীয় লাফ: L3 → DRAMদুইটা স্পষ্ট “লাফ” পাওয়া উচিত — একটা L1→L2 সীমানায়, একটা L2→L3 সীমানায়, আর (যথেষ্ট বড় array দিলে) একটা তৃতীয়টা L3→DRAM সীমানায় (memory-hierarchy লেসনের experiment-এর সরাসরি সম্প্রসারণ, এবার নির্দিষ্টভাবে তিনটা on-chip level আলাদা করে চিহ্নিত করার জন্য)। নিজের মেশিনে lscpu বা (Linux-এ) cat /sys/devices/system/cpu/cpu0/cache/index*/size দিয়ে প্রকৃত আকার বের করে মিলিয়ে দেখুন — লাফগুলো ঠিক আপনার CPU-র নির্দিষ্ট L1/L2/L3 আকারের কাছাকাছি ঘটা উচিত।
Python-এর overhead precise cycle-level পরিমাপ ঢেকে দিতে পারে; C-তে (clock_gettime দিয়ে, memory-hierarchy লেসনের experiment-এর C কোডের ধাঁচে) পার্থক্য আরও তীক্ষ্ণভাবে দেখা যাবে।
একটা বাস্তব CPU-তে ঠিক তিনটা (বা চারটা, DRAM-সহ) latency 'ধাপ' পাওয়া যায়, যেগুলোর আকার আপনার নিজের CPU-র প্রকৃত L1/L2/L3 আকারের সাথে মেলে — এই লেসনের সংখ্যাগুলো তত্ত্ব না, পরিমাপযোগ্য বাস্তবতা।
নিজে বানান
Inclusive বনাম Exclusive — একটা toy দুই-level cache simulator
- একটা L1 (ছোট, ৪ entry) আর একটা L2 (বড়, ১৬ entry) dict-ভিত্তিক cache বানান — উভয়ই LRU replacement ব্যবহার করবে
- একটা INCLUSIVE mode লিখুন — L1-এ কোনো line আনলে সেটা L2-তেও রাখুন (কপি); L1 evict করলে L2 অপরিবর্তিত থাকুক; L2 evict করলে L1 থেকেও সেই line জোরপূর্বক সরান (back-invalidation)
- একটা EXCLUSIVE mode লিখুন — L1-এ কোনো line আনলে সেটা L2 থেকে সরান (যদি সেখানে থাকে); L1 evict করলে সেটা L2-তে ফিরিয়ে দিন (move-back)
- একই memory access trace (একটা address sequence) দুই mode-এ চালান, প্রতিটাতে গণনা করুন মোট hit/miss সংখ্যা আর "duplicate storage" (inclusive-এ) বনাম "move সংখ্যা" (exclusive-এ)
- একটা working-set আকারের trace দিয়ে দেখান exclusive mode একই hardware আকারে বেশি hit rate দেয়, বিনিময়ে বেশি অভ্যন্তরীণ move-অপারেশন লাগে
এই simulator ছোট আকারে ঠিক এই লেসনের central trade-off সংখ্যায় দেখাবে — inclusive mode-এ কম hit rate (কম কার্যকর capacity) কিন্তু সরল eviction, exclusive mode-এ বেশি hit rate কিন্তু প্রতিটা L1↔L2 movement-এ বাড়তি বুককিপিং।
from collections import OrderedDict
class TinyCache:
def __init__(self, capacity):
self.capacity = capacity
self.store = OrderedDict() # address -> True, LRU order বজায় রাখতে
def has(self, addr):
return addr in self.store
def touch(self, addr):
if addr in self.store:
self.store.move_to_end(addr)
def insert(self, addr):
self.store[addr] = True
self.store.move_to_end(addr)
evicted = None
if len(self.store) > self.capacity:
evicted, _ = self.store.popitem(last=False)
return evicted
def remove(self, addr):
if addr in self.store:
del self.store[addr]
def run_inclusive(trace, l1_cap=4, l2_cap=16):
l1, l2 = TinyCache(l1_cap), TinyCache(l2_cap)
hits = misses = duplicates = 0
for addr in trace:
if l1.has(addr):
l1.touch(addr); l2.touch(addr); hits += 1
elif l2.has(addr):
hits += 1
l2.touch(addr)
evicted_from_l1 = l1.insert(addr) # L1-এ কপি, L2-তে থেকেই যায়
duplicates += 1
else:
misses += 1
l2.insert(addr)
evicted_from_l1 = l1.insert(addr) # নতুন line দুই জায়গাতেই
duplicates += 1
return hits, misses, duplicates
def run_exclusive(trace, l1_cap=4, l2_cap=16):
l1, l2 = TinyCache(l1_cap), TinyCache(l2_cap)
hits = misses = moves = 0
for addr in trace:
if l1.has(addr):
l1.touch(addr); hits += 1
elif l2.has(addr):
hits += 1
l2.remove(addr) # L2 থেকে সরানো (move)
evicted_from_l1 = l1.insert(addr)
if evicted_from_l1 is not None:
l2.insert(evicted_from_l1) # L1 থেকে বাদ পড়া line L2-তে ফিরে যায়
moves += 1
else:
misses += 1
evicted_from_l1 = l1.insert(addr)
if evicted_from_l1 is not None:
l2.insert(evicted_from_l1)
return hits, misses, moveshits/misses-এর পার্থক্য ছোট trace-এ নাও দেখা যেতে পারে (dataset যথেষ্ট বড় আর working-set L1+L2 সীমার কাছাকাছি না হলে) — একটা ইচ্ছাকৃতভাবে “L1+L2-এর সমান working set, বারবার পুনরাবৃত্ত” trace বানিয়ে (যেমন ২০টা distinct address, বারবার cycle করে) পার্থক্যটা স্পষ্ট করে তুলুন।
বাস্তব সিস্টেমে
বাস্তব CPU-তে inclusion policy-র সিদ্ধান্ত
-
Intel Sandy Bridge থেকে Skylake client (২০১১-২০১৭ ঘরানা) — কড়াভাবে inclusive L3, প্রতি-line presence bits-সহ, ঠিক এই লেসনের hood অংশে বর্ণিত কৌশল ব্যবহার করে coherence snoop ট্র্যাফিক কমানোর জন্য। একটা দীর্ঘ-স্থায়ী, প্রমাণিত ডিজাইন সিদ্ধান্ত।
-
Intel Skylake-SP / Ice Lake-SP (server, ~২০১৭ থেকে) — সরে গেছে non-inclusive L3-তে, সাথে একটা আলাদা, explicit snoop filter কাঠামো (presence-tracking-এর কাজ এখন inclusion-এর বদলে একটা dedicated structure করে)। কারণ: server চিপে core সংখ্যা অনেক বেশি (৩০+ পর্যন্ত), তাই প্রতিটা core-এর L2 (যথেষ্ট বড়, ~১ MB+) L3-তে বাধ্যতামূলক duplicate রাখলে capacity নষ্টের পরিমাণ আর সহনীয় থাকে না — এই লেসনের example অংশের ৮ MB নষ্টের হিসাব আরও অনেক core-এ আরও বড় আকারে ঘটত।
-
AMD Zen পরিবার (Zen থেকে Zen 4) — L2 আর L3-এর সম্পর্ক মূলত victim/mostly-exclusive ধাঁচের — একটা line সাধারণত প্রথমে L2-তে থাকে, L2 থেকে evict হলেই L3-তে যায় (victim cache-এর একটা বড় আকারের প্রয়োগ, যেমনটা hood অংশে আলোচিত)। এটা AMD-কে তুলনামূলক ছোট L2 রেখেও বড়, কার্যকর, প্রায়-সম্পূর্ণ-unique L3 capacity দিতে সাহায্য করে।
-
AMD 3D V-Cache (Zen 3/4 X3D চিপ) — L3-এর উপর সরাসরি আরেকটা সিলিকন স্তর stack করে বিশাল L3 (যেমন ৯৬ MB পর্যন্ত) যোগ করা — এই বিনিয়োগ সবচেয়ে বেশি লাভজনক হয় ঠিক তখনই যখন সেই বাড়তি L3 capacity প্রকৃতপক্ষে নতুন, অনন্য ডেটা ধরে রাখে (mostly-exclusive ডিজাইনের সরাসরি সুবিধা) — যদি এটা inclusive হতো আর শুধু L2-এর প্রতিধ্বনি বহন করত, এই বিশাল বিনিয়োগের একটা বড় অংশ নষ্ট হয়ে যেত।
-
Apple M-সিরিজ — traditional L1/L2/L3 কাঠামো থেকে সরে গিয়ে একটা per-cluster shared L2 (CPU core-দের একটা গুচ্ছ একটাই বড় L2 শেয়ার করে) আর একটা চিপ-ব্যাপী System Level Cache (SLC) ব্যবহার করে, যা CPU, GPU, Neural Engine সবার মধ্যে shared — একটা সম্পূর্ণ ভিন্ন hierarchy shape, প্রমাণ করে “L1/L2/L3” কোনো বাধ্যতামূলক টেমপ্লেট না।
-
ARM DynamIQ/big.LITTLE cluster — সাধারণত প্রতিটা cluster-এর নিজস্ব private L2 (per-core) আর একটা cluster-shared L3, বেশিরভাগ ডিজাইনে non-inclusive বা exclusive-ধাঁচের সম্পর্ক, ARM নিজে vendor-দের সুনির্দিষ্ট নীতি বেছে নেওয়ার স্বাধীনতা দেয় (ISA-র মতো coherence policy-ও ARM কড়াভাবে নির্দিষ্ট করে না, বরং একটা framework দেয়)।
-
TLB, আবারও fully-associative-এর উদাহরণ —
cache-organizationলেসনে TLB-কে fully-associative-এর একটা বাস্তব উদাহরণ হিসেবে দেখানো হয়েছিল; একইভাবে multi-level TLB hierarchy-তেও (L1 TLB, L2 TLB) inclusion policy-র প্রশ্ন ওঠে, একই যুক্তিতে — যদিও TLB miss-এর দাম (page table walk) এত বেশি যে বেশিরভাগ ডিজাইন এখানে সরলতা (non-inclusive) বেছে নেয়।
যে ভুলগুলো সবাই করে
“Inclusive cache মানে মোট usable capacity সবসময় L1+L2+L3-এর সরল যোগফল।”
উল্টো — inclusive design-এ মোট কার্যকর, অনন্য (unique) capacity প্রায়ই শুধু সবচেয়ে বড় (বাইরের) level-এর সমান, কারণ ভেতরের level-গুলোর ডেটা বাইরের level-এরই একটা subset/প্রতিফলন, নতুন কিছু যোগ করে না worst-case-এ। এই লেসনের example অংশের হিসাব (৩২ MB, যোগফল ৪০ MB না) এটা সরাসরি দেখায়। Exclusive design-এই বরং প্রকৃত যোগফল (L1+L2+L3) কার্যকর capacity হিসেবে পাওয়া যায়।
“Exclusive cache স্ট্রিক্টলি বেশি ভালো, তাই সব আধুনিক CPU exclusive ব্যবহার করা উচিত।”
Exclusive design বেশি effective capacity দেয়, ঠিক — কিন্তু বিনিময়ে প্রতিটা L1↔L2 miss/hit-এ বাড়তি “move” অপারেশন (আর তার নিয়ন্ত্রণ-যুক্তি জটিলতা) লাগে, আর সবচেয়ে গুরুত্বপূর্ণ — inclusive-এর “শুধু L3 দেখলেই যথেষ্ট” coherence-শর্টকাট হারিয়ে যায়। Intel দীর্ঘদিন client CPU-তে inclusive বেছে নিয়েছিল ঠিক এই coherence-সরলতার জন্য, capacity-অপচয় জেনেও। এটা একটা প্রকৃত trade-off, কোনো একমুখী “ভালো/খারাপ” রায় না — বাস্তবে core সংখ্যা, workload ধরন, আর coherence ট্র্যাফিকের গুরুত্ব অনুযায়ী সিদ্ধান্ত ভিন্ন হয় (Intel-এর নিজেরই client বনাম server ডিজাইনে ভিন্ন সিদ্ধান্ত এর প্রমাণ)।
“Inclusion policy শুধু capacity-efficiency-র প্রশ্ন, coherence protocol-এর সাথে সরাসরি সম্পর্ক নেই — সেগুলো আলাদা বিষয়।”
এই লেসনের সবচেয়ে গুরুত্বপূর্ণ সংশোধন। Inclusion policy-র মূল ঐতিহাসিক ও প্রকৌশলগত কারণই হলো coherence checking সহজ করা — hood অংশের presence-bits যুক্তি দেখায় ঠিক কীভাবে inclusive L3 broadcast snoop এড়িয়ে targeted invalidate সম্ভব করে। Capacity-efficiency একটা গুরুত্বপূর্ণ পাল্টা-বিবেচনা, কিন্তু coherence-সরলতাই মূল চালিকাশক্তি ছিল বহু বছরের inclusive-design সিদ্ধান্তের পেছনে — পরের লেসন (cache-coherence-mesi) এই সংযোগ সম্পূর্ণভাবে ব্যাখ্যা করবে।
বুঝেছেন কি না দেখুন
1তিনটা inclusion policy-র নাম বলুন এবং প্রতিটার একটা এক-লাইনের সংজ্ঞা দিন।
স্মরণ
Inclusive — একটা outer level (L2, L3) সবসময় তার সব inner level-এর (L1, L2) সব line-এর কপি ধরে রাখে: ।
Exclusive — একটা line ঠিক একটা level-এ থাকে, কখনো একাধিক level-এ একসাথে না; L1↔L2-এর মধ্যে ডেটা move হয়, কপি না।
Non-inclusive — কোনো কড়া গ্যারান্টি নেই; একটা line দুই জায়গাতেও থাকতে পারে, এক জায়গাতেও থাকতে পারে — hardware কোনো নিয়ম বজায় রাখে না।
2একটা inclusive L3-তে একটা line X evict হয়ে গেল capacity চাপের কারণে। Core ৩-এর L1-এ তখনও X সক্রিয়ভাবে ব্যবহৃত হচ্ছিল। এর পরে ঠিক কী ঘটবে, আর একে কী বলা হয়?
প্রয়োগ
Inclusion গ্যারান্টি () বজায় রাখতে, L3 থেকে X চলে যাওয়ার সাথে সাথেই Core ৩-এর L1 (এবং L2, যদি সেখানেও থাকে)-এ থাকা X-এর কপিও জোর করে invalidate করতে হবে — এমনকি Core ৩ এখনো সক্রিয়ভাবে ব্যবহার করছিল সত্ত্বেও। এই ঘটনাকে বলা হয় back-invalidation। এর ফলে Core ৩ পরবর্তী access-এ একটা অপ্রয়োজনীয় miss পাবে, যা শুধুমাত্র L3-এর capacity চাপের কারণে ঘটল, Core ৩-এর নিজের access pattern-এর কোনো দোষ ছাড়াই — এটাই strict inclusion-এর একটা প্রকৃত, লুকানো পারফরম্যান্স-দাম।
3যুক্তি দিয়ে ব্যাখ্যা করুন — কেন “যদি line X shared L3-তে না থাকে, তাহলে X কোনো core-এর private L1/L2-তেও থাকতে পারে না” — এই সিদ্ধান্তটা শুধু inclusive hierarchy-তেই বৈধ, exclusive বা non-inclusive-এ না।
যুক্তি
এই সিদ্ধান্তটা সরাসরি inclusion-এর সংজ্ঞা থেকে আসে: inclusive মানে — অর্থাৎ L1/L2-এ যা কিছু থাকতে পারে তার সব সম্ভাব্য সদস্যই L3-এর একটা subset। যুক্তি হলো contrapositive: যদি হয়, আর সত্য হয়, তাহলে হতেই হবে (নাহলে subset সম্পর্ক ভেঙে যেত)।
Exclusive-এ এই সম্পর্কটাই উল্টো — L1-এ যা আছে তা ইচ্ছাকৃতভাবে L3-তে নেই (), তাই “L3-তে নেই” থেকে “L1-এও নেই” বলা যায় না — বরং প্রায়ই উল্টোটাই সত্য (L3-তে না থাকা মানে হয়তো L1-এই আছে)।
Non-inclusive-এ কোনো গ্যারান্টিই নেই, তাই “L3-তে নেই” থেকে L1/L2 সম্পর্কে কিছুই logically deduce করা যায় না — precisely এই কারণেই non-inclusive ডিজাইনে coherence-এর জন্য L3-নির্ভর শর্টকাট কাজ করে না, একটা আলাদা explicit snoop filter লাগে।
4একটা ৬৪-core server চিপ ডিজাইন করছেন, প্রতিটা core-এর L2 = ২ MB, shared L3 = ১২৮ MB। Inclusive না non-inclusive বেছে নেবেন, আর কেন? সংখ্যা দিয়ে যুক্তি দিন।
ডিজাইন
Non-inclusive (বা exclusive-ধাঁচের) — ৬৪ core-এ inclusion-এর capacity-দাম বিশাল আকার ধারণ করে।
যদি সব core-এর L2 একসাথে ভরা থাকে (worst-case, কিন্তু বড় server workload-এ বাস্তবসম্মত):
এটা পুরো L3 (১২৮ MB)-র সমান — অর্থাৎ worst-case-এ inclusive ডিজাইনে L3-এর কার্যকর, অনন্য capacity প্রায় শূন্যের কাছাকাছি নেমে যেতে পারে, কারণ পুরো L3 শুধু L2-গুলোর প্রতিধ্বনি বহন করছে! এটা সম্পূর্ণ অগ্রহণযোগ্য।
এই কারণেই বাস্তব উচ্চ-core-count server চিপ (Intel Skylake-SP-উত্তর প্রজন্ম) ঠিক এই যুক্তিতে non-inclusive-এ সরে গেছে, coherence-এর জন্য inclusion-এর বদলে একটা আলাদা, dedicated snoop filter structure ব্যবহার করে — coherence-সরলতার সুবিধা (কিছুটা ভিন্নভাবে হলেও) ধরে রেখে capacity-দাম এড়ানো।
5একটা ৪-core চিপ, প্রতিটার L2 = ৫১২ KB, shared L3 = ৮ MB। Inclusive আর exclusive — দুই ক্ষেত্রেই মোট কার্যকর (unique) capacity হিসাব করুন (worst-case ধরে সব L2 ভরা)।
প্রয়োগ
Inclusive:
Exclusive:
পার্থক্য: বাড়তি কার্যকর capacity exclusive-এ, ঠিক একই মোট hardware (L2+L3) আকারে — এই লেসনের মূল সংখ্যাগত দাবির আরেকটা concrete উদাহরণ।
6Snoop filter আর inclusive L3 — দুইটাই coherence checking-এ “শুধু একটা জায়গা দেখলেই যথেষ্ট” সুবিধা দেয়। তাহলে snoop filter থাকলে কেন সব CPU inclusive L3 বাদ দিয়ে সবসময় non-inclusive + snoop filter বেছে নেয় না?
যুক্তি
কারণ snoop filter কোনো “বিনামূল্যে উন্নতি” না — এটা inclusion-এর capacity-দামকে একটা ভিন্ন দামে (dedicated hardware area, design জটিলতা, আর filter নিজেই ভুল হতে পারা — filter miss হলে conservative-ভাবে broadcast-এ ফিরে যেতে হয়) রূপান্তরিত করে, দাম শূন্য করে না। ছোট core-সংখ্যার চিপে (৪-৮ core) inclusion-এর capacity-অপচয় তুলনামূলক ছোট (এই লেসনের example অংশের হিসাবে ২ MB, সামান্য) — সেখানে inclusive L3-এর সরলতা (আলাদা কোনো নতুন hardware কাঠামো ডিজাইন/verify করতে হয় না) prefereable। বড় core-সংখ্যায় (৩০+) সেই একই অপচয় বিশাল হয়ে ওঠে (৬৪-core উদাহরণে প্রায় পুরো L3), তখনই snoop filter-এর বাড়তি জটিলতা গ্রহণযোগ্য হয়ে ওঠে। এটা তাই কোনো এক-দিকের “উন্নতি” না, বরং core-সংখ্যা অনুযায়ী trade-off-এর ভারসাম্য বিন্দু সরে যাওয়া — ঠিক Intel client (কম core, inclusive) বনাম server (বেশি core, non-inclusive+filter)-এর বাস্তব সিদ্ধান্তের পার্থক্য যা ব্যাখ্যা করে।
এরপর কী
আমরা এখন জানি একাধিক cache level কীভাবে একে অপরের সাথে সম্পর্কিত হতে পারে (inclusive, exclusive, non-inclusive), আর কেন সেই সম্পর্কটা coherence checking-এর কঠিনতা সরাসরি নির্ধারণ করে — inclusive L3 একটা “ফ্রি” filter দেয়, বাকিদের একটা আলাদা snoop filter বানাতে হয়।
কিন্তু আমরা এখনো “coherence checking” জিনিসটা কী সেটাই বিস্তারিত বলিনি — শুধু বলেছি এটা দরকার কারণ private L1/L2-তে একই address-এর একাধিক copy থাকতে পারে। পরের লেসনে (cache-coherence-mesi) আমরা ঠিক সেই প্রশ্নে যাব — MESI protocol, চারটা state (Modified, Exclusive, Shared, Invalid), আর একটা সম্পূর্ণ worked example যেখানে দুইটা core একই address পড়ে, তারপর একটা core লেখে, আর প্রতিটা ধাপে কী inter-core message পাঠানো হয় তা দেখব — ঠিক এই লেসনের presence-bits আর targeted-invalidate ধারণার উপর দাঁড়িয়ে।
আরও পড়ুন
- Computer Architecture: A Quantitative Approach, Appendix B.3 — Hennessy & Patterson · Multi-level cache inclusion policy-র প্রামাণ্য আলোচনা
- An Analysis of Cache Inclusion Policies for Multi-Level Cache Hierarchies — Jaleel, Eden, Steely (Intel/AMD research context) · Inclusive/exclusive/non-inclusive trade-off-এর গবেষণাধর্মী বিশ্লেষণ
- Software Optimization Guide for AMD Family 19h Processors — AMD · Zen-গোত্রের বাস্তব L2/L3 exclusion নীতির বিবরণ