সরাসরি পর্বে যান
পর্ব ৩৩ · ৬৪-এর মধ্যে · মডিউল ৫

TLB আর বড় page table — multilevel, hashed আর inverted

The TLB and Large Page Tables — Effective Access Time, Multilevel, Hashed and Inverted Page Tables

Taught in Bangla, this lesson tackles the double memory access of paging with the TLB, hardware versus software TLB miss handling, the effective access time calculation, TLB reach and huge pages, page table size for 32- and 64-bit addresses, multilevel page tables, hashed page tables and inverted page tables, with worked numericals.

এই পর্বে যা শিখবেন

  • TLB কী, কেন associative, আর hit ও miss-এ ঠিক কী কী ঘটে — hardware page walker (x86, ARM) আর software refill (MIPS)
  • Effective access time-এর দুই প্রচলিত রীতি, এক ও একাধিক স্তরের টেবিলে পূর্ণ সমাধান করা উদাহরণ, আর উল্টো দিকের প্রশ্ন (ন্যূনতম hit ratio)
  • Page table কত বড় হয় — ৩২ ও ৬৪ bit-এ হিসাব — আর two-level, চার স্তর ও পাঁচ স্তরের টেবিল কীভাবে ফাঁকা address space-এ মেমরি বাঁচায়
  • একটি ঠিকানাকে হাতে হাতে two-level ও x86-64-এর চার স্তরের টেবিলের মধ্য দিয়ে অনুবাদ করা
  • Hashed, clustered আর inverted page table — কখন কোনটি, আর তাদের দাম; TLB reach আর huge page-এর বাস্তব প্রভাব

১ · প্রতিটি ঠিকানায় দুবার মেমরি — সমস্যাটা কোথায়

পর্ব ৩২-এ দেখেছেন, paging-এ প্রসেসের virtual address দুই ভাগে ভাঙে — page number আর offset. Page number দিয়ে page table-এ খুঁজে পাওয়া যায় কোন frame-এ ডেটা আছে; তারপর frame number-এর পাশে offset বসালেই physical address. নিয়মটি সুন্দর, কিন্তু একটি বড় খরচ লুকিয়ে আছে: page table নিজেই মেমরিতে থাকে।

অর্থাৎ প্রোগ্রাম যখন একটি ভ্যারিয়েবল পড়ে, CPU-কে আসলে দুবার মেমরিতে যেতে হয় — প্রথমে page table থেকে সেই page-এর page table entry (PTE) পড়া, তারপর আসল ডেটা পড়া। মেমরি একবার পড়তে ১০০ ns লাগলে প্রতিটি ছোঁয়ায় ২০০ ns. প্রোগ্রাম রাতারাতি অর্ধেক গতির হয়ে গেল — শুধু সুরক্ষা আর নমনীয়তার জন্য। কেউ এই দাম দিতে রাজি হতো না।

একটি সরকারি অফিসের কথা ভাবুন। হাজার হাজার ফাইল পেছনের ঘরে বড় ফাইল ক্যাবিনেটে। কোন ফাইল কোন ড্রয়ারে আছে, তা লেখা একটি মোটা রেজিস্টার খাতায় — সেটিও সেই পেছনের ঘরে। প্রতিবার একটি ফাইল লাগলে কেরানিকে দুবার পেছনের ঘরে যেতে হয়: একবার রেজিস্টার দেখে ড্রয়ার নম্বর জানতে, আরেকবার ফাইল আনতে। অভিজ্ঞ কেরানি তাই নিজের টেবিলে একটি ছোট নোটবুক রাখেন — যে ৮–১০টি ফাইল আজ বারবার লাগছে, সেগুলোর ড্রয়ার নম্বর লেখা। নোটবুকে পেলে সরাসরি ড্রয়ারে; না পেলে রেজিস্টার দেখে, নম্বরটি নোটবুকে টুকে রাখেন। এই নোটবুকটিই TLB.

কাজ করে কেন? কারণ প্রোগ্রাম মেমরির সব জায়গা সমানভাবে ছোঁয় না — পর্ব ৪-এর locality. একটি ৪ KiB page-এর মধ্যে সাধারণত অনেকগুলো পরপর ছোঁয়া পড়ে (একটি লুপ, একটি array, stack-এর ওপরের অংশ)। তাই অল্প কয়েকটি page-এর অনুবাদ মনে রাখলেই বেশিরভাগ ছোঁয়া মিটে যায়।

ধারণাটি paging-এর প্রায় সমান পুরোনো। ১৯৬২ সালের দিকে চালু হওয়া Manchester-এর Atlas কম্পিউটারে প্রথম virtual memory আর paging দেখা যায়; সেখানে প্রতিটি physical page-এর জন্য একটি করে associative “page address register” ছিল, যা একসাথে সবগুলো মিলিয়ে দেখত। ১৯৬০-৭০-এর দশকে IBM-এর virtual memory-সমর্থিত মেশিনগুলোতে এমন অনুবাদ-cache নিয়মিত হয়ে ওঠে, আর “translation lookaside buffer” নামটি সাধারণত IBM-এর কাছ থেকে এসেছে বলে ধরা হয়। আজ প্রায় সব সাধারণ-উদ্দেশ্যের CPU-তে একাধিক স্তরের TLB আছে।

২ · TLB — page table-এর ছোট, দ্রুত cache

TLB থাকে MMU-এর ভেতরে, CPU-র একদম কাছে। এর প্রতিটি entry মোটামুটি এরকম: একটি tag (virtual page number), সংশ্লিষ্ট frame number, অনুমতির bit (পড়া/লেখা/চালানো, user না kernel), আর প্রায়ই একটি address-space নম্বর (ASID, x86-এ PCID), যাতে কোন প্রসেসের অনুবাদ তা আলাদা করা যায়। Process switch-এ TLB-র কী হয়, ASID/PCID কীভাবে মুছে ফেলার খরচ বাঁচায় — সেটি পর্ব ৯-এর খরচের অংশে বিস্তারিত দেখেছেন; এখানে আর পুনরাবৃত্তি করছি না।

মূল ধারণা

Associative (content-addressable) খোঁজা: সাধারণ মেমরিতে আপনি ঠিকানা দেন, মেমরি সেই ঘরের জিনিস দেয়। Associative মেমরিতে আপনি জিনিসটি (এখানে page number) দেন, আর hardware একসাথে সব entry-র tag-এর সাথে তুলনা করে বলে মিলেছে কি না। এজন্য প্রতিটি entry-র পাশে আলাদা তুলনার circuit লাগে — দামি, গরম, তাই TLB ছোট: সাধারণত কয়েক ডজন থেকে কয়েক হাজার entry. বড় TLB প্রায়ই পুরোপুরি associative না হয়ে set-associative হয়, ঠিক CPU cache-এর মতো।

TLB নিজেও স্তরে স্তরে সাজানো থাকে: instruction আর data-র জন্য আলাদা ছোট L1 TLB (খুব দ্রুত), তাদের পেছনে একটি বড় যৌথ L2 TLB. সংখ্যাগুলো প্রসেসর-প্রজন্মভেদে আলাদা, তাই এই পর্বে উদাহরণ হিসেবে ধরা হবে L1-এ ৬৪টি আর L2-তে ১৫৩৬টি entry — আধুনিক desktop প্রসেসরে এর কাছাকাছি সংখ্যা দেখা যায়, কিন্তু আপনার মেশিনে আলাদা হতে পারে।

নিচের stepper-এ একটি ৪-entry TLB দেখুন (page size ৪ KiB, তাই ঠিকানার শেষ ১২ bit = শেষ ৩টি hex অঙ্ক offset). প্রথমে একটি hit, তারপর একটি miss. TLB-র লাইনগুলো বাঁ থেকে ডানে “সবচেয়ে পুরোনো ব্যবহার” থেকে “সবচেয়ে সাম্প্রতিক”-এ সাজানো — জায়গা না থাকলে বাঁয়েরটি বাদ যায়, যাকে বলে LRU.

1 VA 0x40A10 -> VPN 0x40, offset 0xA102 compare 0x40 with ALL tags at once -> HIT3 PA = frame 0x07 + 0xA10 = 0x07A10, read data4 VA 0x99123 -> VPN 0x99: no tag matches -> MISS5 page walk: read PTE[0x99] from memory -> 0x2E6 evict LRU 0x12, insert 0x99->0x2E, read data
অবস্থা (ধাপের শেষে)
TLB (পুরোনো → নতুন)
এই ছোঁয়ায় মেমরি-পড়া
সময়
  1. ঠিকানা ভাঙা। CPU virtual address 0x40A10 চাইল। শেষ ১২ bit (0xA10) offset, বাকিটা page number 0x40. TLB-তে আছে চারটি অনুবাদ; 0x40 সবচেয়ে পুরোনো ব্যবহারের দ্বিতীয়টি।
  2. এক ধাক্কায় তুলনা — hit. TLB চারটি tag-এর সাথে একসাথে 0x40 মেলায়; দ্বিতীয়টি মিলল। এতে সময় লাগে শুধু TLB-র খোঁজার সময় t. ব্যবহার হলো বলে 0x40 এখন সবচেয়ে ডানে (সবচেয়ে সাম্প্রতিক)।
  3. Physical address আর ডেটা। Frame 0x07-এর পাশে offset বসিয়ে 0x07A10; মেমরিতে একবারই যেতে হলো। মোট সময় t + m — ধরুন ২০ + ১০০ = ১২০ ns. Page table ছোঁয়াই হয়নি।
  4. নতুন ঠিকানা — miss. এবার 0x99123, page 0x99. কোনো tag মেলে না। এটি জানতেও t সময় গেছে; এখন page table-এ যেতে হবে।
  5. Page walk. Page table কোথায় আছে, তা থাকে একটি register-এ (PTBR; x86-এ CR3). এক স্তরের টেবিলে PTBR + 0x99 × (PTE-র আকার) ঠিকানা থেকে PTE পড়া হলো — একবার মেমরি। PTE বলছে frame 0x2E, valid. (Valid না হলে page fault — পর্ব ৩৫.)
  6. TLB-তে রাখা, তারপর ডেটা। TLB ভর্তি, তাই সবচেয়ে পুরোনো 0x12 বাদ, নতুন 0x99→0x2E ঢুকল। এরপর 0x2E123 থেকে ডেটা — দ্বিতীয়বার মেমরি। মোট t + 2m = ২২০ ns. পরের বার 0x99-এর যেকোনো ঠিকানা hit হবে।

এখানে hit ratio (h) মানে মোট ছোঁয়ার কত ভাগ TLB-তে মিলল। বাস্তব প্রোগ্রামে এটি সাধারণত ৯৯%-এর আশেপাশে বা বেশি থাকে — কারণ একটি ৪ KiB page-এ ৬৪টি cache line, আর লুপ একই page বারবার ছোঁয়। কিন্তু বড় ডেটা এলোমেলোভাবে ছোঁয়া প্রোগ্রামে (বড় hash table, graph, database index) এটি অনেক কমে যেতে পারে — অংশ ৫-এ মেপে দেখবেন।

৩ · TLB miss কে সামলায় — hardware না software

Miss হলে page table হাঁটার কাজটি কে করবে, তা নিয়ে ইতিহাসে দুটি স্কুল আছে।

Hardware-managed TLB

Miss হলে CPU-র ভেতরের একটি circuit — hardware page walker — নিজেই CR3 থেকে শুরু করে টেবিল হাঁটে, PTE এনে TLB-তে বসায়। OS-কে জানানোই হয় না; শুধু PTE invalid হলে page fault দিয়ে OS-কে ডাকা হয়। শর্ত: page table-এর গঠন CPU-র নির্ধারিত হতে হবে।

x86 / x86-64, ARM (AArch64), সাধারণত RISC-V

Software-managed TLB

Miss হলে CPU একটি বিশেষ exception দেয়; OS-এর একটি ছোট, খুব দ্রুত handler নিজের পছন্দের যেকোনো গঠনের টেবিল থেকে অনুবাদ খুঁজে বিশেষ instruction দিয়ে TLB-তে লেখে, তারপর থেমে থাকা instruction আবার চলে। নমনীয়, কিন্তু প্রতিটি miss-এ exception-এর খরচ।

ক্লাসিক MIPS; কিছু পুরোনো SPARC আর Alpha-র নকশাতেও

Software-managed TLB-এর সবচেয়ে পরিচিত উদাহরণ MIPS: TLB miss হলে একটি নির্দিষ্ট “TLB refill” exception ঠিকানায় লাফ, সেখানে kernel-এর মাত্র কয়েক ডজন instruction-এর handler, আর TLB-তে লেখার জন্য tlbwr-জাতীয় instruction. পাঠ্যবইয়ের এই গল্পটি সরলীকৃত — পরের প্রজন্মের MIPS-এ hardware-সহায়তাও যোগ হয়েছিল, আর Alpha-র ক্ষেত্রে কাজটি করত firmware-এর একটি স্তর (PALcode). মূল শিক্ষা একটাই: software refill-এ OS যেকোনো ধরনের টেবিল (hashed, inverted, নিজের তৈরি) ব্যবহার করতে পারে, কিন্তু প্রতিটি miss বেশি দামি।

বিষয়Hardware walkerSoftware refill
Miss-এর খরচকয়েকটি মেমরি-পড়া (প্রায়ই cache থেকে), pipeline থামে না বললেই চলেexception, handler চালানো, ফেরা — তুলনামূলক বেশি
Page table-এর গঠনCPU ঠিক করে দেয় (x86-64-এ চার বা পাঁচ স্তর)OS-এর ইচ্ছামতো
OS-এর কাজটেবিল ঠিকঠাক রাখা, বদলালে TLB-র পুরোনো entry বাতিল করাhandler লেখা, TLB-তে লেখা, বাতিল করা
আজকের বাজারপ্রায় সব desktop, ফোন আর serverবেশিরভাগ ঐতিহাসিক বা বিশেষ embedded নকশা

Hardware walker থাকলেও TLB-কে “সামঞ্জস্যপূর্ণ” রাখা OS-এর দায়িত্ব। OS কোনো PTE বদলালে (page সরানো, অনুমতি কমানো, মেমরি ফেরত নেওয়া) TLB-তে পুরোনো অনুবাদ থেকে যেতে পারে — CPU নিজে থেকে তা জানে না। তাই OS সেই entry বাতিল করে (x86-এ invlpg instruction). একাধিক core থাকলে অন্য core-এর TLB-তেও পুরোনো entry থাকতে পারে; তখন সেগুলোকে interrupt পাঠিয়ে বাতিল করাতে হয় — একে বলে TLB shootdown, আর বড় মেশিনে এটি বেশ দামি।

৪ · Effective access time — পরীক্ষার সবচেয়ে পরিচিত হিসাব

পর্ব ৪-এ cache-এর জন্য AMAT আর effective access time দেখেছেন। TLB-র ক্ষেত্রেও একই কাঠামো: hit হলে এক রকম সময়, miss হলে আরেক রকম, আর hit ratio দিয়ে ওজন করা গড়ই effective access time (EAT). ধরুন:

  • m = একবার main memory পড়ার সময়
  • t = একবার TLB খোঁজার সময়
  • h = TLB hit ratio
  • L = page table-এর স্তর (সাধারণ paging-এ L = ১)

Hit-এ: TLB খোঁজা + ডেটা পড়া = t + m. Miss-এ: TLB খোঁজা + L বার টেবিল পড়া + ডেটা = t + (L + 1)m. তাই:

EAT = h(t + m) + (1 − h)(t + 2m) · এক স্তরের টেবিল
EAT = h(t + m) + (1 − h)(t + (L + 1)m) · L স্তরের টেবিল

সরল করলে এক স্তরে EAT = t + m + (1 − h)m, আর L স্তরে EAT = t + m + (1 − h)·L·m. অর্থাৎ প্রতিটি miss-এর বাড়তি খরচ L·m, আর সেটি ঘটে (1 − h) ভাগ সময়ে।

দুই রীতি — t গোনা হবে কি না

পরীক্ষায় সবচেয়ে বেশি গোলমাল হয় এখানে। বই আর প্রশ্নভেদে দুটি রীতি চলে:

  • রীতি ক (t গোনা হয়): উপরের সূত্র। TLB খোঁজার পরেই মেমরিতে যাওয়া, তাই hit আর miss দুই পথেই t যোগ হয়। প্রশ্নে “TLB access time ২০ ns” দেওয়া থাকলে সাধারণত এটিই চাওয়া হয়।
  • রীতি খ (t নগণ্য ধরা হয়): কিছু পাঠ্যবই (যেমন Silberschatz-এর পরিচিত বইয়ের নতুন সংস্করণগুলো) ধরে নেয় TLB খোঁজা মেমরি-পড়ার তুলনায় এত দ্রুত বা মেমরি-পড়ার সাথে এমনভাবে মিশে যায় যে আলাদা গোনার দরকার নেই: EAT = h·m + (1 − h)·2m. প্রশ্নে t না থাকলে এটিই।

আরও একটি মিশ্র রূপ কোথাও কোথাও দেখা যায় — hit-এ t + m, কিন্তু miss-এ 2m (TLB আর টেবিল খোঁজা একসাথে শুরু ধরে)। নিরাপদ নিয়ম: উত্তরের শুরুতে লিখে দিন কোন রীতি ধরছেন, তাহলে পরীক্ষক আপনার যুক্তি বুঝবেন।

উদাহরণ ১ — এক স্তর, h = ৮০%

m = ১০০ ns, t = ২০ ns, h = ০.৮০.

রীতি ক: EAT = ০.৮০ × (২০ + ১০০) + ০.২০ × (২০ + ২০০) = ০.৮০ × ১২০ + ০.২০ × ২২০ = ৯৬ + ৪৪ = ১৪০ ns.
রীতি খ: EAT = ০.৮০ × ১০০ + ০.২০ × ২০০ = ৮০ + ৪০ = ১২০ ns.
তুলনা: TLB না থাকলে প্রতিবার ২০০ ns. আদর্শ (অনুবাদের কোনো খরচ নেই) হলে ১০০ ns. রীতি ক-তে ধীরগতি ৪০%.

উদাহরণ ২ — এক স্তর, h = ৯৮%

রীতি ক: ০.৯৮ × ১২০ + ০.০২ × ২২০ = ১১৭.৬ + ৪.৪ = ১২২ ns (আদর্শের চেয়ে ২২% বেশি; এর ২০ ns-ই TLB খোঁজার নিজের খরচ).
রীতি খ: ০.৯৮ × ১০০ + ০.০২ × ২০০ = ৯৮ + ৪ = ১০২ ns — মাত্র ২% ধীর।
শিক্ষা: hit ratio ৮০% থেকে ৯৮%-এ তুললে বাড়তি খরচ (রীতি খ) ২০ ns থেকে ২ ns — দশ ভাগের এক ভাগ।

উদাহরণ ৩ — চার স্তরের টেবিল (x86-64), h = ৯৮%

L = ৪, তাই miss-এ t + ৫m = ২০ + ৫০০ = ৫২০ ns.
EAT = ০.৯৮ × ১২০ + ০.০২ × ৫২০ = ১১৭.৬ + ১০.৪ = ১২৮ ns. দুই স্তরে (L = ২) একই হিসাবে ০.৯৮ × ১২০ + ০.০২ × ৩২০ = ১১৭.৬ + ৬.৪ = ১২৪ ns.
TLB ছাড়া চার স্তরে প্রতিটি ছোঁয়া ৫ × ১০০ = ৫০০ ns হতো — পাঁচ গুণ ধীর। Multilevel টেবিল TLB ছাড়া কার্যত অচল।

উদাহরণ ৪ — উল্টো দিক থেকে

এক স্তর, m = ১০০, t = ২০, রীতি ক. EAT ১৩০ ns-এর বেশি হতে দেওয়া যাবে না — ন্যূনতম h কত?

t + m + (1 − h)m ≤ ১৩০ ⟹ ১২০ + (1 − h) × ১০০ ≤ ১৩০ ⟹ 1 − h ≤ ০.১০ ⟹ h ≥ ০.৯০. যাচাই: h = ০.৯০-তে ০.৯ × ১২০ + ০.১ × ২২০ = ১০৮ + ২২ = ১৩০ ns ✓.

নিচের calculator দুই রীতিতে এক, দুই, চার আর পাঁচ স্তরের EAT একসাথে ছাপে। পরীক্ষার আগে নিজের বইয়ের সংখ্যা বসিয়ে হাতের উত্তর মিলিয়ে নিতে পারেন — কিন্তু আগে হাতে করুন।

// Effective access time with a TLB. m = one memory access, t = one TLB lookup (ns).
const m = 100, t = 20;
// Convention A (t counted): hit = t + m, miss = t + (L + 1) m   (L = page-table levels)
const eatA = (h, L) => h * (t + m) + (1 - h) * (t + (L + 1) * m);
// Convention B (t ignored, "TLB time negligible"): hit = m, miss = (L + 1) m
const eatB = (h, L) => h * m + (1 - h) * (L + 1) * m;
const f = x => x.toFixed(1).padStart(7);

console.log("     h |   1L A   1L B |   2L A |   4L A   4L B |   5L A");
for (const h of [0.80, 0.90, 0.95, 0.98, 0.99, 0.999]) {
  console.log((h * 100).toFixed(1).padStart(5) + "% |" + f(eatA(h, 1)) + f(eatB(h, 1)) +
    " |" + f(eatA(h, 2)) + " |" + f(eatA(h, 4)) + f(eatB(h, 4)) + " |" + f(eatA(h, 5)));
}
console.log("No TLB: 1-level " + 2 * m + " ns, 4-level " + 5 * m + " ns");
// Reverse question: smallest h so that 1-level EAT (A) <= 130 ns
// t + m + (1 - h) m <= 130  ->  1 - h <= (130 - t - m) / m
const target = 130;
console.log("EAT <= " + target + " ns needs h >= " + (1 - (target - t - m) / m).toFixed(2));
// Slowdown compared with an ideal machine that needs only m per access
console.log("h = 98%, 4-level: " + eatA(0.98, 4).toFixed(1) + " ns = " +
  ((eatA(0.98, 4) / m - 1) * 100).toFixed(0) + "% slower than " + m + " ns");

আউটপুট (নির্ধারিত, প্রতিবার একই):

     h |   1L A   1L B |   2L A |   4L A   4L B |   5L A
 80.0% |  140.0  120.0 |  160.0 |  200.0  180.0 |  220.0
 90.0% |  130.0  110.0 |  140.0 |  160.0  140.0 |  170.0
 95.0% |  125.0  105.0 |  130.0 |  140.0  120.0 |  145.0
 98.0% |  122.0  102.0 |  124.0 |  128.0  108.0 |  130.0
 99.0% |  121.0  101.0 |  122.0 |  124.0  104.0 |  125.0
 99.9% |  120.1  100.1 |  120.2 |  120.4  100.4 |  120.5
No TLB: 1-level 200 ns, 4-level 500 ns
EAT <= 130 ns needs h >= 0.90
h = 98%, 4-level: 128.0 ns = 28% slower than 100 ns

“1L A” মানে এক স্তর, রীতি ক; “4L B” মানে চার স্তর, রীতি খ. প্রথম আর চতুর্থ সারিতে উদাহরণ ১–৩-এর সব সংখ্যা: ১৪০/১২০, ১২২/১০২, দুই স্তরে ১২৪, চার স্তরে ১২৮. দুটি জিনিস খেয়াল করুন। এক, h ৮০% থেকে ৯৯.৯%-এ যেতে চার স্তরের EAT ২০০ থেকে ১২০.৪ — স্তর যত বেশি, hit ratio তত গুরুত্বপূর্ণ। দুই, h খুব বেশি হলে স্তরের সংখ্যা প্রায় অপ্রাসঙ্গিক হয়ে যায় (৯৯.৯%-এ ১২০.১ বনাম ১২০.৫) — এজন্যই x86-64 চার থেকে পাঁচ স্তরে যাওয়ার খরচ মেনে নিতে পেরেছে। রীতি ক-তে সর্বনিম্ন সম্ভব মান ১২০, কারণ প্রতিটি ছোঁয়ায় t + m লাগবেই।

খাতায় সবচেয়ে বেশি হওয়া ভুল: (১) miss-এ শুধু 2m লিখে t ভুলে যাওয়া, অথচ hit-এ t যোগ করা — অর্থাৎ দুই রীতি মিশিয়ে ফেলা; (২) L স্তরে miss-এর সময় L·m লেখা, অথচ ডেটা পড়াসহ (L + 1)·m; (৩) hit ratio শতাংশে দিয়ে গুণ করা (৯৮ × ১২০); (৪) page fault-এর সময়কে TLB miss-এর সাথে গুলিয়ে ফেলা — TLB miss মানে টেবিল মেমরিতেই আছে, page fault মানে ডেটা মেমরিতেই নেই (পর্ব ৩৫-এর হিসাব)।

৫ · TLB reach আর huge page

TLB কত বড় মেমরির অনুবাদ একসাথে ধরে রাখতে পারে, তার মাপ TLB reach:

TLB reach = TLB-র entry সংখ্যা × page size
TLB (উদাহরণ)Page sizeReach
L1 data TLB, ৬৪ entry৪ KiB৬৪ × ৪ KiB = ২৫৬ KiB
L2 TLB, ১৫৩৬ entry৪ KiB১৫৩৬ × ৪ KiB = ৬ MiB
L2 TLB, ১৫৩৬ entry২ MiB১৫৩৬ × ২ MiB = ৩ GiB
৪টি entry১ GiB৪ GiB

৬ MiB! অথচ আজকের একটি ব্রাউজার, database বা machine-learning প্রোগ্রাম কয়েক GB মেমরি নিয়ে কাজ করে। যদি প্রোগ্রাম সেই বড় অংশ এলোমেলোভাবে ছোঁয়, প্রায় প্রতিটি ছোঁয়াই TLB miss. সমাধান: বড় page. x86-64-এ ৪ KiB-এর পাশাপাশি ২ MiB আর ১ GiB page আছে; ARM64-এও granule অনুযায়ী বড় block আছে। একটি huge page-এর একটি TLB entry ৫১২টি সাধারণ page-এর কাজ করে, আর page walk-এও এক স্তর কম লাগে (অংশ ৭)।

দাম কী? Internal fragmentation — ২ MiB-এর page-এ ১০ KiB দরকার হলেও পুরো ২ MiB আটকে যায়; আর একটানা ২ MiB খালি physical মেমরি খুঁজে পাওয়াও কঠিন হতে পারে (fragmentation, পর্ব ৩১)। Linux-এ দুই পথ: আগে থেকে সংরক্ষিত hugetlbfs page (প্রোগ্রামকে বিশেষভাবে চাইতে হয়), আর transparent huge page (THP), যেখানে kernel নিজেই সুযোগ পেলে সাধারণ মেমরিকে ২ MiB-এ বসায়। Windows-এ এর নাম “large pages” (বিশেষ অনুমতি লাগে), আর Apple silicon-এ macOS-এর সাধারণ page-ই ১৬ KiB — যা reach চার গুণ বাড়ায়। বিস্তারিত পর্ব ৩৮-এ।

নিজের হাতে মাপা — একই ডেটা, দুই ক্রমে

নিচের প্রোগ্রাম ৬৪ MiB মেমরি নেয় (১৬৩৮৪টি ৪ KiB page), আর দুবারই প্রতিটি ৬৪-byte cache line ঠিক একবার পড়ে — মোট পড়া সমান। পার্থক্য শুধু ক্রমে: stride 64-এ একটি page-এর ৬৪টি line শেষ করে পরের page-এ যায়; stride 4096-এ প্রতিটি পড়াই নতুন page-এ — ১৬৩৮৪টি page ঘুরে তারপর পরের offset. madvise(MADV_NOHUGEPAGE) দিয়ে kernel-কে বলা হয়েছে huge page না দিতে, আর memset দিয়ে সব page আগেই ছুঁয়ে নেওয়া হয়েছে, যাতে সময়ের মধ্যে page fault না ঢোকে।

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/mman.h>
#define SIZE (64UL << 20)   /* 64 MiB = 16384 pages of 4 KiB */
#define PAGE 4096UL
#define LINE 64UL
static unsigned long sink;
static double now_ns(void) {
    struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1e9 + ts.tv_nsec; }
/* Both orders read every 64-byte line exactly once; only the ORDER differs. */
static double run(const volatile unsigned char *b, unsigned long stride) {
    unsigned long s = 0; double t0 = now_ns();
    if (stride == LINE)                              /* 64 reads per page, then next page */
        for (unsigned long i = 0; i < SIZE; i += LINE) s += b[i];
    else                                             /* every read lands on a new page */
        for (unsigned long off = 0; off < PAGE; off += LINE)
            for (unsigned long p = 0; p < SIZE; p += PAGE) s += b[p + off];
    sink += s;
    return (now_ns() - t0) / (SIZE / LINE);
}
int main(void) {
    unsigned char *b = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (b == MAP_FAILED) { perror("mmap"); return 1; }
    madvise(b, SIZE, MADV_NOHUGEPAGE);              /* ask for plain 4 KiB pages */
    memset(b, 1, SIZE);                             /* take all page faults BEFORE timing */
    for (int r = 1; r <= 3; r++) {
        double a = run(b, LINE), c = run(b, PAGE);
        printf("run %d: stride 64 = %.2f ns/read, stride 4096 = %.2f ns/read (%.1fx)\n", r, a, c, c / a);
    }
    printf("reads per run %lu, checksum %lu\n", SIZE / LINE, sink);
    return munmap(b, SIZE);
}

একটি আসল চালানোর আউটপুট (Wandbox, Linux x86-64; সংখ্যা প্রতিবার আর প্রতি মেশিনে বদলাবে):

run 1: stride 64 = 6.14 ns/read, stride 4096 = 20.69 ns/read (3.4x)
run 2: stride 64 = 4.63 ns/read, stride 4096 = 21.20 ns/read (4.6x)
run 3: stride 64 = 5.30 ns/read, stride 4096 = 20.59 ns/read (3.9x)
reads per run 1048576, checksum 6291456

একই ১০,৪৮,৫৭৬টি পড়া, তবু page-ঘোরা ক্রমে প্রতিটি পড়া প্রায় ২১ ns, আর line-ধরে চলায় ৫–৬ ns — সাড়ে তিন থেকে সাড়ে চার গুণ পার্থক্য। একই রানের তিনটি মাপও এক নয়: মেশিনটি অন্যদের সাথে ভাগ করা, তাই কোনো কোনো চালানোয় সংখ্যা হঠাৎ অনেক বেড়েও যেতে পারে — একবারের মাপে সিদ্ধান্ত নেবেন না। checksum (৬ রান × ১০,৪৮,৫৭৬ × ১) প্রমাণ করে দুই ক্রমে একই ডেটা পড়া হয়েছে। কিন্তু সততার সাথে বলি — পুরো পার্থক্যটি TLB-র নয়। stride 4096-এ (ক) প্রায় প্রতিটি পড়ায় TLB miss, কারণ ১৬৩৮৪টি page যেকোনো TLB-র চেয়ে বড়; (খ) CPU-র hardware prefetcher পরের line আগে আনতে পারে না — prefetcher সাধারণত page-সীমা পেরোয় না; (গ) cache-এর একই set-এ অনেক line ভিড় করে। stride 64-এ প্রতি ৬৪টি পড়ায় একটি নতুন page, আর prefetcher পুরো সুবিধা পায়। কোন অংশ কতটা, আলাদা করতে perf stat -e dTLB-load-misses-এর মতো hardware counter লাগে, যা এই container-এ নেই (পর্ব ৫৭)। আর compile কোনো optimization ছাড়া হয়েছে, তাই প্রতিটি পড়ার সাথে লুপের কয়েক ns খরচও ধরা আছে।

আপনার Linux-এ page size আর huge page-এর অবস্থা দেখতে root লাগে না:

# Page size and huge-page settings of this Linux machine (read-only, no root needed)
echo "base page size: $(getconf PAGESIZE) bytes"
grep -i huge /proc/meminfo
echo "THP mode: $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || echo 'not available')"
# This shell's own memory: how much of it sits in transparent huge pages?
grep -E '^(Rss|AnonHugePages):' /proc/self/smaps_rollup 2>/dev/null || echo "smaps_rollup not available"

Wandbox-এর container-এ একটি চালানোর আউটপুট:

base page size: 4096 bytes
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
THP mode: always [madvise] never
Rss:                1844 kB
AnonHugePages:         0 kB

সাধারণ page ৪০৯৬ byte, huge page-এর আকার ২০৪৮ kB = ২ MiB. HugePages_Total: 0 মানে hugetlbfs-এর জন্য আগে থেকে কোনো page সংরক্ষিত নেই। THP-র মোডে বন্ধনীতে [madvise] — অর্থাৎ kernel নিজে থেকে huge page দেয় না, শুধু যে প্রোগ্রাম madvise(MADV_HUGEPAGE) দিয়ে চায় তাকে দেয়। তাই উপরের C প্রোগ্রামে MADV_NOHUGEPAGE এখানে আসলে ডিফল্টই ছিল; always মোডের মেশিনে ওই লাইনটি না থাকলে ফল বদলে যেতে পারত। শেষ দুই লাইন এই shell-এর নিজের মেমরি: ১৮৪৪ kB, তার কিছুই huge page-এ নেই। আপনার মেশিনে সংখ্যা আলাদা হবে — আর এই মান বদলাতে (যেমন /sys/kernel/mm/transparent_hugepage/enabled-এ লেখা) root লাগে, যা পরীক্ষা না করে production-এ করা উচিত নয়।

৬ · Page table কত বড় — ৩২ আর ৬৪ bit-এর হিসাব

এবার অন্য সমস্যা। TLB গতির সমস্যা মেটায়, কিন্তু page table নিজে কত মেমরি খায়? সাধারণ (সমতল, এক স্তরের) টেবিলে প্রতিটি সম্ভাব্য virtual page-এর জন্য একটি entry রাখতে হয় — প্রসেস সেটি ব্যবহার করুক বা না করুক, কারণ page number দিয়ে সরাসরি index করতে হবে।

হিসাবের নিয়ম

Entry সংখ্যা = ২(virtual address bit − offset bit); টেবিলের আকার = entry সংখ্যা × PTE-র আকার। Offset bit = log₂(page size), ৪ KiB-এ ১২.

Address spacePagePTEEntryপ্রতি প্রসেসে টেবিল
৩২-bit৪ KiB৪ byte২২০ ≈ ১০ লাখ২২২ byte = ৪ MiB
৪৮-bit (x86-64-এর ব্যবহৃত অংশ)৪ KiB৮ byte২৩৬২৩৯ byte = ৫১২ GiB
৫৭-bit (পাঁচ স্তর)৪ KiB৮ byte২৪৫২৪৮ byte = ২৫৬ TiB
পূর্ণ ৬৪-bit৪ KiB৮ byte২৫২২৫৫ byte = ৩২ PiB

৩২-bit-এ ৪ MiB হয়তো সহ্য করা যায় — কিন্তু ১০০টি প্রসেস মানে ৪০০ MiB শুধু টেবিলে, আর তাও একটানা physical মেমরিতে। ৬৪-bit-এ সমতল টেবিল অসম্ভব: পৃথিবীর কোনো কম্পিউটারে একটি প্রসেসের জন্য ৫১২ GiB RAM শুধু টেবিলের জন্য নেই।

অথচ বাস্তব প্রসেস তার address space-এর খুব সামান্য অংশ ব্যবহার করে: নিচে code আর data, তার ওপরে heap, অনেক ওপরে stack, মাঝে কিছু shared library — মাঝখানে বিশাল ফাঁকা জায়গা। একে বলে sparse address space. সমতল টেবিলের প্রায় সব entry তাই “invalid” — অপচয়। পরের তিনটি অংশের তিনটি কৌশল এই অপচয় এড়ানোর তিন উপায়।

৭ · Multilevel page table — টেবিলেরও page table

মূল চিন্তা: বড় টেবিলটিকে page-আকারের টুকরোয় ভাগ করুন, আর একটি ছোট “ওপরের” টেবিলে লিখে রাখুন কোন টুকরো কোথায় আছে। যে টুকরোর সব entry invalid, সেটি বানানোই হয় না — ওপরের টেবিলে তার entry-তে শুধু “নেই” লেখা থাকে। এটিই hierarchical বা multilevel page table.

লাইব্রেরির তাকের কথা ভাবুন। একটি বিশাল খাতায় প্রতিটি সম্ভাব্য বইয়ের নম্বরের জন্য একটি লাইন রাখার বদলে, প্রথমে একটি ছোট সূচি: “বিষয় ১ — ক তাক, বিষয় ২ — খ তাক, বিষয় ৩ — এখনো কোনো বই নেই।” প্রতিটি তাকের পাশে আলাদা ছোট তালিকা। যে বিষয়ে বই নেই, তার তালিকাই নেই — কাগজ বাঁচল। দাম: একটি বই খুঁজতে এখন দুবার দেখতে হয় — সূচি, তারপর তাকের তালিকা।

৩২-bit-এর two-level: ১০/১০/১২

৪ KiB page আর ৪ byte PTE হলে একটি page-এ ৪০৯৬ ÷ ৪ = ১০২৪টি entry ধরে — ঠিক ১০ bit. তাই ২০ bit-এর page number দুই ভাগে: ওপরের ১০ bit (p1) দিয়ে outer page table-এ index, পরের ১০ bit (p2) দিয়ে ভেতরের টেবিলে index, আর শেষ ১২ bit offset. Classic ৩২-bit x86-এ ঠিক এই গঠন — ওপরেরটির নাম page directory.

 31          22 21          12 11              0
┌──────────────┬──────────────┬─────────────────┐
│ p1 (10 bit)  │ p2 (10 bit)  │ offset (12 bit) │
└──────────────┴──────────────┴─────────────────┘
  outer index    inner index    byte in page

আকারের হিসাব: outer table = ১০২৪ × ৪ byte = ৪ KiB, প্রতিটি inner table-ও ৪ KiB, আর একটি inner table ১০২৪ × ৪ KiB = ৪ MiB virtual মেমরি ঢাকে। ধরুন একটি ছোট প্রসেসের code 0x00400000-এ, heap 0x00800000-এ, আর stack ৩ GiB-এর ঠিক নিচে (0xBFFF0000–0xBFFFFFFF). এই তিন অংশ পড়ে outer index ১, ২ আর ৭৬৭-এ। দরকার মাত্র ১টি outer + ৩টি inner = ৪টি page = ১৬ KiB, সমতল টেবিলের ৪ MiB-এর বদলে। পুরো ৪ GiB ব্যবহার করলে অবশ্য ১ + ১০২৪টি page = ৪ MiB + ৪ KiB — সমতলের চেয়ে সামান্য বেশি। Multilevel-এর লাভ পুরোটাই sparse-তার ওপর।

এবার একটি ঠিকানা হাতে হাতে অনুবাদ করুন: 0x00403A7C. ধরুন outer table আছে frame 0x010-এ, তার entry ১ বলছে inner table frame 0x2B-তে, আর সেই inner table-এর entry ৩ বলছে frame 0x1F5. TLB শুরুতে খালি।

1 VA 0x00403A7C -> p1 = 1, p2 = 3, d = 0xA7C2 TLB lookup VPN 0x00403 -> MISS3 read outer[1] at PTBR (frame 0x010) -> 0x2B4 read inner[3] in frame 0x2B -> frame 0x1F55 PA = 0x1F5 << 12 | 0xA7C = 0x001F5A7C6 next: VA 0x00403A80 -> TLB HIT
অবস্থা (ধাপের শেষে)
p1 / p2 / offset
মেমরি-পড়া (এই ঠিকানায়)
TLB
Physical address
  1. ৩২ bit তিন ভাগে। 0x00403A7C-কে binary-তে লিখলে ওপরের ১০ bit 0000000001 = ১, পরের ১০ bit 0000000011 = ৩, শেষ ১২ bit 0xA7C. হাতে করার সহজ উপায়: p1 = VA ÷ ২২২ (৪ MiB), p2 = (VA ÷ ২১২) mod ১০২৪.
  2. আগে TLB. TLB-তে tag হিসেবে পুরো ২০ bit-এর page number 0x00403 খোঁজা হয় — আলাদা দুই ভাগ নয়। শুরুতে খালি, তাই miss; এখন টেবিল হাঁটতে হবে।
  3. Outer table — প্রথম মেমরি-পড়া। PTBR (৩২-bit x86-এ CR3) দেখায় outer table frame 0x010-এ। তার entry ১ পড়া হলো: valid, inner table আছে frame 0x2B-তে। Entry ১-এর ঘরটি invalid হলে এখানেই page fault হতো, আর inner table-এর অস্তিত্বই থাকত না।
  4. Inner table — দ্বিতীয় মেমরি-পড়া। Frame 0x2B-র entry ৩: valid, ডেটার frame 0x1F5. এখানে অনুমতির bit-ও দেখা হয় (পর্ব ৩২-এর PTE bit) — লেখা নিষেধ থাকলে আর প্রোগ্রাম লিখতে চাইলে protection fault.
  5. Physical address — তৃতীয় মেমরি-পড়া। Frame 0x1F5-এর পাশে offset 0xA7C: PA 0x001F5A7C. অনুবাদটি TLB-তে রাখা হলো, তারপর ডেটা পড়া। মোট তিনবার মেমরি — two-level-এ miss মানে (L + 1) = ৩।
  6. পরের ঠিকানা — hit. 0x00403A80 একই page-এ (শুধু offset আলাদা)। TLB-তে 0x00403 মিলল, কোনো টেবিল ছোঁয়া নেই, মেমরি মাত্র একবার। Locality থাকলে বেশিরভাগ ছোঁয়া এমনই।

এবার এই একই টেবিল একটি ছোট simulator-এ — ৪-entry LRU TLB, ১৬টি ঠিকানা, আর প্রতিটির জন্য কত মেমরি-পড়া লাগল তার হিসাব। টেবিলে outer index ১ (code), ২ (heap) আর ৭৬৭ (stack) ছাড়া কিছু নেই — sparse.

// Two-level page table (10/10/12) + a 4-entry LRU TLB. Frame numbers in hex.
const outer = { 1: 0x2B, 2: 0x2C, 767: 0x2D };          // outer index -> frame of inner table
const inner = { 0x2B: { 0: 0x1A0, 1: 0x1A1, 3: 0x1F5 }, // inner index -> data frame
                0x2C: { 0: 0x0C3, 1: 0x0C4 }, 0x2D: { 1023: 0x07E } };
const tlb = new Map();                                   // vpn -> frame, oldest first
const hex = (x, w) => x.toString(16).toUpperCase().padStart(w, "0");
let hits = 0, misses = 0, faults = 0, memReads = 0;
const trace = [0x00400010, 0x00403A7C, 0x00400020, 0xBFFFFFF0, 0x00800100,
  0x00400030, 0x00801008, 0x00403A80, 0xBFFFFFE8, 0x00400040, 0x00801010,
  0x00403A84, 0x00C00000, 0xBFFFFFE0, 0x00400050, 0x00800200];
for (const va of trace) {
  const p1 = va >>> 22, p2 = (va >>> 12) & 0x3FF, off = va & 0xFFF, vpn = va >>> 12;
  let frame, how;
  if (tlb.has(vpn)) {                                    // TLB hit: no table reads
    frame = tlb.get(vpn); tlb.delete(vpn); tlb.set(vpn, frame); hits++; how = "hit ";
  } else {
    misses++; memReads++;                                // read the outer entry
    const t2 = outer[p1];
    if (t2 !== undefined) { memReads++; frame = inner[t2][p2]; } // read the inner entry
    if (frame === undefined) { faults++; console.log(hex(va, 8) + " " + p1 + "/" + p2 + " miss -> PAGE FAULT"); continue; }
    if (tlb.size === 4) tlb.delete(tlb.keys().next().value); // evict least recently used
    tlb.set(vpn, frame); how = "miss";
  }
  memReads++;                                            // the data access itself
  console.log(hex(va, 8) + " " + p1 + "/" + p2 + "/" + hex(off, 3) + " " + how + " -> PA " + hex(frame * 4096 + off, 8));
}
const n = trace.length;
console.log("accesses " + n + ", TLB hits " + hits + ", misses " + misses + " (faults " + faults + ")");
console.log("hit ratio " + (100 * hits / n).toFixed(1) + "%, memory reads " + memReads +
  " (" + (memReads / n).toFixed(2) + " per access; without a TLB " + (3 * (n - faults) + faults) + ")");

আউটপুট (নির্ধারিত):

00400010 1/0/010 miss -> PA 001A0010
00403A7C 1/3/A7C miss -> PA 001F5A7C
00400020 1/0/020 hit  -> PA 001A0020
BFFFFFF0 767/1023/FF0 miss -> PA 0007EFF0
00800100 2/0/100 miss -> PA 000C3100
00400030 1/0/030 hit  -> PA 001A0030
00801008 2/1/008 miss -> PA 000C4008
00403A80 1/3/A80 miss -> PA 001F5A80
BFFFFFE8 767/1023/FE8 miss -> PA 0007EFE8
00400040 1/0/040 hit  -> PA 001A0040
00801010 2/1/010 hit  -> PA 000C4010
00403A84 1/3/A84 hit  -> PA 001F5A84
00C00000 3/0 miss -> PAGE FAULT
BFFFFFE0 767/1023/FE0 hit  -> PA 0007EFE0
00400050 1/0/050 hit  -> PA 001A0050
00800200 2/0/200 miss -> PA 000C3200
accesses 16, TLB hits 7, misses 9 (faults 1)
hit ratio 43.8%, memory reads 32 (2.00 per access; without a TLB 46)

দ্বিতীয় লাইনটি উপরের stepper: 1/3/A7C → 0x001F5A7C. Miss-গুলো তিন ধরনের। প্রথম পাঁচটি page প্রথমবার ছোঁয়ার miss (cold miss). সপ্তম ঠিকানা থেকে TLB-র ৪টি ঘরে ৫টি page-এর ঠেলাঠেলি — 0x00403A80 miss হয়, কারণ ঠিক আগের ধাপে LRU নিয়মে 0x00403 বাদ পড়েছিল (capacity miss). আর 0x00C00000-এর outer index ৩-এ কোনো inner table নেই: একটি মেমরি-পড়াতেই জানা গেল, page fault. হিসাব: ৭টি hit × ১ + ৮টি সাধারণ miss × ৩ + ১টি fault × ১ = ৩২. TLB ছাড়া লাগত ১৫ × ৩ + ১ = ৪৬. মাত্র ১৬টি ছোঁয়ার ছোট trace বলে hit ratio ৪৩.৮% — বাস্তব প্রোগ্রামে লাখো ছোঁয়ায় cold miss-এর ভাগ নগণ্য হয়ে যায়।

x86-64: চার স্তর, ৯/৯/৯/৯/১২

৬৪-bit-এ PTE ৮ byte, তাই একটি ৪ KiB page-এ ৫১২টি entry = ৯ bit. x86-64 ৬৪ bit-এর মধ্যে ৪৮ bit ব্যবহার করে: ৪ স্তর × ৯ bit + ১২ bit offset = ৪৮. বাকি ওপরের ১৬ bit-কে bit ৪৭-এর কপি হতে হয় (canonical address) — ফলে নিচের ১২৮ TiB user-এর, ওপরের ১২৮ TiB সাধারণত kernel-এর, মাঝে বিশাল নিষিদ্ধ ফাঁক। স্তরগুলোর নাম (Intel-এর পরিভাষায়) ওপর থেকে PML4 → PDPT → PD → PT.

স্তরBitএকটি entry কতটা ঢাকেপুরো টেবিল (৫১২ entry) ঢাকে
PML4৪৭–৩৯৫১২ GiB২৫৬ TiB (পুরো ৪৮-bit)
PDPT৩৮–৩০১ GiB (এখানে leaf হলে ১ GiB page)৫১২ GiB
PD২৯–২১২ MiB (এখানে leaf হলে ২ MiB page)১ GiB
PT২০–১২৪ KiB২ MiB

প্রতিটি টেবিল ঠিক ৪ KiB, একটি page-এ আঁটে — এটাই নকশার সৌন্দর্য। একটি ছোট প্রসেস (code 0x400000-এ ১৬ KiB, heap 0x1000000-এ ১৩২ KiB, stack 0x7ffffffde000-এ ১৩২ KiB) গুনে দেখা যায়: ১টি PML4, ২টি PDPT (code-heap একটিতে, stack আরেকটিতে), ২টি PD, ৩টি PT = ৮টি page = ৩২ KiB. ৫১২ GiB-এর তুলনায় প্রায় শূন্য।

1 48-bit VA -> 254 / 232 / 145 / 325 / 0x6782 CR3 -> PML4[254] -> PDPT frame 0x2A3F13 PDPT[232] -> PD frame 0x0B7714 PD[145] -> PT frame 0x13C02 (PS = 0)5 PT[325] -> data frame 0x4D2E96 PA = 0x4D2E9 << 12 | 0x678 = 0x4D2E9678
অবস্থা (ধাপের শেষে)
এখনকার স্তর
মেমরি-পড়া
এ পর্যন্ত বাছাই করা অংশ
  1. ৪৮ bit পাঁচ ভাগে। নিচের ৪৮ bit-কে ৯-৯-৯-৯-১২ করে ভাঙলে index ২৫৪, ২৩২, ১৪৫, ৩২৫ আর offset 0x678. ওপরের ১৬ bit সব ০, কারণ bit ৪৭ = ০ — canonical, user-অংশের ঠিকানা।
  2. PML4. CR3 থেকে PML4 টেবিলের ঠিকানা; তার entry ২৫৪ পড়া হলো (প্রথম মেমরি-পড়া)। Present, পরের টেবিল (PDPT) frame 0x2A3F1-এ। (এই পর্বের frame নম্বরগুলো উদাহরণের জন্য বানানো।)
  3. PDPT. Entry ২৩২ (দ্বিতীয় পড়া)। এর একটি bit — PS (page size) — ১ হলে এটিই শেষ ধাপ হতো আর ১ GiB-এর page পাওয়া যেত। এখানে PS = ০, তাই PD টেবিল frame 0x0B771-এ।
  4. PD. Entry ১৪৫ (তৃতীয় পড়া)। PS = ১ হলে ২ MiB page, আর বাকি ২১ bit পুরোটাই offset — এক স্তর কম হাঁটা আর একটি TLB entry-তে ২ MiB. এখানে PS = ০, তাই PT frame 0x13C02-এ।
  5. PT. Entry ৩২৫ (চতুর্থ পড়া) — ডেটার frame 0x4D2E9. Present না হলে page fault, আর CPU সেই ঠিকানা CR2 register-এ রেখে OS-কে ডাকে।
  6. Physical address আর খরচ। 0x4D2E9 আর 0x678 জুড়ে 0x4D2E9678; তারপর ডেটা (পঞ্চম পড়া)। এক TLB miss-এ পাঁচবার মেমরি — এজন্য CPU ওপরের স্তরগুলোর entry আলাদা ছোট cache-এ রাখে (paging-structure cache), আর টেবিলের entry সাধারণ data cache-এও থাকে; ফলে বাস্তবে সব পড়া RAM পর্যন্ত যায় না।

পাঁচ স্তর (LA57) আর ARM64

৪৮ bit = ২৫৬ TiB virtual মেমরি। কয়েক TB RAM-ওয়ালা বড় server আর বিশাল memory-mapped ডেটার যুগে এটিও ছোট হয়ে আসছিল। তাই Intel একটি পঞ্চম স্তর (PML5) যোগ করে — five-level paging, CPU-র বৈশিষ্ট্যের নাম LA57: ৫ × ৯ + ১২ = ৫৭ bit, অর্থাৎ ১২৮ PiB. Linux-এ এর সমর্থন ২০১৭ সালের দিকে (4.14 সংস্করণের আশেপাশে) এসেছে, আর এটি শুধু সেসব CPU-তে চালু হয় যেগুলো LA57 দেয় — মূলত নতুন প্রজন্মের কিছু server প্রসেসর। দাম: TLB miss-এ এক স্তর বেশি হাঁটা। তাই Linux-এ সাধারণ প্রোগ্রাম ডিফল্টভাবে ৪৭-bit-এর নিচের ঠিকানাই পায়; বড় ঠিকানা চাইলে প্রোগ্রামকে স্পষ্টভাবে mmap-এ উঁচু ঠিকানার ইঙ্গিত দিতে হয়।

ARM64 (AArch64)-এ ৪ KiB granule-এর সাধারণ কনফিগারেশন প্রায় একই রকম: ৪৮-bit virtual address, চার স্তর (ARM-এর নামে level 0–3), প্রতিটিতে ৯ bit. পার্থক্য: ARM-এ user আর kernel-এর জন্য আলাদা দুটি টেবিল-register (TTBR0 আর TTBR1), আর ১৬ KiB ও ৬৪ KiB granule-ও আছে — বড় granule-এ স্তর কম লাগে আর TLB reach বেশি। Apple silicon-এ macOS আর iOS ১৬ KiB page ব্যবহার করে।

৮ · Hashed page table — যত page ব্যবহার, তত entry

৬৪-bit-এ চার-পাঁচ স্তর হাঁটা দামি। বিকল্প চিন্তা: page number-কে একটি hash table-এ রাখা, যেখানে entry থাকে শুধু ব্যবহৃত page-এর জন্য। প্রতিটি bucket-এ একটি linked list (chaining), আর প্রতিটি element-এ তিনটি জিনিস: virtual page number, frame number, আর পরের element-এর pointer. Hash table-এর মৌলিক ধারণা DSA কোর্সের hash table পর্বে আছে।

translate(vpn, offset):
    bucket = hash(vpn) mod TABLE_SIZE
    for e in chain[bucket]:          // usually 1 or 2 elements
        if e.vpn == vpn:
            return e.frame * PAGE_SIZE + offset
    page_fault(vpn)                   // not in the table: not mapped / not in memory

সুবিধা: টেবিলের আকার address space-এর আকারের ওপর নির্ভর করে না — ৪৮ bit হোক বা ৬৪ bit, ব্যবহৃত page-এর সংখ্যার সমানুপাতিক। ভালো hash function আর যথেষ্ট বড় table হলে গড়ে এক-দুটি তুলনাতেই পাওয়া যায়। অসুবিধা: chain লম্বা হলে খোঁজা ধীর; প্রতিটি element-এ vpn আর pointer রাখতে হয় বলে entry বড়; আর পাশাপাশি page-এর entry মেমরিতে ছড়িয়ে থাকে, cache-বান্ধব নয়।

Clustered page table এই শেষ সমস্যার উত্তর: প্রতিটি hash element একটির বদলে পরপর কয়েকটি page-এর (যেমন ১৬টি) অনুবাদ রাখে। প্রোগ্রাম সাধারণত পাশাপাশি page একসাথে ব্যবহার করে, তাই একটি খোঁজায় অনেকটা ঢাকা যায়, আর vpn-tag ও pointer-এর খরচ ১৬ ভাগে ভাগ হয়। ১৯৯০-এর দশকের মাঝামাঝি ৬৪-bit address space-এর জন্য এটি গবেষণায় প্রস্তাবিত হয়েছিল। এটি বিশেষত sparse অথচ গুচ্ছবদ্ধ (clustered) ব্যবহারে ভালো।

৯ · Inverted page table — প্রতিটি frame-এ একটি entry

এখন পর্যন্ত সব টেবিল ছিল প্রতি প্রসেসে একটি, আর virtual page দিয়ে সাজানো। Inverted page table উল্টো দিক থেকে ভাবে: পুরো সিস্টেমে একটিই টেবিল, physical মেমরির প্রতিটি frame-এর জন্য একটি entry. Entry i বলে “frame i-তে এখন কোন প্রসেসের কোন page আছে”: <pid, vpn>. টেবিলের আকার তাই RAM-এর আকারের সমানুপাতিক — প্রসেস যতই হোক, address space যত বড়ই হোক। ৮ GiB RAM আর ৪ KiB frame-এ ২০,৯৭,১৫২টি entry; প্রতিটি ৮ byte হলে মোট ১৬ MiB, পুরো সিস্টেমের জন্য।

সমস্যা: অনুবাদ করতে হয় <pid, vpn> থেকে frame-এ, অথচ টেবিল সাজানো frame দিয়ে। সরলভাবে পুরো টেবিল খুঁজতে হতো — ২০ লাখ entry, প্রতিটি মেমরি-ছোঁয়ায়! তাই বাস্তবে সঙ্গে থাকে একটি hash: <pid, vpn>-কে hash করে একটি ছোট hash anchor table-এ যাওয়া, যা বলে কোন frame-এর entry থেকে খোঁজা শুরু করতে হবে; তারপর একই hash-এর entry-গুলো একটি chain-এ জোড়া। আর অবশ্যই TLB প্রথমে দেখা হয়; inverted table-এ যাওয়া কেবল TLB miss-এ।

1 pid 2, VA 0x5ABC -> vpn 5, offset 0xABC2 h = (pid + vpn) mod 8 = 7 -> HAT[7] = frame 33 IPT[3] = <3, 4> no match -> next = 24 IPT[2] = <1, 6> no match -> next = 15 IPT[1] = <2, 5> MATCH -> frame = index 16 pid 2, vpn 9: h = 3 -> IPT[0] <1,0x12>, end -> FAULT
অবস্থা (ধাপের শেষে)
খোঁজা হচ্ছে
এখন দেখা entry
মেমরি-পড়া (টেবিল)
ফল
  1. খোঁজার চাবি। প্রসেস ২ virtual address 0x5ABC চাইল, TLB-তে মেলেনি। Page number ৫, offset 0xABC. Inverted table-এ চাবি হলো জোড়া <pid, vpn> = <২, ৫> — কারণ টেবিল সব প্রসেসের যৌথ।
  2. Hash. উদাহরণের জন্য সরল hash: (pid + vpn) mod ৮ = ৭. Hash anchor table-এর ঘর ৭ বলছে: chain শুরু frame ৩-এর entry থেকে (প্রথম মেমরি-পড়া)। খেয়াল করুন, <৩, ৪>, <১, ৬> আর <২, ৫> — তিনটিরই hash ৭, তাই তারা এক chain-এ।
  3. প্রথম entry — মেলেনি। IPT[৩] = <৩, ৪>: pid আলাদা। next = ২ (দ্বিতীয় পড়া)।
  4. দ্বিতীয় entry — মেলেনি। IPT[২] = <১, ৬>: এবারও না। next = ১ (তৃতীয় পড়া)। Chain যত লম্বা, miss তত দামি — ভালো hash আর বড় anchor table এজন্যই জরুরি।
  5. মিলেছে — frame = entry-র নিজের নম্বর। IPT[১] = <২, ৫> (চতুর্থ পড়া)। এখানেই “inverted”-এর মজা: entry-তে frame number লেখা থাকে না, entry-র অবস্থান (index ১)-ই frame number. PA = ১ × ৪০৯৬ + 0xABC = 0x1ABC. এরপর অনুবাদ TLB-তে, আর ডেটা পড়া।
  6. না পেলে — page fault. এবার <২, ৯>: hash ১১ mod ৮ = ৩, HAT[৩] = ০, IPT[০] = <১, 0x12> মেলে না, chain শেষ। Page মেমরিতে নেই — page fault. কিন্তু খেয়াল করুন: page-টি ডিস্কে কোথায় আছে, সেই তথ্য inverted table-এ নেই (এতে শুধু মেমরিতে থাকা page). তাই OS-কে প্রতিটি প্রসেসের জন্য আলাদা একটি সাধারণ টেবিলও রাখতে হয় — যা অনেকটা লাভ খেয়ে ফেলে।

Sharing-এর দাম। দুটি প্রসেস একটি library বা shared memory একই frame-এ ভাগ করতে চাইলে সাধারণ টেবিলে কোনো সমস্যা নেই — দুজনের টেবিলে একই frame number. কিন্তু inverted table-এ প্রতিটি frame-এর একটিই entry, তাতে একটিই <pid, vpn> ধরে। সমাধানগুলো জোড়াতালির: এক সময়ে শুধু একজনের mapping রেখে অন্যজন ছুঁলে fault নিয়ে বদলে দেওয়া, entry-তে একাধিক mapping-এর chain রাখা, বা pid-এর বদলে একটি যৌথ “segment” পরিচয় ব্যবহার করা (IBM-এর নকশাগুলোতে এমন কিছু দেখা যায়)। ফলে shared memory ভারী কাজে inverted table অসুবিধাজনক।

বাস্তবে কোথায়? IBM-এর RT PC আর পরের POWER/PowerPC পরিবার hashed/inverted ধাঁচের টেবিলের জন্য পরিচিত (PowerPC-র “hashed page table”); Intel-এর IA-64 (Itanium)-এ OS-এর পছন্দে একটি hash-ভিত্তিক টেবিল (VHPT) ছিল, আর HP-র PA-RISC-ও inverted ধাঁচ ব্যবহার করত। বিস্তারিত প্রজন্মভেদে আলাদা, তাই এগুলোকে “ধাঁচ” হিসেবেই মনে রাখুন। আজকের x86-64 আর ARM64 দুটোই multilevel (radix) টেবিল ব্যবহার করে।

তুলনা — কোনটি কখন

ধরনটেবিলের আকারTLB miss-এ খোঁজাSharingকোথায়
সমতল (এক স্তর)পুরো address space-এর সমানুপাতিক; ৬৪-bit-এ অসম্ভব১টি পড়াসহজছোট ৩২-bit বা embedded সিস্টেম, পাঠ্যবই
Multilevelব্যবহৃত অংশের সমানুপাতিক (page-এর টুকরোয়)L টি পড়া (৪–৫)সহজ (একই frame, এমনকি একই নিচের টেবিল)x86, x86-64, ARM64, RISC-V
Hashed (chaining)ব্যবহৃত page-এর সমানুপাতিকগড়ে ১–২টি, খারাপ হলে বেশিসহজ৬৪-bit-এর বিকল্প নকশা; clustered রূপে গবেষণায়
Inverted (+ hash)physical RAM-এর সমানুপাতিক, পুরো সিস্টেমে একটিhash + chainকঠিনIBM PowerPC ধাঁচ, IA-64-এর বিকল্প, PA-RISC

আরও গভীরে: Virtual machine-এ অনুবাদ দুই স্তরের: guest-এর virtual → guest-এর “physical”, তারপর সেটি → host-এর আসল physical (Intel-এ EPT, AMD-তে NPT, যাকে সাধারণভাবে nested paging বলে)। দুই দিকেই চার স্তর হলে একটি TLB miss-এ সবচেয়ে খারাপ ক্ষেত্রে ২৪টি মেমরি-পড়া লাগতে পারে — (৪ + ১) × (৪ + ১) − ১. এজন্যই VM-এ huge page আর বড় TLB-র মূল্য আরও বেশি (পর্ব ৫০)। Linux-এর ভেতরে page table-এর স্তরগুলো সাধারণ নামে (pgd, p4d, pud, pmd, pte) লেখা, যাতে একই কোড চার আর পাঁচ স্তরে, এমনকি দুই স্তরের CPU-তেও চলে — অনুপস্থিত স্তরগুলো “ভাঁজ করে” (folded) রাখা হয়। আর PowerPC-তে Linux নিজের radix-ধাঁচের টেবিল রাখে, hashed page table-কে তার একটি cache হিসেবে ব্যবহার করে; IBM-এর POWER9 থেকে CPU-তে radix অনুবাদের মোডও এসেছে।

১০ · AI-এর যুগে TLB আর page table

এই বিষয়ের numerical আর “আমার প্রোগ্রাম ধীর কেন” — দুই ধরনের প্রশ্নই মানুষ AI-কে করে। কয়েকটি ভুল বারবার দেখা যায়:

AI-এর ভুলকেন ভুলকীভাবে যাচাই করবেন
EAT-এ hit-এ t গোনা, miss-এ বাদ (বা উল্টো)দুই রীতি মিশে গেছেকোন রীতি ধরছে লিখতে বলুন; উপরের calculator-এ মেলান
L স্তরে miss-এর সময় L·mডেটা পড়াসহ (L + 1)·mStepper-এর মতো একটি walk হাতে গুনুন
“Inverted page table প্রতিটি প্রসেসের জন্য একটি”পুরো সিস্টেমে একটি, frame প্রতি entryসংজ্ঞাটি পাঠ্যবইয়ে মিলিয়ে নিন
“x86-এ TLB miss OS-এর handler সামলায়”x86-এ hardware walker; OS আসে শুধু page fault-এCPU-র architecture manual-এর paging অধ্যায়
“আধুনিক CPU-তে প্রতিটি context switch-এ পুরো TLB মুছে যায়”PCID/ASID থাকলে দরকার নেই (পর্ব ৯)CPU-র flag আর kernel ডকুমেন্টেশন দেখুন
“গতি বাড়াতে sudo দিয়ে THP always করে দিন”কিছু workload-এ latency আর মেমরি বাড়ে; পরিবর্তন সিস্টেম-ব্যাপীআগে মাপুন, test মেশিনে চেষ্টা করুন, সফটওয়্যারের নিজস্ব পরামর্শ পড়ুন

Numerical দেওয়ার সময় রীতি আর ধাপ স্পষ্ট করে চান, যাতে প্রতিটি সংখ্যা যাচাই করা যায়:

একটি paging সিস্টেমে main memory access time m = 100 ns, TLB lookup time t = 20 ns, TLB hit ratio 98%. ১. এক স্তরের page table-এ effective access time বের করো দুই রীতিতে: (ক) hit-এ t + m, miss-এ t + 2m; (খ) t নগণ্য ধরে hit-এ m, miss-এ 2m. প্রতিটি গুণ-যোগ আলাদা লাইনে দেখাও। ২. চার স্তরের টেবিলে (ক) রীতিতে EAT কত? miss-এ মোট কয়বার মেমরি পড়া হয়, কারণসহ বলো। ৩. EAT ≤ 130 ns রাখতে এক স্তরে (ক) রীতিতে ন্যূনতম hit ratio কত? ৪. কোথাও অনুমান করলে স্পষ্ট বলো, আর আমি যেন হাতে মিলিয়ে নিতে পারি এমনভাবে লেখো।

AI-এর উত্তর মানার আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • TLB কী, আর কেন লাগে? — সাম্প্রতিক page-অনুবাদের ছোট, দ্রুত associative cache; না থাকলে প্রতিটি ছোঁয়ায় page table পড়তে বাড়তি মেমরি-পড়া লাগে।
  • m = ১০০ ns, t = ২০ ns, h = ৮০% — EAT? — ০.৮ × ১২০ + ০.২ × ২২০ = ১৪০ ns (t গুনে); t নগণ্য ধরলে ১২০ ns.
  • ৩২-bit, ৪ KiB page, ৪ byte PTE — সমতল টেবিল কত বড়, two-level-এ ভাগ কেমন? — ২২০ × ৪ = ৪ MiB; ১০/১০/১২, কারণ একটি page-এ ১০২৪টি PTE.
  • Inverted page table-এর সুবিধা ও অসুবিধা? — আকার RAM-এর সমানুপাতিক, সিস্টেমে একটি; কিন্তু খোঁজা ধীর (hash লাগে) আর shared page কঠিন।
  • Huge page কীভাবে সাহায্য করে? — TLB reach বাড়ায় (একটি entry ২ MiB ঢাকে) আর page walk-এ স্তর কমায়; দাম internal fragmentation.

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Atlas-এর প্রতিটি physical page-এর জন্য একটি associative register ছিল। এটি কি আসলে একটি “inverted page table” ছিল? তাহলে পরের দশকগুলোতে পৃথিবী কেন উল্টো দিকে — প্রতি প্রসেসের টেবিল আর ছোট TLB-র দিকে — গেল?

চিন্তাটি অনেকটাই ঠিক। ১৯৬০-এর দশকের শুরুতে Manchester বিশ্ববিদ্যালয় আর Ferranti-র তৈরি Atlas-এ main memory ছিল ছোট core store, আর তার পেছনে বড় ধীর drum. প্রতিটি core-page-এর জন্য একটি register রাখা হতো, যাতে লেখা থাকত এই মুহূর্তে সেখানে কোন virtual page আছে; একটি ঠিকানা এলে hardware সব register একসাথে মিলিয়ে দেখত। অর্থাৎ entry ছিল frame প্রতি, আর খোঁজা হতো page number দিয়ে — ঠিক inverted table-এর মতো, শুধু পুরোটাই associative hardware-এ। মেমরিতে মাত্র কয়েক ডজন page থাকায় এটি সম্ভব ছিল।

সমস্যা এল মেমরি বড় হতেই। Associative তুলনার circuit প্রতিটি entry-র পাশে লাগে; হাজার বা লাখো frame-এর জন্য তা অসম্ভব দামি আর ধীর। তাই নকশাকারেরা দুই ভাগে ভাগ করলেন: সব অনুবাদের “সত্য” রাখা হলো সাধারণ মেমরির টেবিলে (সস্তা, বড়), আর তার সাম্প্রতিক অল্প কিছু অংশ রাখা হলো ছোট associative cache-এ — TLB. এটি কম্পিউটার নকশার একটি চিরন্তন ধাঁচ: বড় ধীর সঞ্চয় + ছোট দ্রুত cache, locality-র ওপর ভরসা করে।

প্রতি-প্রসেস টেবিল কেন জিতল? Multiprogramming আর time-sharing (১৯৬০-এর দশকের শেষ থেকে) বাড়ার সাথে সাথে প্রতিটি প্রসেসের নিজের address space দরকার হলো — সুরক্ষা, আলাদা ঠিকানা, আর সহজে মেমরি ভাগ করা (একই frame দুই টেবিলে)। প্রতি-প্রসেস টেবিলে এগুলো স্বাভাবিক; inverted ধাঁচে sharing কঠিন, আর প্রতিটি entry-তে pid রাখতে হয়। তবু inverted ধারণা মরে যায়নি: IBM-এর ৮০-৯০-এর দশকের RISC নকশায় আর PA-RISC-এ এটি ফিরে আসে, কারণ তাদের বিশাল (৬৪-bit বা segment-ভিত্তিক) virtual address space-এ প্রতি-প্রসেস সমতল টেবিল অসম্ভব ছিল, আর RAM-আকারের একটি টেবিল আকর্ষণীয় লাগছিল। শেষ পর্যন্ত x86-64 আর ARM64 multilevel টেবিল আর বড় TLB দিয়ে সেই সমস্যা অন্যভাবে মেটায় — sparse অংশের টেবিল না বানিয়ে।

মূল উপলব্ধি: একই ধারণা প্রযুক্তির দাম অনুযায়ী বারবার ফিরে আসে — associative hardware যখন যথেষ্ট, তখন “frame প্রতি entry” সহজ; মেমরি বড় হলে “সত্য টেবিলে, দ্রুত অংশ cache-এ” — TLB-র জন্ম সেখান থেকেই।

প্র ০২MIPS software দিয়ে TLB ভরাত, x86 hardware দিয়ে। নমনীয়তা বনাম গতির এই লড়াইয়ে শেষ পর্যন্ত hardware walker কেন প্রায় সর্বত্র জিতল? Virtual machine-এর যুগে এই সিদ্ধান্তের দাম কী?

১৯৮০-এর দশকের RISC আন্দোলনের দর্শন ছিল: hardware সরল রাখা, জটিল কাজ software-কে দেওয়া। MIPS-এর নকশায় TLB miss একটি exception, আর OS-এর একটি ছোট handler নিজের টেবিল থেকে entry লিখে দেয়। লাভ বাস্তব — OS যেকোনো টেবিল-গঠন বেছে নিতে পারে (সমতল virtual টেবিল, hash, inverted), CPU-র নকশা সরল থাকে, আর গবেষকেরা নতুন মেমরি-ব্যবস্থাপনা পরীক্ষা করতে পারেন। সে যুগে workload ছোট ছিল, TLB miss বিরল, তাই handler-এর কয়েক ডজন instruction-এর খরচ সহ্য করা যেত।

সময়ের সাথে তিনটি জিনিস বদলাল। প্রথমত, মেমরি আর ডেটা বিশাল হলো, TLB reach-এর তুলনায় working set অনেক বড় — miss ঘন ঘন। দ্বিতীয়ত, CPU-র pipeline গভীর আর out-of-order হলো; একটি exception মানে pipeline খালি করে আবার শুরু, যার খরচ অনেক বেড়ে গেল, অথচ hardware walker miss চলাকালীন অন্য কাজ চালিয়ে যেতে পারে, এমনকি একসাথে একাধিক walk করতে পারে। তৃতীয়ত, বাজার: x86-এর বিশাল সফটওয়্যার-ভিত্তি আর ARM-এর মোবাইল-আধিপত্য — দুটোই hardware walker নিয়ে এসেছে। নমনীয়তার মূল্য কমল, কারণ প্রায় সব OS-ই এখন radix (multilevel) টেবিলেই সন্তুষ্ট।

Virtual machine-এ এই সিদ্ধান্তের একটি পরিণাম দেখা যায়। প্রথম দিকে hypervisor-কে “shadow page table” রাখতে হতো — guest-এর টেবিল দেখে দেখে আসল অনুবাদের একটি কপি, যা জটিল আর guest টেবিল বদলালেই দামি। পরে Intel (EPT) আর AMD (NPT) hardware-এ দুই স্তরের walk যোগ করে: guest-এর টেবিল হাঁটার প্রতিটি ধাপের ঠিকানাও host-এর টেবিল দিয়ে অনুবাদ হয়। দুই দিকে চার স্তর হলে সবচেয়ে খারাপ ক্ষেত্রে ২৪টি পড়া। Hardware এই জটিলতা লুকিয়ে রাখে, কিন্তু খরচ লুকায় না — এজন্যই cloud-এ huge page, বড় TLB আর paging-structure cache এত গুরুত্বপূর্ণ।

মূল উপলব্ধি: কাজ hardware না software করবে, সেই উত্তর প্রযুক্তির খরচের অনুপাতের ওপর নির্ভর করে; exception দামি আর workload বড় হওয়ায় hardware walker জিতেছে, কিন্তু virtualization সেই walk-এর খরচকে আবার সামনে এনেছে।

প্র ০৩x86-64 কেন পুরো ৬৪ bit না নিয়ে ৪৮ bit আর “canonical” ঠিকানার নিয়ম বেছে নিল? পাঁচ স্তরে যাওয়ার সিদ্ধান্তে কী লাভ, কী দাম?

পুরো ৬৪ bit virtual address মানে ৫২ bit-এর page number. ৯ bit করে ভাগ করলে প্রায় ছয় স্তর — প্রতিটি TLB miss-এ ছয়-সাতবার মেমরি। ২০০০-এর দশকের শুরুতে AMD যখন x86-এর ৬৪-bit সম্প্রসারণ নকশা করে, তখন ২৫৬ TiB virtual মেমরি কল্পনাতীত বড় লাগছিল; তাই চার স্তর, ৪৮ bit — যথেষ্ট বড়, অথচ walk ছোট। বাকি ১৬ bit-কে bit ৪৭-এর কপি হতে বাধ্য করার (canonical) চালাকিটি দূরদর্শী: কেউ যেন সেই bit-গুলোতে গোপনে অন্য তথ্য (যেমন pointer-এর ট্যাগ) না রাখে, যাতে ভবিষ্যতে address space বাড়ালে পুরোনো প্রোগ্রাম না ভাঙে। Non-canonical ঠিকানা ছুঁলেই fault হয়।

প্রায় দেড় দশক পরে চাপ এল। কয়েক TB RAM-এর server, বিশাল in-memory database, আর persistent memory বা বড় ফাইলকে মেমরিতে map করার চাহিদা — ১২৮ TiB-এর user অংশও কোথাও কোথাও সংকীর্ণ লাগছিল, কারণ virtual address space শুধু RAM নয়, ফাঁকা জায়গা আর map করা ফাইলও ধরে। Intel পাঁচ স্তরের (LA57) নকশা প্রকাশ করে, আর Linux ২০১৭ সালের দিকে তা সমর্থন করে। Canonical নিয়মের জন্য পরিবর্তনটি মসৃণ: এখন bit ৫৬-এর কপি হতে হয়, আর আগের ৪৮-bit ঠিকানা স্বাভাবিকভাবেই বৈধ থাকে।

দাম দুই রকম। এক, প্রতিটি TLB miss-এ এক স্তর বেশি — যদিও উপরের EAT simulator দেখিয়েছে, hit ratio বেশি হলে পার্থক্য সামান্য, আর ওপরের স্তরের entry সাধারণত cache-এ থাকে। দুই, সামঞ্জস্য: কিছু প্রোগ্রাম (যেমন কিছু JavaScript engine বা language runtime) pointer-এর ওপরের অব্যবহৃত bit-এ তথ্য রাখার কৌশল নেয়। তাই Linux সাবধানী: প্রোগ্রাম নিজে উঁচু ঠিকানা না চাইলে mmap তাকে ৪৭-bit-এর নিচেই ঠিকানা দেয়। এটি একটি ভালো নকশা-শিক্ষা — নতুন ক্ষমতা যোগ করুন, কিন্তু পুরোনো অনুমান ভাঙবেন না, “opt-in” রাখুন।

মূল উপলব্ধি: ঠিকানার bit-সংখ্যা একটি ভবিষ্যৎ-বাজি; ৪৮ bit আর canonical নিয়ম walk ছোট রেখেও বাড়ার পথ খোলা রেখেছিল, আর পাঁচ স্তর এসেছে opt-in হিসেবে, যাতে পুরোনো সফটওয়্যার না ভাঙে।

প্র ০৪Huge page TLB reach বহু গুণ বাড়ায়। তবু কেন কিছু database আর সফটওয়্যারের ডকুমেন্টেশন বহু বছর ধরে transparent huge page বন্ধ রাখার পরামর্শ দিয়েছে? “সবসময় বড় page” কেন সহজ উত্তর নয়?

অংশ ৫-এর হিসাবে huge page-এর লাভ পরিষ্কার: একই TLB-তে ৫১২ গুণ বেশি মেমরি ঢাকা যায়, আর page walk-এ এক স্তর কম। বড় array, matrix বা VM-এর মেমরির মতো ঘন, একটানা ব্যবহারে লাভ বড়। তাহলে সমস্যা কোথায়? Linux-এর transparent huge page (THP) প্রোগ্রামকে কিছু না জানিয়ে huge page দেয়, আর এর জন্য kernel-কে কয়েকটি কাজ করতে হয় যেগুলো সবসময় সস্তা নয়।

প্রথমত, একটানা ২ MiB খালি physical মেমরি লাগে। অনেক দিন চলা সিস্টেমে মেমরি টুকরো টুকরো হয়ে যায়; তখন kernel হয় memory compaction চালায় (page সরিয়ে জায়গা বানায়) নয়তো পিছিয়ে যায়। Page fault-এর সময় এই compaction হঠাৎ কয়েক মিলিসেকেন্ডের দেরি আনতে পারে — একটি database বা cache সার্ভারের tail latency-তে এটি চোখে পড়ার মতো। দ্বিতীয়ত, internal fragmentation আর মেমরির ফোলা: যে প্রোগ্রাম মেমরির ছোট ছোট অংশ এলোমেলোভাবে ছোঁয়, সে ৪ KiB-এর জায়গায় পুরো ২ MiB পেয়ে মোট মেমরি অনেক বাড়িয়ে ফেলতে পারে। তৃতীয়ত, fork আর copy-on-write (পর্ব ৩৫): একটি byte বদলালে পুরো ২ MiB কপি করতে হতে পারে। Redis-এর মতো সফটওয়্যার, যা snapshot নিতে fork ব্যবহার করে, এজন্য দীর্ঘদিন THP বন্ধ রাখার পরামর্শ দিয়েছে; কিছু অন্য database-ও একই ধরনের সতর্কতা দিয়েছে (সংস্করণভেদে পরামর্শ বদলেছে, তাই সবসময় সেই সফটওয়্যারের বর্তমান ডকুমেন্টেশন দেখুন)।

Kernel-এর দিক থেকেও উন্নতি হয়েছে — madvise মোড (শুধু যে চায়, সে পায়, যা Wandbox-এর মেশিনে দেখেছেন), পেছনে ধীরে ধীরে page জোড়া দেওয়ার khugepaged, আর সাম্প্রতিক বছরগুলোতে ২ MiB আর ৪ KiB-এর মাঝামাঝি আকারের “folio”/mTHP-এর দিকে কাজ। Database-গুলো প্রায়ই নিজের বড় buffer-এর জন্য আলাদাভাবে সংরক্ষিত hugetlbfs page চায় — নিয়ন্ত্রণ নিজের হাতে রাখে।

মূল উপলব্ধি: বড় page একটি trade-off — TLB-র লাভের বিপরীতে fragmentation, compaction-এর দেরি আর copy-on-write-এর খরচ; কোন workload-এ কোনটি ভারী, তা মেপে ঠিক করতে হয়, অনুমানে নয়।

প্র ০৫Inverted আর hashed page table-এর আকার address space-এর ওপর নির্ভর করে না — ৬৪-bit যুগে এটি তো আদর্শ মনে হয়। তবু x86-64 আর ARM64 কেন radix (multilevel) টেবিলই বেছে নিল, আর IBM-ও কেন POWER-এ radix মোড আনল?

কাগজে inverted table অসাধারণ: ৮ GiB RAM-এ পুরো সিস্টেমের জন্য ১৬ MiB, প্রসেস হাজারটা হোক বা ৬৪-bit ঠিকানা হোক। কিন্তু একটি OS শুধু অনুবাদ করে না। তাকে প্রতিটি প্রসেসের পুরো virtual মেমরির হিসাব রাখতে হয় — কোন page ডিস্কে কোথায়, কোনটি এখনো তৈরিই হয়নি, কোনটি copy-on-write. Inverted table-এ শুধু মেমরিতে থাকা page থাকে, তাই OS-কে পাশে প্রতি-প্রসেস আরেকটি কাঠামো রাখতেই হয়। তখন inverted table হয়ে দাঁড়ায় একটি বড় “cache” — Linux PowerPC-তে hashed page table ঠিক এভাবেই ব্যবহার করে।

দ্বিতীয় সমস্যা locality আর hardware. Radix টেবিলে পাশাপাশি virtual page-এর PTE মেমরিতেও পাশাপাশি — একটি cache line-এ ৮টি PTE, আর ওপরের স্তরের entry প্রায় সবসময় cache-এ থাকে। Hash table ইচ্ছা করেই পাশাপাশি key-কে দূরে ছড়ায়, তাই prefetch বা cache-এর সুবিধা কম, আর collision chain অনুমান-অযোগ্য দেরি আনে। Hardware walker-এর জন্য radix-এর নির্দিষ্ট, সীমিত সংখ্যক ধাপ নকশা করা সহজ; huge page-ও স্বাভাবিকভাবে আসে (মাঝের স্তরে থেমে যাওয়া)। Sharing-ও radix-এ সরল — একই frame দুই টেবিলে, এমনকি একটি নিচের-স্তরের টেবিল দুই প্রসেস ভাগ করতে পারে।

তৃতীয়ত, OS-এর সফটওয়্যার-কাঠামো। Linux, Windows আর BSD-র মেমরি-ব্যবস্থাপনা কোড দশকের পর দশক radix ধারণায় লেখা; x86 আর ARM সেটিকে hardware-এ সরাসরি সমর্থন করে, ফলে দুই স্তরে একই কাঠামো — কোনো অনুবাদ-স্তর লাগে না। IBM-এর POWER9 (ISA ৩.০)-এ radix অনুবাদের মোড আসাকে এই বাস্তবতার স্বীকৃতি হিসেবেই দেখা হয়, যদিও hashed মোডও রাখা হয়েছে। Inverted ধারণা অবশ্য অন্য রূপে বেঁচে আছে: OS-এর “reverse mapping” (কোন frame কোন প্রসেস ব্যবহার করছে — page সরানোর সময় দরকার) মূলত frame প্রতি একটি হিসাব।

মূল উপলব্ধি: শুধু টেবিলের আকার দিয়ে নকশা বিচার করা যায় না — locality, hardware-এর সরলতা, sharing আর OS-এর বাকি কাঠামোর সাথে মিল মিলিয়ে radix টেবিল জিতেছে; inverted ধারণা বেঁচে আছে reverse mapping-এর মতো সহায়ক কাঠামোয়।

অনুশীলন

হাতে হিসাব, আউটপুট অনুমান, ঠিকানা অনুবাদ আর নকশা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব: m = ৮০ ns, t = ১০ ns, TLB hit ratio ৯৫%. (ক) এক স্তরের টেবিলে EAT, দুই রীতিতে। (খ) তিন স্তরের টেবিলে EAT (t গুনে)। (গ) তিন স্তরে EAT ৯৫ ns-এর মধ্যে রাখতে ন্যূনতম hit ratio কত?

    (ক) t গুনে: ০.৯৫ × (১০ + ৮০) + ০.০৫ × (১০ + ১৬০) = ৮৫.৫ + ৮.৫ = ৯৪ ns. t নগণ্য ধরে: ০.৯৫ × ৮০ + ০.০৫ × ১৬০ = ৭৬ + ৮ = ৮৪ ns.

    (খ) Miss-এ t + ৪m = ১০ + ৩২০ = ৩৩০ ns. EAT = ০.৯৫ × ৯০ + ০.০৫ × ৩৩০ = ৮৫.৫ + ১৬.৫ = ১০২ ns.

    (গ) সরল রূপ: EAT = t + m + (1 − h) × ৩m = ৯০ + (1 − h) × ২৪০ ≤ ৯৫ ⟹ 1 − h ≤ ৫/২৪০ ≈ ০.০২০৮ ⟹ h ≥ ০.৯৭৯ (প্রায় ৯৭.৯%). যাচাই: h = ০.৯৮ হলে ৯০ + ০.০২ × ২৪০ = ৯৪.৮ ns ✓. (EAT simulator-এ m = 80, t = 10 বসিয়ে মিলিয়ে নিতে পারেন।)

  2. Page table-এর নকশা: একটি ৩২-bit সিস্টেমে page size ৮ KiB, PTE ৪ byte. (ক) Offset কত bit, সমতল টেবিলে কতটি entry আর কত বড়? (খ) প্রতিটি নিচের-স্তরের টেবিল যেন ঠিক একটি page-এ আঁটে, এমন two-level বিভাজন দিন।

    (ক) ৮ KiB = ২১৩, তাই offset ১৩ bit; page number ৩২ − ১৩ = ১৯ bit, entry ২১৯ = ৫,২৪,২৮৮টি; টেবিল ২১৯ × ৪ = ২২১ byte = ২ MiB.

    (খ) একটি page-এ ৮১৯২ ÷ ৪ = ২০৪৮ = ২১১টি PTE, তাই ভেতরের index ১১ bit. বাকি ১৯ − ১১ = ৮ bit outer index-এর। বিভাজন ৮/১১/১৩. Outer table-এ ২৫৬টি entry (১ KiB, page-এর চেয়ে ছোট — সমস্যা নেই)। প্রতিটি inner table ২০৪৮ × ৮ KiB = ১৬ MiB virtual মেমরি ঢাকে।

  3. আউটপুট অনুমান: Two-level walker simulator-এ tlb.size === 4 বদলে tlb.size === 8 করলে শেষ দুই লাইন কী হবে? আর শেষ ঠিকানা 0x00800200-এর লাইন কী দেখাবে? চালানোর আগে হিসাব করুন।

    Trace-এ মোট আলাদা valid page মাত্র ৫টি (0x00400, 0x00403, 0xBFFFF, 0x00800, 0x00801), যা ৮টি ঘরে এঁটে যায় — তাই আর কোনো capacity miss নেই, শুধু ৫টি cold miss আর ১টি fault. Hit = ১৬ − ৬ = ১০. মেমরি-পড়া = ১০ × ১ + ৫ × ৩ + ১ = ২৬. আসল আউটপুট:

    00800200 2/0/200 hit  -> PA 000C3200
    accesses 16, TLB hits 10, misses 6 (faults 1)
    hit ratio 62.5%, memory reads 26 (1.63 per access; without a TLB 46)

    শিক্ষা: TLB বাড়ালে cold miss কমে না — প্রথমবারের ছোঁয়া সবসময় miss. কমে শুধু capacity miss, যা working set TLB-র চেয়ে বড় হলে আসে।

  4. হাতে অনুবাদ: একই two-level টেবিল ধরে (outer[২] → inner frame 0x2C, তাতে entry ০ → 0x0C3, entry ১ → 0x0C4; outer[৭৬৭] → 0x2D, তাতে entry ১০২৩ → 0x07E) নিচের ঠিকানাগুলোর p1, p2, offset আর physical address বের করুন: (ক) 0x00801008 (খ) 0xBFFFF123 (গ) 0x00802000.

    (ক) 0x00801008 ÷ ২২২ = ২ (0x00800000 = ২ × ৪ MiB); (0x00801008 ÷ ২১২) mod ১০২৪ = 0x801 mod 0x400 = ১; offset 0x008. outer[২] → 0x2C, entry ১ → 0x0C4. PA 0x000C4008 — simulator-এর সপ্তম লাইনের সাথে মেলে।

    (খ) 0xBFFFF123 ÷ 0x400000 = 0x2FF = ৭৬৭; page number 0xBFFFF, mod 0x400 = 0x3FF = ১০২৩; offset 0x123. → 0x2D → 0x07E. PA 0x0007E123.

    (গ) 0x00802000: p1 = ২, p2 = 0x802 mod 0x400 = ২, offset 0x000. Inner table 0x2C আছে, কিন্তু তার entry ২ invalid — দুটি মেমরি-পড়ার পর page fault. খেয়াল করুন, 0x00C00000-এর fault একটি পড়াতেই ধরা পড়েছিল (outer entry-ই নেই); এখানে দুটি।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমx86-64-এর মতো hardware-managed TLB-তে একটি মেমরি-ছোঁয়ায় TLB miss হলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Virtual address থেকে page number আর offset আলাদা করে TLB-তে খোঁজা — মেলেনি
  2. CR3 থেকে শুরু করে hardware walker ওপরের স্তরগুলোর entry পড়ে
  3. শেষ স্তরের PTE পড়ে valid কি না আর অনুমতি দেখা
  4. অনুবাদটি TLB-তে রাখা (দরকারে একটি পুরোনো entry বাদ দিয়ে)
  5. Frame number আর offset জুড়ে physical address থেকে ডেটা পড়া

Main memory access time ১০০ ns, TLB lookup time ২০ ns, hit ratio ৮০%, এক স্তরের page table. TLB lookup-এর সময় দুই পথেই গুনলে effective access time কত?

  • ১২০ ns
  • ১৪০ ns
  • ১২০ ns, কারণ hit আর miss-এ সময় সমান
  • ২০০ ns

৩২-bit virtual address, ৪ KiB page আর ৪ byte page table entry হলে একটি প্রসেসের সমতল (এক স্তরের) page table কত বড়?

  • ৪ KiB
  • ১ MiB
  • ৮ MiB
  • ৪ MiB

৩২-bit two-level page table-এ বিভাজন ১০/১০/১২. Virtual address 0x00403A7C-এর outer index, inner index আর offset কোনটি?

  • ১, ৩, 0xA7C
  • ০, 0x403, 0xA7C
  • ১, ০, 0x3A7C
  • ৩, ১, 0xA7C

একটি TLB-তে ১৫৩৬টি entry. Page size ৪ KiB থেকে ২ MiB করলে TLB reach কত থেকে কত হয়?

  • বদলায় না, কারণ entry সংখ্যা একই
  • ২৫৬ KiB থেকে ১২৮ MiB
  • ৬ MiB থেকে ৩ GiB
  • ৬ MiB থেকে ৬ GiB

Inverted page table সম্পর্কে কোন কথাটি সঠিক?

  • প্রতিটি প্রসেসের আলাদা inverted table থাকে, virtual page প্রতি একটি entry
  • পুরো সিস্টেমে একটি টেবিল, physical frame প্রতি একটি entry, যাতে <pid, vpn> লেখা থাকে
  • Shared memory সবচেয়ে সহজে সামলানো যায়
  • Page number দিয়ে সরাসরি index করে একটি পড়াতেই frame পাওয়া যায়

x86-64-এর চার স্তরের page table (৯/৯/৯/৯/১২)-এ PD স্তরের একটি entry-র PS bit = ১ হলে কী বোঝায়?

  • Page-টি মেমরিতে নেই, page fault হবে
  • এটি একটি ১ GiB-এর page
  • Page-টিতে শুধু পড়া যায়
  • এটি একটি ২ MiB-এর huge page; PT স্তরে যেতে হয় না

ক্লাসিক MIPS-এর মতো software-managed TLB-তে TLB miss হলে কী ঘটে?

  • CPU একটি exception দেয়, আর OS-এর handler অনুবাদ খুঁজে TLB-তে লেখে
  • CPU-র hardware walker CR3 থেকে চার স্তর হাঁটে
  • OS সবসময় ডিস্ক থেকে page নিয়ে আসে
  • প্রোগ্রামটি segmentation fault দিয়ে বন্ধ হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. TLB: page-অনুবাদের ছোট associative cache. Hit-এ t + m, এক স্তরে miss-এ t + 2m; miss হলে x86/ARM-এ hardware walker, ক্লাসিক MIPS-এ OS-এর software handler.
  2. EAT = h(t + m) + (1 − h)(t + (L + 1)m): m = ১০০, t = ২০-এ এক স্তরে h = ৮০% → ১৪০ ns, ৯৮% → ১২২ ns (t নগণ্য ধরলে ১২০ আর ১০২); চার স্তরে ৯৮% → ১২৮ ns. কোন রীতি, তা লিখে দিন।
  3. আকার: ৩২-bit সমতল টেবিল ৪ MiB, ৪৮-bit-এ ৫১২ GiB. Two-level ১০/১০/১২ ছোট প্রসেসে ১৬ KiB; x86-64 ৯/৯/৯/৯/১২ (৪৮ bit), LA57-এ পাঁচ স্তর (৫৭ bit); ARM64 ৪ KiB granule-এ চার স্তর।
  4. Hashed আর inverted: hashed — ব্যবহৃত page প্রতি entry, chain; clustered — এক entry-তে কয়েকটি পাশাপাশি page. Inverted — frame প্রতি একটি entry, পুরো সিস্টেমে একটি, <pid, vpn> hash দিয়ে খোঁজা, sharing কঠিন।
  5. TLB reach = entry × page size: ১৫৩৬ × ৪ KiB = ৬ MiB, × ২ MiB = ৩ GiB. Huge page reach বাড়ায় আর walk ছোট করে; দাম fragmentation আর latency-র ঝুঁকি।

পরের পর্বে: Paging ছাড়াও মেমরি ভাগ করার আরেকটি পুরোনো পথ আছে — প্রোগ্রামকে তার যুক্তিসঙ্গত অংশে (code, stack, data) ভাগ করা। পর্ব ৩৪-এ segmentation আর x86-এর memory মডেল।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান