RISC বনাম CISC — Instruction Set ডিজাইনের দুই প্রতিদ্বন্দ্বী দর্শন
RISC vs CISC
CISC আর RISC দুইটা প্রতিদ্বন্দ্বী উত্তর একই প্রশ্নের — একটা instruction-এ কতটা কাজ গুঁজে দেওয়া উচিত? ইতিহাস (VAX থেকে Berkeley RISC), hardware trade-off (fixed বনাম variable length), আর সবচেয়ে চমকপ্রদ মোড় — আধুনিক 'CISC' x86-64 ভেতরে আসলে RISC-সদৃশ micro-op-এ চলে — মিলিয়ে দেখব এই বিভাজনটা আজও কেন প্রাসঙ্গিক, অথচ শুদ্ধ কৃষ্ণ-শুভ্র না।
আগে এটা বুঝি
গত লেসনের শেষ উদাহরণটা আরেকবার দেখা যাক। একই সহজ কাজ — a = b + c — RISC-V-তে কম্পাইল হলে ৪টা instruction লাগে (lw, lw, add, sw), আর x86-64-তে মাত্র ৩টা (mov, add, mov) — কারণ x86-64-র add instruction সরাসরি memory থেকে একটা operand পড়তে পারে, RISC-V-র পারে না। এই একটা ছোট পার্থক্যের পেছনে computer architecture-এর ইতিহাসের সবচেয়ে বিখ্যাত বিতর্ক লুকিয়ে আছে — আর আজকের লেসন সেটাই খুলে দেখাবে।
প্রশ্নটা এভাবে বলা যায়: একটা instruction-এ hardware-কে কতটা কাজ করতে দেওয়া উচিত? একদিকের উত্তর — যতটা সম্ভব বেশি। একটা instruction-ই memory থেকে data আনুক, তার উপর গণনা করুক, ফলাফল আবার memory-তে রাখুক — যেন একটা instruction একটা সম্পূর্ণ ছোট বাক্য প্রকাশ করে। এই দর্শনের নাম CISC — Complex Instruction Set Computer। উল্টো দিকের উত্তর — যতটা সম্ভব কম। প্রতিটা instruction একটাই সরল কাজ করুক (শুধু যোগ, শুধু memory-তে read, শুধু memory-তে write), আর জটিল কাজ কয়েকটা সরল instruction জুড়ে তৈরি হোক। এই দর্শনের নাম RISC — Reduced Instruction Set Computer।
এই দুইটা নাম শুনে মনে হতে পারে এটা নিছক একটা “কম বনাম বেশি” গণনার প্রশ্ন — instruction গুনলেই বোঝা যাবে কোনটা কী। গত লেসনের misconception সেকশনেই এই ভুলটা আগাম ভাঙা হয়েছিল: instruction সংখ্যা speed-এর মাপকাঠি না। আজ দেখব আসল প্রশ্নটা আসলে hardware-এর জটিলতা কোথায় রাখা হবে তা নিয়ে — compiler-এ, নাকি circuit-এ; আর কেন ১৯৮০-র দশকের একটা গবেষণা-ফলাফল পুরো ইন্ডাস্ট্রিকে দ্বিতীয়বার ভাবতে বাধ্য করেছিল। শেষে দেখব সবচেয়ে চমকপ্রদ মোড়টা — আজকের x86-64, যাকে আমরা এখনও “CISC” বলি, ভেতরে আসলে RISC-এরই একটা কৌশল ব্যবহার করে।
মূল ধারণা
CISC-এর জন্ম — ১৯৭০-এর memory-সীমিত জগৎ
১৯৬০-৭০-এর দশকে computer memory ছিল আজকের তুলনায় অকল্পনীয় ব্যয়বহুল আর ধীর। একটা প্রোগ্রামের বাইনারি যত বড়, তত বেশি ব্যয়বহুল memory লাগে, আর তত বেশি সময় লাগে সেই memory থেকে instruction fetch করতে (memory bandwidth-ও সীমিত ছিল)। এই বাস্তবতায় একটা স্বাভাবিক প্রকৌশল সিদ্ধান্ত ছিল: প্রতিটা instruction-এ যতটা সম্ভব বেশি কাজ গুঁজে দাও, যাতে কম instruction দিয়েই একই কাজ শেষ হয় — code density বাড়াও।
এই দর্শনের সবচেয়ে বিখ্যাত উদাহরণ DEC-এর VAX-11/780 (১৯৭৭)। VAX-এ এমন সব instruction ছিল যেগুলো আজকের চোখে অবিশ্বাস্য রকম জটিল — যেমন POLY instruction, যেটা একটা সম্পূর্ণ polynomial evaluate করত (Horner’s method-এর মতো একটা গোটা লুপ, একটাই instruction-এ!), অথবা MVCL (VAX-এর character-string move), যেটা একটা সম্পূর্ণ memory-block কপি করত ভেরিয়েবল দৈর্ঘ্যে। এই instruction-গুলো ভেতরে microcode দিয়ে বাস্তবায়িত হতো — অর্থাৎ CPU-র ভেতরে একটা ছোট, লুকানো ROM-এ প্রতিটা জটিল instruction-এর জন্য মাইক্রো-প্রোগ্রাম লেখা থাকত, আর একটা POLY instruction execute করা মানে ভেতরে সেই মাইক্রো-প্রোগ্রামের বহু ধাপ চালানো।
CISC-এর পেছনে আরেকটা যুক্তি ছিল যাকে বলা হয় semantic gap কমানো — high-level language (যেমন Pascal, C)-এর একটা statement-কে যদি hardware-এর একটা instruction-এর সাথে সরাসরি মেলানো যায় (একটা POLY instruction সরাসরি একটা polynomial-evaluation loop-এর জায়গা নেয়), তাহলে কম্পাইলার লেখা সহজ হয়, আর হাতে assembly লেখাও সহজ হয় — সেই যুগে compiler প্রযুক্তি আজকের মতো পরিণত ছিল না, তাই প্রোগ্রামাররা প্রায়ই সরাসরি assembly লিখতেন। “hardware কষ্ট করুক, প্রোগ্রামারের জীবন সহজ হোক” — এটাই ছিল যুক্তির মূল সুর।
Intel-এর x86 এই একই যুগের সন্তান। ১৯৭৮ সালে আসা Intel 8086-ও CISC দর্শনে ডিজাইন করা — variable-length instruction, memory-directly-touching arithmetic instruction, আর অনেকগুলো বিশেষায়িত instruction (যেমন LOOP, স্ট্রিং-manipulation instruction MOVS/STOS)। এই ঐতিহ্যই আজকের x86-64 পর্যন্ত চলে এসেছে — গত লেসনে যেমন দেখলেন, x86-64-র add eax, [c_addr] একই instruction-এ memory read আর addition দুটোই করে, RISC-V-র মতো আলাদা lw লাগে না।
RISC-এর জন্ম — ১৯৮০-র দশকের একটা অস্বস্তিকর পরিমাপ
১৯৭০-এর দশকের শেষে IBM, বার্কলে, আর স্ট্যানফোর্ডের গবেষকরা একটা অস্বাভাবিক প্রশ্ন করলেন: compiler-লেখা প্রোগ্রামে VAX-এর সেই জটিল instruction-গুলো আদৌ কতটা ব্যবহৃত হয়? তারা প্রকৃত compiled প্রোগ্রামের dynamic instruction trace মেপে দেখলেন — অর্থাৎ প্রোগ্রাম চলার সময় প্রতিটা instruction ঠিক কতবার execute হচ্ছে তা গুনলেন। ফলাফলটা industry-কে অবাক করেছিল।
David Patterson আর David Ditzel-এর ১৯৮০ সালের পেপার “The Case for the Reduced Instruction Set Computer” এই পরিমাপের কেন্দ্রীয় প্রমাণ তুলে ধরে: VAX-এর মতো ISA-র জটিল instruction-গুলোর (POLY, MVCL-এর মতো) ব্যবহারের হার নগণ্য। বাস্তব প্রোগ্রামে execute হওয়া instruction-এর সিংহভাগ ছিল সরল কাজ — data move (assignment), সাধারণ arithmetic, comparison, আর branch। Compiler-রা জটিল instruction-গুলো প্রায় ব্যবহারই করত না — কারণ কম্পাইলারের optimization pass-এর জন্য সরল, uniform instruction থেকে ভালো কোড generate করা সহজ ছিল, একটা মনোলিথিক জটিল instruction-এর ভেতরে optimize করার সুযোগ কম।
এই পর্যবেক্ষণ থেকে একটা যুক্তিসঙ্গত সিদ্ধান্ত এলো: যদি জটিল instruction-গুলো কম ব্যবহৃত হয়, অথচ সেগুলো বাস্তবায়ন করতে microcode-এর বিশাল জটিলতা লাগে (chip area, design সময়, verification খরচ) — তাহলে সেই জটিলতা বাদ দিয়ে বরং যেই সরল instruction-গুলো বেশি ব্যবহৃত হয়, সেগুলোকেই দ্রুততম করা উচিত। এটাই RISC-এর মূল যুক্তি — Amdahl’s Law-এর একটা প্রাথমিক প্রয়োগ বলা যায়: যে কাজ বেশি হয়, সেটাকেই optimize করো, বিরল কাজের জন্য জটিলতা বাড়িও না।
Berkeley RISC I/II (David Patterson-এর নেতৃত্বে, ১৯৮০-এর দশকের প্রথম দিকে) আর Stanford MIPS (John Hennessy-এর নেতৃত্বে) — এই দুইটা একাডেমিক প্রজেক্টই RISC দর্শনের প্রথম বাস্তব বাস্তবায়ন। তাদের মূল ডিজাইন সিদ্ধান্তগুলো:
- Uniform, fixed-length instruction — প্রতিটা instruction ঠিক একই সংখ্যক bit, যাতে decode circuit সরল হয়।
- Load-store architecture — শুধু
LOAD/STOREinstruction memory স্পর্শ করে; বাকি সব arithmetic শুধু register-এর উপর। কোনো instruction-এ memory operand সরাসরি arithmetic-এ ব্যবহার করা যায় না (RISC-V-র উদাহরণেই দেখেছেন —addশুধু register নেয়, memory না)। - বড় register file — memory access ধীর, register access দ্রুত, তাই যত বেশি ভেরিয়েবল register-এ রাখা যায়, তত ভালো। Compiler-রা register allocation-এ আরও স্বাধীনতা পান।
- Hardwired control, microcode না — যেহেতু instruction-গুলো সরল, প্রতিটার জন্য আলাদা মাইক্রো-প্রোগ্রামের দরকার নেই; সরাসরি combinational logic দিয়ে control signal তৈরি করা যায় (গত মডিউলের single-cycle datapath-এর মতোই — microcode ছাড়াই)।
- Pipelining-বান্ধব ডিজাইন — এটাই পরের সেকশনের মূল বিষয়, কারণ এইখানেই RISC-এর আসল practical সুবিধাটা সবচেয়ে স্পষ্ট হয়।
দুই দর্শনের পাশাপাশি তুলনা
| CISC দর্শন | RISC দর্শন | |
|---|---|---|
| মূলমন্ত্র | Compiler-এর কাজ সহজ করো, hardware কষ্ট করুক | Hardware সরল রাখো, compiler-কে বেশি কাজ দাও |
| Instruction জটিলতা | একটা instruction অনেক কাজ করে (memory access + arithmetic + …) | একটা instruction একটাই সরল কাজ করে |
| Instruction length | সাধারণত variable (প্রয়োজন অনুযায়ী ছোট বা বড়) | সাধারণত fixed (সব instruction একই আকার) |
| Memory access | বহু instruction সরাসরি memory operand নেয় (register-memory) | শুধু LOAD/STORE (load-store architecture) |
| Register সংখ্যা | ঐতিহাসিকভাবে কম (৮টা x86-এর মূল ডিজাইনে) | ঐতিহাসিকভাবে বেশি (৩২টা RISC-V/MIPS-এ) |
| Control বাস্তবায়ন | Microcode (জটিল instruction-এর জন্য) | Hardwired/direct (সাধারণত) |
| Code density | বেশি (কম instruction দিয়ে বেশি কাজ) | কম (বেশি instruction লাগে একই কাজে) |
| আদর্শ যুগ | ১৯৬০-৭০, memory ব্যয়বহুল/ধীর যখন | ১৯৮০-এর পর, pipelining/cache যখন memory-র সমস্যা কমাল |
| উদাহরণ | VAX-11/780, প্রাথমিক x86 (8086-80386) | Berkeley RISC, MIPS, SPARC, ARM, RISC-V |
তৃতীয় একটা অক্ষ — memory operand কতটা গভীরভাবে instruction-এ ঢুকতে পারে
RISC/CISC-এর পার্থক্যটা প্রায়ই একটা আরও নির্দিষ্ট classification দিয়ে বোঝানো হয় — একটা arithmetic instruction তার operand memory থেকে কতটা সরাসরি নিতে পারে, তার ভিত্তিতে। একাডেমিক সাহিত্যে এই taxonomy-র তিনটা ক্লাসিক বিভাগ:
| ধরন | Arithmetic instruction memory ছোঁয় কতবার? | উদাহরণ ISA |
|---|---|---|
| Register-register (load-store) | শূন্যবার — শুধু আলাদা LOAD/STORE memory ছোঁয়, arithmetic শুধু register-এ | RISC-V, ARM64, MIPS, Berkeley RISC |
| Register-memory | একবার — arithmetic instruction একটা operand সরাসরি memory থেকে নিতে পারে, ফলাফল register-এ | x86-64 (add eax, [addr]), x86 (ঐতিহাসিকভাবে) |
| Memory-memory | দুইবার বা তারও বেশি — উভয় operand-ই memory-তে থাকতে পারে, register লাগেই না | VAX (আংশিকভাবে), পুরনো CISC মেশিন |
আমাদের toy ISA (গত লেসন) আসলে register-memory ঘরানার কাছাকাছি ছিল — LOAD Rd ← Mem[Rs + offset] আলাদা ছিল ঠিকই, কিন্তু ISA-টা এত ছোট যে কোনো instruction memory-থেকে-memory সরাসরি অপারেট করেনি। RISC-V সম্পূর্ণ বিশুদ্ধ register-register/load-store — এই কারণেই RISC-V-র লুপ উদাহরণে lw আলাদা instruction লাগল, x86-64-তে add eax, [addr] একটাতেই হলো। এই একই তিন-ভাগের taxonomy দিয়েই বলা যায়, memory-memory ঘরানা (VAX-এর মতো) আজ কার্যত বিলুপ্ত — register-register আর register-memory-ই টিকে আছে, কারণ memory-memory instruction pipelining-এ সবচেয়ে বেশি ক্ষতিকর (একটা instruction-এই দুইবার অনির্দিষ্ট-বিলম্বের memory access, যেটা কোনো pipeline stage-এ predictably বসানো কঠিন)।
ভেতরে কী ঘটছে
কেন instruction length সরাসরি pipelining-কে প্রভাবিত করে
RISC-এর “fixed-length instruction” সিদ্ধান্তটা স্রেফ নান্দনিকতার বিষয় না — এর পেছনে একটা কঠিন hardware-প্রকৌশল কারণ আছে, আর সেটা বোঝাই এই সেকশনের কেন্দ্রীয় লক্ষ্য।
একটা CPU-কে instruction execute করার আগে প্রথমে জানতে হয় instruction-টা ঠিক কত bit/byte লম্বা — কারণ পরের instruction-টা কোথা থেকে শুরু হচ্ছে সেটা জানতে এই তথ্য লাগে। এখানেই দুই দর্শনের পার্থক্যটা মৌলিক হয়ে ওঠে:
Fixed-length (RISC-V, বেশিরভাগ ARM64) — প্রতিটা instruction ঠিক ৩২ bit (৪ byte)। CPU জানে পরের instruction ঠিক ৪ byte পরে শুরু হবে — কোনো decode ছাড়াই, শুধু address গণনা করে। এর মানে:
- একাধিক instruction সমান্তরালে fetch/decode করা যায় (superscalar front-end) — কারণ ২য়, ৩য় instruction-এর ঠিকানা আগে থেকেই জানা, ১ম-টা decode করার অপেক্ষা করতে হয় না।
- Branch target গণনা সহজ — কোনো instruction-এর precise byte-offset জানতে instruction-গুলো গোনা মানেই সরাসরি গুণ (
n × 4)। - Decode circuit সরল — bit-field-গুলো সবসময় একই জায়গায় (opcode সবসময় একই bit range-এ), তাই decode একটা সরল combinational lookup।
Variable-length (x86-64) — একটা instruction ১ byte থেকে ১৫ byte পর্যন্ত যেকোনো দৈর্ঘ্যের হতে পারে (legacy prefix বাইট, REX prefix, opcode বাইট, ModRM বাইট, SIB বাইট, displacement, immediate — প্রতিটা ঐচ্ছিক, নির্ভর করে ঠিক কোন instruction আর কোন operand)। সমস্যাটা এখানে: CPU-কে জানতে হলে instruction-টা কত byte, তাকে সেটা আংশিকভাবে decode করতেই হয় — এটাই তথাকথিত “length-decode” সমস্যা। এর ফলাফল:
- পরের instruction কোথায় শুরু, তা জানতে বর্তমান instruction-টা আগে (অন্তত আংশিক) decode করতে হয় — instruction-গুলো সমান্তরালে decode করা কঠিন, sequential নির্ভরতা তৈরি হয়।
- x86-64 CPU-র decode stage তাই সবচেয়ে জটিল আর সবচেয়ে বেশি power-খরচকারী অংশগুলোর একটা — বিশেষ length-decode hardware লাগে যেটা প্রতিটা সম্ভাব্য instruction শুরুর বিন্দু থেকে দৈর্ঘ্য অনুমান করার চেষ্টা করে, প্রায়ই একাধিক সম্ভাবনা সমান্তরালে try করে (speculative length decoding)।
RISC-V (fixed 32-bit) — প্রতিটা instruction ঠিক ৪ byte
addr 0x00: [xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx] ← instruction 1 (৪ byte)
addr 0x04: [xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx] ← instruction 2 (৪ byte)
addr 0x08: [xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx] ← instruction 3 (৪ byte)
পরের instruction-এর ঠিকানা = current + 4, সবসময়। কোনো decode লাগে না।
x86-64 (variable 1-15 byte) — প্রতিটা instruction ভিন্ন দৈর্ঘ্য
addr 0x00: [55] ← push rbp (১ byte)
addr 0x01: [48 89 e5] ← mov rbp,rsp (৩ byte)
addr 0x04: [8b 45 fc] ← mov eax,[rbp-4] (৩ byte)
addr 0x07: [48 83 c0 01] ← add rax,1 (৪ byte)
পরের instruction-এর ঠিকানা জানতে বর্তমানটা partially decode করতেই হয়।এখন সেই twist — আধুনিক x86-64 ভেতরে RISC-এর মতো আচরণ করে
এতক্ষণ শুনে মনে হতে পারে x86-64 pipelining-এর জন্য মৌলিকভাবে অসুবিধাজনক — আর এক অর্থে তাই, এইজন্যই x86 decoder ঐতিহাসিকভাবে সবচেয়ে জটিল অংশ। কিন্তু Intel আর AMD একটা চতুর সমাধান খুঁজে পেয়েছিল, আর এটাই এই লেসনের কেন্দ্রীয় ঐতিহাসিক twist।
১৯৯৫ সালে Intel-এর P6 microarchitecture (Pentium Pro) থেকে শুরু করে, x86 CPU-গুলো একটা দুই-ধাপের কৌশল নেয়:
- Frontend decode: জটিল, variable-length x86 instruction (“macro-op”) পড়া হয়, আর সেটাকে একটা বা একাধিক সরল, fixed-format, RISC-সদৃশ micro-operation (micro-op বা “uop”)-এ ভেঙে ফেলা হয়। যেমন x86-এর
add eax, [c_addr](memory read + add একসাথে) ভেতরে ভেঙে যায় প্রায় দুইটা uop-এ — একটা memory-থেকে-load uop, আরেকটা register-add uop — ঠিক RISC-V-রlw+add-এর মতোই! - Backend execution: এই uop-গুলোই আসল execution engine execute করে — out-of-order scheduling, register renaming, সবকিছু এই সরল, uniform uop-এর উপর কাজ করে, জটিল x86 instruction-এর উপর সরাসরি না।
এর মানে: x86-64-র বাইরের চেহারা (ISA) CISC, কিন্তু ভেতরের বাস্তবায়ন (microarchitecture) RISC-সদৃশ। এটাই গত লেসনের ISA-বনাম-microarchitecture বিভাজনের সবচেয়ে নাটকীয় উদাহরণ — একই ISA-র নিচে সম্পূর্ণ ভিন্ন দর্শনের একটা microarchitecture থাকতে পারে।
- x86-64 instruction (macro-op)যেমন add eax, [c_addr] — একই instruction-এ memory read + add
- Length decodeপ্রথমে instruction-টা ঠিক কত byte তা বের করা — x86-এর সবচেয়ে জটিল ধাপ
- Instruction decode → micro-op translationmacro-op ভেঙে ১-৪টা সরল, fixed-format micro-op তৈরি
- Micro-op cache (উপলব্ধ থাকলে)বারবার চলা loop-এর decode করা uop cache-এ রাখা, পুনরায় decode এড়ানো — Sandy Bridge (2011)-এর পর থেকে সাধারণ
- Out-of-order execution engineuop-গুলো নিয়ে register renaming, reorder buffer, scheduling — RISC-সদৃশ, uniform কাজের ইউনিট নিয়ে
- Retirement — architectural state আপডেটফলাফল ISA-র visible register/memory-তে, ঠিক ISA স্পেক অনুযায়ী — বাইরের চুক্তি অক্ষুণ্ণ
কেন এই কৌশলটা কাজ করে, ISA-র ধারণাটাই মনে করিয়ে দেয়। গত লেসনে শিখেছিলেন ISA শুধু বলে কী করা যায়, microarchitecture ঠিক করে কীভাবে। Micro-op translation ঠিক এই বিভাজনটাই কাজে লাগায় — বাইরের প্রোগ্রামার/compiler x86-64 ISA-র ভাষায় কথা বলে (backward-compatible, বিশাল software ecosystem অক্ষুণ্ণ থাকে), কিন্তু ভেতরে CPU যা খুশি করতে পারে — এমনকি RISC গবেষণার সমস্ত সুবিধা (সরল, uniform pipelining unit) আত্মসাৎ করে নিতে পারে, যতক্ষণ শেষ ফলাফল ISA-র semantics মেনে চলে।
Decode width — fixed-length-এর সুবিধা সংখ্যায় দেখা
Length-decode-এর জটিলতাটা বিমূর্ত মনে হতে পারে, কিন্তু এর প্রভাব একটা খুব concrete সংখ্যায় ধরা পড়ে — একটা CPU একই সাথে কতগুলো instruction fetch/decode করতে পারে প্রতি clock cycle-এ (একে বলে decode width বা issue width)। এই সংখ্যাটাই মোটামুটিভাবে নির্ধারণ করে CPU সর্বোচ্চ কত instruction-per-cycle (IPC) achieve করতে পারবে তার একটা upper bound।
| CPU (আনুমানিক, generation-ভেদে বদলায়) | ISA পরিবার | Decode width (প্রায়) |
|---|---|---|
| Apple M-series (M1 এবং পরবর্তী) | ARM64, fixed-length | ~৮ instruction/cycle |
| AMD Zen 4 | x86-64, variable-length | ~৪-৬ instruction/cycle (front-end decoder), micro-op cache থেকে বেশি |
| Intel Skylake-family | x86-64, variable-length | ~৪-৫ instruction/cycle (legacy decoder), micro-op cache থেকে ~৬ |
| RISC-V উচ্চ-ক্ষমতার core (যেমন SiFive P-সিরিজ) | RISC-V, fixed-length | ~৩-৪ instruction/cycle |
সংখ্যাগুলো generation-ভেদে বদলায় (এখানে শুধু order-of-magnitude ধারণা দেওয়ার জন্য), কিন্তু প্যাটার্নটা স্পষ্ট: fixed-length ARM64 core-এ decode width সবচেয়ে চওড়া করা তুলনামূলক সহজ, কারণ প্রতিটা সম্ভাব্য instruction-শুরুর বিন্দু আগে থেকেই জানা (n×4 ঠিকানায়)। Variable-length x86-64-এ legacy decoder-কে চওড়া করা কঠিন (প্রতিটা parallel decode lane-কে আগের lane-এর length-decode ফলাফলের জন্য অপেক্ষা করতে হয়) — এই জন্যই micro-op cache-এর মতো বাড়তি স্তর লাগে সেই সীমা পাশ কাটাতে।
উদাহরণ
তিনটা বাস্তব ISA পাশাপাশি — সংখ্যা দিয়ে
| x86-64 | ARM64 / AArch64 | RISC-V (RV64GC) | |
|---|---|---|---|
| দর্শন | CISC (heritage), RISC-সদৃশ ভেতরে (micro-op) | RISC | RISC |
| Instruction length | Variable, ১-১৫ byte | Fixed, ৪ byte (৩২ bit) | সাধারণত fixed ৪ byte, C extension-এ ২ byte-ও (মিশ্র) |
| GPR সংখ্যা | ১৬টা (rax-rbx-rcx-rdx-rsi-rdi-rbp-rsp, r8-r15) | ৩১টা (x0-x30) + স্বতন্ত্র SP | ৩২টা (x0-x31), x0 হার্ডওয়্যার্ড শূন্য |
| Register width | ৬৪ bit | ৬৪ bit | ৬৪ bit (RV64), ৩২ bit (RV32) |
| Memory access | বহু instruction-এ সরাসরি memory operand (register-memory) | Load-store শুধু | Load-store শুধু |
| Addressing modes | অনেকগুলো (base+index+scale+displacement একসাথে) | সীমিত কয়েকটা | খুব সীমিত (base+offset) |
| ফ্ল্যাগশিপ ব্যবহার | Desktop/server (Intel, AMD) | Mobile (প্রায় সব smartphone), Apple Silicon | Embedded, growing server/mobile interest, open hardware আন্দোলন |
| মালিকানা | Intel/AMD মালিকানাধীন, license প্রয়োজন | ARM Holdings-এর license প্রয়োজন | সম্পূর্ণ open, রয়্যালটি-ফ্রি |
| Base instruction সংখ্যা | কয়েকশো (base) + হাজার হাজার SIMD/AVX opcode | প্রায় হাজারখানেক (সব extension সহ) | ৪৭ (RV32I base), extension যোগ হলে বাড়ে |
একই ফাংশন, তিনটা ISA — array-loop দিয়ে RISC-এর কৌশলটা আরও স্পষ্ট
গত লেসনের add উদাহরণটা ছিল সবচেয়ে সরল সম্ভাব্য ফাংশন — RISC আর CISC-এর পার্থক্য সেখানে ইতিমধ্যে দেখা গেছে, কিন্তু একটা লুপ-সহ উদাহরণ পার্থক্যটা আরও তীক্ষ্ণ করে তোলে:
// sum.c — array-এর সব উপাদান যোগ করা
int sum(int *arr, int n) {
int total = 0;
for (int i = 0; i \< n; i++) {
total += arr[i];
}
return total;
}x86-64-র loop body-র মূল অংশ (সরলীকৃত, -O2):
.loop:
add eax, [rdi + rcx*4] ; total += arr[i] — memory read + add, একই instruction!
add rcx, 1 ; i++
cmp rcx, rsi ; i \< n?
jl .loopRISC-V-র সমতুল্য (সরলীকৃত):
.loop:
lw t0, 0(a0) # t0 = arr[i] — শুধু load, arithmetic না
add a2, a2, t0 # total += t0 — শুধু register arithmetic, memory ছোঁয় না
addi a0, a0, 4 # arr++ (পরের element-এর ঠিকানা)
addi a1, a1, -1 # n--
bnez a1, .loop # n != 0? — compare আর branch একসাথে, এক instruction!লক্ষ্য করার মতো দুইটা বিপরীতমুখী বিষয়: x86-64-র add eax, [rdi + rcx*4] একটা instruction-এই memory addressing (base+index+scale) + load + add তিনটা কাজ করছে — CISC-এর ক্লাসিক উদাহরণ, complex addressing mode-এর সুবিধা। কিন্তু একই সাথে, RISC-V-র bnez (branch if not equal to zero) একটা instruction-এই compare + branch করছে — যেটা x86-64-তে আলাদা cmp + jl দুইটা instruction লাগে! এখান থেকেই বোঝা যায় “RISC মানে সবসময় কম instruction” ধারণাটা অতিসরলীকরণ — RISC-V-র branch design এখানে বরং x86-64-র চেয়ে কম instruction ব্যবহার করছে, কারণ compare-and-branch একসাথে একটা instruction — এই বিস্তারিত পরের লেসনে (registers, PC, flags) আরও স্পষ্ট হবে।
Bit-level encoding পাশাপাশি — কথায় না, বিটে দেখা
“Fixed” আর “variable” শব্দ দুটো আরও concrete হয় যদি সত্যিকারের bit layout পাশাপাশি রাখা যায়। একটা সাধারণ ADD instruction তিনটা ISA-তে কেমন এনকোড হয় তার একটা সরলীকৃত ছবি:
RISC-V R-type (add rd, rs1, rs2) — সবসময় ঠিক ৩২ বিট
┌─────────┬────────┬────────┬────────┬────────┬─────────┐
│ funct7 │ rs2 │ rs1 │ funct3 │ rd │ opcode │
│ (7 bit) │(5 bit) │(5 bit) │(3 bit) │(5 bit) │ (7 bit) │
└─────────┴────────┴────────┴────────┴────────┴─────────┘
প্রতিটা field-এর অবস্থান, প্রতিটা instruction-এ, সবসময় অভিন্ন — decode একটা fixed lookup।
ARM64 (add w0, w0, w1) — সবসময় ঠিক ৩২ বিট
┌──────────┬──────────┬──────────┬──────────┐
│ opcode/ │ Rm │ Rn │ Rd │
│ sf/সাধারণ │ (5 bit) │ (5 bit) │ (5 bit) │
│ ফিল্ড │ │ │ │
└──────────┴──────────┴──────────┴──────────┘
RISC-V-র সাথে বিস্তারিত ভিন্ন, কিন্তু মূলনীতি একই — fixed ৩২ বিট, fixed field-অবস্থান।
x86-64 (add eax, [rdi+rcx*4]) — ৩ থেকে ৭ বিট-না, বাইট, আর সংখ্যা ভিন্ন হতে পারে
┌─────────┬──────────┬─────────┬─────────────┐
│ opcode │ ModRM │ SIB │ displacement │
│ (1 byte)│ (0-1 byte)│(0-1 byte)│ (0, 1, বা 4 byte, ঐচ্ছিক)│
└─────────┴──────────┴─────────┴─────────────┘
কোনো field সবসময় থাকে না — presence নিজেই আগের byte-এর মান-নির্ভর।RISC-V আর ARM64-র diagram দুটো লক্ষ্য করুন — দুটোই “৫ বিট করে register field” ব্যবহার করে (2^5 = 32টা register ঠিক উপযুক্ত, এক bit-ও নষ্ট হয় না)। এটা কাকতালীয় না — যেকোনো ISA-তে register field-এর bit-width নির্ধারিত হয় register সংখ্যা দিয়ে (log2(register সংখ্যা)), আর এই সম্পর্কটাই পরের লেসনের একটা কেন্দ্রীয় বিষয় হবে যখন আমরা দেখব কেন RISC-V ঠিক ৩২টা (৪৮ বা ৬৪ না) register বেছে নিয়েছিল — encoding budget-এর সাথে register-সংখ্যার এই ট্রেড-অফটাই তার একটা বড় কারণ।
নিজে চালিয়ে দেখুন
Instruction byte-length নিজে মেপে RISC/CISC-এর পার্থক্য প্রত্যক্ষ করুন
অংশ ১ — x86-64-এ raw bytes দেখুন (নিজের মেশিনে, GCC লাগবে):
// sum.c
int sum(int *arr, int n) {
int total = 0;
for (int i = 0; i \< n; i++) total += arr[i];
return total;
}gcc -O2 -c sum.c -o sum.o
objdump -d sum.oআউটপুটে বাম দিকের ঠিকানা কলাম আর তার পাশের raw hex byte-গুলো লক্ষ্য করুন:
0000000000000000 <sum>:
0: 31 c0 xor %eax,%eax
2: 85 f6 test %esi,%esi
4: 7e 0a jle 10 <sum+0x10>
6: 8d 04 8f lea (%rdi,%rcx,4),%eax
...ঠিকানা 0 থেকে 2 — মাত্র ২ byte। 2 থেকে 4 — আবার ২ byte। 4 থেকে 6 — ২ byte। কিন্তু 6-এ থাকা lea instruction ৩ byte নিচ্ছে। প্রতিটা instruction ভিন্ন দৈর্ঘ্য — এলোমেলো, predictable প্যাটার্ন নেই।
নিজে গণনা করুন: পুরো objdump -d sum.o আউটপুটে অন্তত ১০টা instruction-এর byte-length লিখুন (ঠিকানার পার্থক্য থেকে)। সর্বনিম্ন আর সর্বোচ্চ length কত পেলেন? গড় length কত?
অংশ ২ — Compiler Explorer-এ RISC-V/ARM64-এ একই তুলনা:
godbolt.org-এ একই sum ফাংশন পেস্ট করুন। Compiler dropdown-এ RISC-V (64-bit) gcc বেছে নিন, output-এ address column-টা “Show” করুন (সাধারণত options-এ একটা টগল থাকে, বা ডান-ক্লিক মেনুতে)। লক্ষ্য করুন প্রতিটা instruction-এর ঠিকানা ঠিক পরের instruction থেকে precisely ৪ বাড়ে — 0x0, 0x4, 0x8, 0xc, … — ব্যতিক্রমহীনভাবে। ARM64-এও একই জিনিস দেখবেন।
যদি RISC-V-তে C extension সক্রিয় থাকে (-march=rv64gc, ডিফল্ট প্রায়ই): কিছু instruction-এ ২ byte-ও দেখতে পারেন (compressed instruction, c. prefix সহ mnemonic) — এটা একটা গুরুত্বপূর্ণ ব্যতিক্রম, misconception সেকশনে বিস্তারিত আলোচনা আছে।
x86-64-র প্রতিটা instruction ভিন্ন সংখ্যক byte নেয় (ঠিকানাগুলো অসম দূরত্বে বাড়ে), অথচ RISC-V/ARM64-এ প্রতিটা instruction ঠিক ৪ byte নেয় (ঠিকানা সবসময় ঠিক ৪ করে বাড়ে) — এটাই fixed বনাম variable-length-এর সরাসরি, চোখে-দেখা প্রমাণ, কোনো তত্ত্ব ছাড়াই।
নিজে বানান
Toy ISA-কে variable-length CISC স্টাইলে রিডিজাইন করুন — আর decode খরচটা নিজে হিসাব করুন
- গত লেসনের toy ISA (ADD, SUB, LOAD, STORE, ১৬-বিট fixed encoding) থেকে শুরু করুন
- একটা নতুন CISC-স্টাইল instruction ডিজাইন করুন — যেমন MAC (multiply-accumulate: Rd তে Rs*Rt যোগ করে রাখা) যেটা একই instruction-এ memory-থেকে-load, multiply, আর add তিনটা কাজ করে
- এই instruction-এর encoding এমনভাবে ডিজাইন করুন যাতে এটা মূল ১৬-বিট instruction-এর চেয়ে দীর্ঘ হয় (যেমন ৩২ বিট, কারণ তিনটা register আর একটা memory offset একসাথে লাগবে)
- এখন লিখুন — আপনার fetch stage কীভাবে বুঝবে পরবর্তী instruction কোথায় শুরু, যেহেতু কিছু instruction ১৬ বিট আর কিছু ৩২ বিট? (একটা length-indicator bit বা bit-pattern দরকার হবে)
- একটা ছোট অনুচ্ছেদে লিখুন — এই length-varying সিদ্ধান্তটা আপনার single-cycle datapath-কে (গত মডিউলের control_unit) ঠিক কীভাবে জটিল করে তুলবে
RISC-V-র creator-রাও ঠিক এই একই decision face করেছিলেন compressed (C) extension ডিজাইন করার সময় — কীভাবে ১৬-বিট আর ৩২-বিট instruction একসাথে মেশানো যায় অথচ decode simple রাখা যায়। তাদের সমাধান: প্রথম দুই bit (opcode[1:0]) দেখেই বলে দেওয়া যায় instruction ১৬-বিট না ৩২-বিট (11 মানে ৩২-বিট, বাকি সব মানে ১৬-বিট) — এক bit-field-এই length-decode সম্পূর্ণ, কোনো জটিল multi-step length-decode লাগে না, যেটা x86-64-র থেকে সম্পূর্ণ ভিন্ন কৌশল।
আপনার কাজ:
আপনার MAC instruction ডিজাইন করার সময় ভাবুন RISC-V-র এই কৌশলটা অনুসরণ করা যায় কি না — যেমন, আপনার toy ISA-র বিদ্যমান opcode বিটগুলো (২ বিট) দিয়ে কি নতুন length-কে সংকেত দেওয়া সম্ভব, নাকি সম্পূর্ণ নতুন একটা bit-field লাগবে?
বিদ্যমান instruction (১৬ বিট):
┌──────────┬────────────┬────────────┬────────────┐
│ opcode │ Rd (2 bit)│ Rs (2 bit)│ Rt (2 bit) │
│ (2 bit) │ │ │ │
└──────────┴────────────┴────────────┴────────────┘
আপনার MAC (৩২ বিট প্রস্তাবিত) — নিজে সম্পূর্ণ করুন:
┌──────────┬────────────┬────────────┬────────────┬───────────────────┐
│ opcode │ Rd │ Rs │ Rt │ offset/immediate │
│ (? bit) │ (? bit) │ (? bit) │ (? bit) │ (? bit) │
└──────────┴────────────┴────────────┴────────────┴───────────────────┘নিজে বাড়ান: আপনার সহপাঠীর ডিজাইন করা length-encoding স্কিমটা দেখুন (বা কয়েকদিন পর নিজেরটাই আবার দেখুন) — শুধু bit pattern দেখে, কোনো ব্যাখ্যা ছাড়া, একজন কি বলতে পারবেন এটা ১৬-বিট না ৩২-বিট instruction? যদি না পারেন, আপনার length-encoding যথেষ্ট স্পষ্ট না — এটাই ঠিক সেই সমস্যা যা x86-64-র জটিল length-decode hardware সমাধান করার চেষ্টা করে।
বাস্তব সিস্টেমে
RISC বনাম CISC যেখানে সিদ্ধান্ত নির্ধারণ করেছে
Apple Silicon-এর ARM64-এ স্থানান্তর (২০২০)। গত লেসনে দেখেছেন Apple x86-64 থেকে ARM64-এ গেছে, Rosetta 2 emulation-সহ। এই স্থানান্তরের একটা বড় motivation ছিল সরাসরি এই লেসনের বিষয়: ARM64-র fixed-length, RISC ডিজাইন কম power খরচে বেশি instruction-per-cycle decode করতে পারে (একসাথে ৮টা পর্যন্ত instruction decode — Apple M-series চিপে, যেখানে x86-64 CPU সাধারণত ৪-৫টার বেশি decode করতে কষ্ট পায় ঠিক এই length-decode জটিলতার কারণে)। ফলাফল — একই বা কম power-এ তুলনীয় বা বেশি performance, বিশেষত mobile/laptop-এর battery-সীমিত জগতে।
Mobile-এ ARM-এর সম্পূর্ণ আধিপত্য। প্রায় প্রতিটা smartphone (Android, iPhone) ARM-ভিত্তিক ISA চালায় — power efficiency-ই মূল কারণ, আর সেই efficiency-র একটা বড় উৎস RISC ডিজাইনের সরলতা (কম decode hardware, কম transistor, কম leakage power)। x86-64 মোবাইলে কখনো সফল হয়নি এই একই কারণে — Intel বহুবার চেষ্টা করেছে (Atom সিরিজ) কিন্তু CISC-heritage-এর power-খরচ কাটিয়ে উঠতে পারেনি প্রতিযোগিতামূলকভাবে।
RISC-V-র open ISA আন্দোলন — এই platform-এর CPU Emulator প্রজেক্টের ভিত্তি। RISC-V-র মূল architect Krste Asanović আর নেতৃত্বে David Patterson নিজে — সেই একই Patterson যিনি ১৯৮০-এর দশকে Berkeley RISC প্রজেক্ট নেতৃত্ব দিয়েছিলেন, চার দশক পর আবার একটা RISC ISA-র নেতৃত্বে, এবার সম্পূর্ণ open আর royalty-free। RV32I-র মাত্র ৪৭টা base instruction, সরল fixed-length encoding, uniform register model — এই সরলতাই এটাকে শেখার জন্য আদর্শ করে তোলে, তাই এই platform-এর পরবর্তী CPU Emulator প্রজেক্ট RV32I বেছে নিয়েছে (x86-64 না, কারণ তার variable-length decode logic হাতে লেখা emulator-এর জন্য অপ্রয়োজনীয়ভাবে জটিল — মূল শেখার লক্ষ্য fetch-decode-execute cycle বোঝা, decode-এর edge case না)।
IBM POWER/PowerPC — আরেকটা প্রধান RISC পরিবার। Apple নিজেই এক দশকের বেশি (১৯৯৪-২০০৬) PowerPC ব্যবহার করেছিল Mac-এ, x86-এ যাওয়ার আগে। PowerPC আজও IBM-এর server line-এ (POWER10) আর কিছু embedded/gaming ব্যবহারে (পুরোনো PlayStation 3, Xbox 360, Wii-U) টিকে আছে — RISC দর্শনের আরেকটা স্বাধীন বাস্তবায়ন।
Itanium/EPIC — একটা তৃতীয় পথের ব্যর্থ পরীক্ষা। ১৯৯০-এর দশকের শেষে Intel আর HP মিলে একটা সম্পূর্ণ ভিন্ন দর্শন — VLIW/EPIC (Explicitly Parallel Instruction Computing) — চেষ্টা করেছিল, যেখানে compiler-ই আগে থেকে ঠিক করে দেয় কোন instruction-গুলো সমান্তরালে চলবে (hardware-কে runtime-এ সেই সিদ্ধান্ত নিতে হয় না)। বাণিজ্যিকভাবে ব্যর্থ হয় — compiler প্রযুক্তি সেই সময়ের জটিল real-world কোডে যথেষ্ট ভালো static scheduling করতে পারেনি। এই ব্যর্থতা RISC/CISC-এর মাঝামাঝি কোনো “তৃতীয় বিকল্প” যে সহজ না, তারই প্রমাণ।
ARM-এর নিজস্ব Thumb/Thumb-2 — RISC-এর ভেতরেই একটা compromise। পুরোনো ৩২-বিট ARM (AArch32)-এ Thumb mode ছিল — একটা ১৬-বিট encoding মোড, code density বাড়ানোর জন্য (embedded system-এ memory এখনও দামি হতে পারে)। এটা দেখায় “code density বনাম decode-সরলতা” trade-off-টা RISC পরিবারের ভেতরেও একটা চলমান আলোচনা, শুধু RISC-বনাম-CISC-এর বাইরের প্রশ্ন না। আধুনিক AArch64 (pure ৬৪-বিট মোড)-এ অবশ্য Thumb নেই — শুধু fixed ৩২-বিট, ডিজাইন সরল রাখার সিদ্ধান্ত জিতেছে।
x86-64-র backward-compatibility-চালিত টিকে থাকা। সম্পূর্ণ RISC-এর technical সুবিধা থাকা সত্ত্বেও x86-64 আজও desktop/server-এ প্রধান কারণ প্রযুক্তিগত না — software ecosystem-এর জড়তা (inertia)। কোটি কোটি ডলারের সফটওয়্যার x86-64-এর জন্য কম্পাইল করা, বিশাল টুলচেইন আর অপ্টিমাইজেশন দশক ধরে জমা — এই সব ফেলে ARM64/RISC-V-তে যাওয়ার খরচ Apple-এর মতো এক কোম্পানির (নিজের পুরো software stack নিয়ন্ত্রণ করে) জন্য যতটা সহজ, বাকি ইন্ডাস্ট্রির জন্য ততটা না।
যে ভুলগুলো সবাই করে
“RISC মানেই কম instruction, তাই RISC ISA সবসময় CISC-এর চেয়ে ছোট (opcode গণনায়)।”
এটা এক সময় মোটামুটি সত্য ছিল, কিন্তু আজকের বাস্তব ISA-গুলোতে আর সরল সত্য না। RISC-V RV32I base-এ মাত্র ৪৭টা instruction, ঠিক — কিন্তু বাস্তব ব্যবহারে RISC-V CPU সাধারণত অনেকগুলো extension একসাথে ব্যবহার করে (M — multiply/divide, A — atomic, F/D — floating point, C — compressed), আর মোট instruction সংখ্যা কয়েকশোতে পৌঁছায়। ARM64-এ SIMD/NEON/crypto extension-সহ প্রায় হাজারখানেক instruction।
আসল পার্থক্যটা instruction গণনায় না, বরং প্রতিটা individual instruction কতটা কাজ করে আর কতটা uniform/predictable তার format তাতে — RISC-V-র হাজার instruction থাকলেও প্রতিটাই সরল, একই ধরনের সীমিত কাজ করে, ফিক্সড ফরম্যাটে। x86-64-এ instruction সংখ্যা কম হলেও (আপেক্ষিকভাবে) একেকটা instruction (যেমন MVCL-সদৃশ string instruction) একাই বহু কাজ একসাথে করতে পারে।
“আধুনিক x86-64 CPU আজও ISA-তে লেখা instruction-গুলোই সরাসরি, ধাপে ধাপে execute করে — ঠিক যেমন ISA স্পেক বর্ণনা করে।”
এটাই এই লেসনের কেন্দ্রীয় সংশোধন। Pentium Pro (১৯৯৫) থেকে শুরু করে প্রতিটা আধুনিক x86-64 CPU প্রথমে জটিল x86-64 instruction-কে সরল, RISC-সদৃশ micro-op-এ ভেঙে ফেলে, তারপর সেই micro-op-এর উপর কাজ করে। ISA স্পেক শুধু বাইরের আচরণ নির্দিষ্ট করে (গত লেসনের ভাষায় — কী দেখা যাবে, কীভাবে বাস্তবায়িত হচ্ছে সেটা নির্দিষ্ট করে না) — আর এই micro-op translation ঠিক সেই স্বাধীনতার একটা চরম, অত্যন্ত সফল উদাহরণ।
এই কারণেই একটা x86-64 CPU-র ভেতরের ব্লক ডায়াগ্রাম দেখলে আপনি প্রায়ই “RISC core” বা “out-of-order execution engine”-এর মতো লেবেল দেখবেন, যদিও চিপটার বাইরের মার্কেটিং নাম “x86 প্রসেসর” — দুটোই সত্য, দুইটা ভিন্ন স্তরের বর্ণনা।
“Fixed-length instruction মানে RISC ISA-তে সবসময় প্রতিটা instruction একই সংখ্যক byte নেয় — কোনো ব্যতিক্রম নেই।”
বেশিরভাগ ক্ষেত্রে সত্য, কিন্তু পুরোপুরি না — এখানেই RISC-V-র C (compressed) extension আর পুরোনো ARM-এর Thumb mode ব্যতিক্রম তৈরি করে। RISC-V-র base RV32I ঠিকই সম্পূর্ণ fixed ৩২-বিট, কিন্তু C extension ঐচ্ছিকভাবে যোগ হলে সাধারণ instruction-গুলোর (যেমন ছোট immediate-সহ addi, বা register-থেকে-register move) একটা ১৬-বিট সংক্ষিপ্ত রূপ ব্যবহার করা যায়, code density বাড়ানোর জন্য — মূলত embedded system-এ instruction memory-র জায়গা বাঁচাতে।
তবে এই “মিশ্রণ”-টা x86-64-র বিশৃঙ্খল variable-length (১-১৫ byte, বহু সম্ভাব্য দৈর্ঘ্য) থেকে গুণগতভাবে ভিন্ন — RISC-V-তে মাত্র দুইটা সম্ভাব্য দৈর্ঘ্য (২ বা ৪ byte), আর কোনটা কোনটা তা প্রথম দুই bit দেখেই নির্ধারিত হয়, কোনো জটিল multi-step length-decode লাগে না (build সেকশনে বিস্তারিত)। “Fixed-length” ধারণাটার তাই সঠিক পাঠ: RISC design predictable, সহজে-নির্ধারণযোগ্য length রাখে, আক্ষরিক অর্থে সবসময় একটাই সংখ্যা রাখে না।
“CISC ছিল একটা ঐতিহাসিক প্রকৌশল ভুল — RISC গবেষণা প্রমাণ করেছে CISC 'ভুল' ডিজাইন ছিল।”
এটা অন্যায্য একটা রায় — CISC তার সময়ের বাস্তবতায় (ব্যয়বহুল, ধীর memory; কম পরিণত compiler প্রযুক্তি; pipeline-ভিত্তিক CPU ডিজাইন তখনও প্রধান কৌশল না) সম্পূর্ণ যুক্তিসঙ্গত একটা সিদ্ধান্ত ছিল। RISC “জিতেছে” কারণ প্রযুক্তিগত প্রেক্ষাপট বদলেছিল — memory সস্তা ও দ্রুত হলো, cache প্রযুক্তি memory bandwidth-এর চাপ কমাল, compiler প্রযুক্তি অনেক পরিণত হলো, আর pipelining CPU performance-এর প্রধান লিভার হয়ে উঠল, যেখানে fixed-length instruction-এর সুবিধা সবচেয়ে বেশি স্পষ্ট।
এমনকি আজও CISC-এর নির্দিষ্ট সুবিধা টিকে আছে — code density (যেটা এমবেডেড সিস্টেমে এখনও গুরুত্বপূর্ণ হতে পারে) আর x86-64-র বিশাল legacy software ecosystem। “RISC জিতেছে” বলাটাও অতিসরলীকরণ — বাস্তবে আজকের সবচেয়ে সফল CPU-গুলো (x86-64) দুই দর্শনের একটা hybrid (বাইরে CISC ইন্টারফেস, ভেতরে RISC বাস্তবায়ন), যা প্রমাণ করে দুইটা দর্শনই মূল্যবান ধারণা ধারণ করেছিল, একটা অন্যটাকে সম্পূর্ণ ভুল প্রমাণ করেনি।
বুঝেছেন কি না দেখুন
1CISC দর্শন কেন ১৯৭০-এর দশকে যুক্তিসঙ্গত ছিল, দুইটা নির্দিষ্ট কারণ দিয়ে ব্যাখ্যা করুন। তারপর ব্যাখ্যা করুন প্রযুক্তিগত প্রেক্ষাপটের ঠিক কোন পরিবর্তনগুলো RISC-কে ১৯৮০-র দশকে ব্যবহারিক করে তুলেছিল।
যুক্তি
CISC কেন যুক্তিসঙ্গত ছিল ১৯৭০-এ:
১. Memory ব্যয়বহুল আর ধীর ছিল — একটা instruction-এ বেশি কাজ গুঁজে দিলে কম instruction লাগে একই কাজ করতে, তাই বাইনারি ছোট হয় (code density বেশি) — এবং memory থেকে fetch করার সংখ্যাও কমে।
২. Compiler প্রযুক্তি অপরিণত ছিল, প্রোগ্রামাররা প্রায়ই assembly সরাসরি লিখতেন — একটা জটিল instruction যা সরাসরি একটা high-level কাজের (যেমন polynomial evaluation) সাথে মেলে, প্রোগ্রামারের জীবন সহজ করে (semantic gap কমায়)।
RISC-কে যা ব্যবহারিক করে তুলল ১৯৮০-এ:
- Memory ধীরে ধীরে সস্তা আর দ্রুত হতে লাগল, cache প্রযুক্তি এলো — code density-র চাপ কমল।
- Compiler প্রযুক্তি অনেক পরিণত হলো — compiler নিজেই ভালো optimization করতে সক্ষম হলো, প্রোগ্রামারদের সরাসরি assembly লেখার প্রয়োজন কমল।
- সবচেয়ে গুরুত্বপূর্ণ — pipelining CPU performance বাড়ানোর প্রধান কৌশল হয়ে উঠল, আর pipelining-এর জন্য fixed-length, uniform instruction অনেক সহজ বাস্তবায়ন করা যায় (hood সেকশনে বিস্তারিত)। Patterson & Ditzel-এর measurement দেখাল compiler আসলে CISC-এর জটিল instruction-গুলো কমই ব্যবহার করে — তাই সেই জটিলতা বহন করার খরচ (chip area, verification) ন্যায্য না।
2একটা x86-64 disassembly-তে দেখলেন ঠিকানা এভাবে বাড়ছে: 0x00, 0x02, 0x05, 0x06, 0x0a। একটা RISC-V disassembly-তে দেখলেন: 0x00, 0x04, 0x08, 0x0c, 0x10। শুধু এই প্যাটার্ন দেখে কীভাবে বলবেন কোনটা কোন পরিবারের ISA, আর কেন?
প্রয়োগ
0x00, 0x02, 0x05, 0x06, 0x0a। একটা RISC-V disassembly-তে দেখলেন: 0x00, 0x04, 0x08, 0x0c, 0x10। শুধু এই প্যাটার্ন দেখে কীভাবে বলবেন কোনটা কোন পরিবারের ISA, আর কেন?x86-64 disassembly: ঠিকানার পার্থক্য এলোমেলো — 0x00→0x02 (২ byte), 0x02→0x05 (৩ byte), 0x05→0x06 (১ byte), 0x06→0x0a (৪ byte)। এই অসম, অনিয়মিত দৈর্ঘ্য variable-length encoding-এর সরাসরি চিহ্ন — শুধু CISC ISA-তেই এমন প্যাটার্ন দেখা যায়, কারণ প্রতিটা instruction তার নিজের প্রয়োজন অনুযায়ী byte নেয় (prefix, ModRM, SIB, displacement, immediate — যেগুলো ঐচ্ছিক)।
RISC-V disassembly: ঠিকানার পার্থক্য সবসময় ঠিক ৪ — নিখুঁত, ব্যতিক্রমহীন নিয়মিততা। এটা fixed-length encoding-এর প্রমাণ — এই ISA-তে (compressed extension ছাড়া) প্রতিটা instruction ঠিক একই আকার।
কারণটা মৌলিক: fixed-length ISA-তে instruction-এর দৈর্ঘ্য জানতে কোনো decode লাগে না (constant জানা থাকে), তাই ঠিকানা predictably বাড়ে। Variable-length ISA-তে দৈর্ঘ্য নিজেই instruction-এর content-নির্ভর, তাই decode না করে জানা যায় না — ফলে ঠিকানার প্যাটার্নে সেই অনিশ্চয়তা প্রতিফলিত হয়।
3“আধুনিক x86-64 CPU ভেতরে RISC-সদৃশ micro-op ব্যবহার করে” — এই তথ্যটা যদি সত্যি হয়, তাহলে কেন Intel/AMD পুরোপুরি x86-64 ISA-টাই বাতিল করে সরাসরি একটা RISC ISA (যেমন RISC-V বা তাদের নিজস্ব একটা RISC ISA) গ্রহণ করে না — যেহেতু তারা ইতিমধ্যেই ভেতরে RISC-এর সুবিধা ব্যবহার করছে?
যুক্তি
এখানেই গত লেসনের ISA-বনাম-microarchitecture বিভাজনের practical গুরুত্বটা সবচেয়ে স্পষ্ট হয়। Micro-op translation-এর সুবিধাটা তারা ইতিমধ্যেই পাচ্ছে — ভেতরের RISC-সদৃশ execution engine থেকে ঠিক তেমন pipelining/out-of-order সুবিধা পাচ্ছে যেমন একটা “খাঁটি” RISC CPU পেত। ISA বদলানোর অতিরিক্ত কোনো performance লাভ হবে না, কিন্তু খরচ বিশাল।
খরচটা backward compatibility-তে। x86-64 ISA বদলানো মানে গত লেসনে Apple-এর x86→ARM64 উদাহরণের মতো — পুরো software ecosystem (কোটি কোটি ডলারের compiled software, operating system, driver) হয় recompile করতে হবে, নয়তো একটা ধীর translation layer (Rosetta 2-এর মতো) দিয়ে চালাতে হবে। Intel/AMD-র গ্রাহকরা (enterprise, consumer সবাই) এই ব্যয় বহন করতে রাজি হবে না যতক্ষণ না performance লাভ বিশাল আর স্পষ্ট — যা এখানে নেই, কারণ তারা ইতিমধ্যেই micro-op translation দিয়ে সেই লাভ আংশিকভাবে আদায় করে নিয়েছে (যদিও length-decode-এর extra power খরচ, micro-op cache-এর মতো বাড়তি hardware-এর মূল্য দিয়ে)।
সংক্ষেপে: যখন ভেতরের বাস্তবায়ন বদলে বাইরের সুবিধা প্রায় সমান পাওয়া যায়, ISA-স্তরে বদলানোর যুক্তি দুর্বল হয়ে যায় — এটাই ISA-র “স্থিতিশীল থাকার” প্রবণতার একটা গভীর কারণ।
4Build সেকশনে RISC-V C extension-এর length-encoding কৌশলটা দেখেছেন — প্রথম দুই bit দেখেই বলা যায় instruction ১৬-বিট না ৩২-বিট। এই কৌশলটা x86-64-র length-decode সমস্যার সাথে তুলনা করে ব্যাখ্যা করুন কেন RISC-V-র কৌশলটা “সস্তা” আর x86-64-র কৌশলটা “ব্যয়বহুল”।
প্রয়োগ
RISC-V C extension-এ length জানতে মাত্র প্রথম দুই bit পড়তে হয় — একটা constant-সময়, single-step lookup (if bits == 11 then 32-bit else 16-bit)। এটা কোনো “decode” বলাই বাড়াবাড়ি — এটা একটা সরাসরি bit-check, কোনো iterative বা conditional multi-step প্রসেস না। Hardware-এ এটা একটা ২-ইনপুট গেট দিয়েই বাস্তবায়ন করা যায়।
x86-64-এ length নির্ধারণ করতে অনেক বেশি ধাপ লাগে: প্রথমে ঐচ্ছিক legacy prefix byte-গুলো (কয়টা আছে, কোনগুলো?) চিনতে হয়, তারপর ঐচ্ছিক REX prefix, তারপর opcode byte (যেটা নিজেই ১ বা ২ বাইট হতে পারে, আর opcode-এর মান অনুযায়ী পরের byte-গুলোর presence নির্ধারিত হয়), তারপর ঐচ্ছিক ModRM byte, তার ভেতরের bit অনুযায়ী ঐচ্ছিক SIB byte, তারপর ঐচ্ছিক displacement (০, ১, বা ৪ byte), তারপর ঐচ্ছিক immediate (০, ১, ২, বা ৪ byte)। প্রতিটা ধাপ আগের ধাপের ফলাফলের উপর নির্ভরশীল (sequential dependency) — এটা একটা প্রকৃত multi-stage decode প্রসেস, প্রতিটা ধাপে hardware লাগে, আর ধাপগুলো সমান্তরালে করা কঠিন কারণ পরের ধাপ জানতে আগের ধাপের ফলাফল লাগে।
মূল পার্থক্য: RISC-V-র কৌশল constant-depth (সবসময় ঠিক এক ধাপ), x86-64-র কৌশল variable-depth (কতগুলো ধাপ লাগবে তা instruction-ভেদে বদলায়, আর প্রতিটা ধাপ আগেরটার উপর নির্ভরশীল)। এই constant-বনাম-variable-depth পার্থক্যটাই hardware জটিলতা, power খরচ, আর decode-এর সমান্তরালতার সীমা নির্ধারণ করে।
5আপনি একটা নতুন embedded microcontroller-এর জন্য ISA ডিজাইন করছেন, যেখানে instruction memory (flash) খুবই সীমিত (মাত্র কয়েক KB), কিন্তু pipelining/out-of-order execution-এর কোনো প্রয়োজন নেই (single-cycle বা সরল ২-স্টেজ পাইপলাইনই যথেষ্ট, কারণ CPU-টা খুব সাধারণ কাজ করে)। RISC না CISC — কোন দর্শনের দিকে ঝুঁকবেন, আর কেন? আপনার উত্তরে এই লেসনের কোন design trade-off-টা কেন্দ্রীয় ভূমিকা পালন করছে তা স্পষ্ট করুন।
ডিজাইন
এই প্রশ্নের সঠিক উত্তর নির্ভর করে কোন trade-off-টা আপনার প্রেক্ষাপটে সক্রিয়, তার উপর — আর এখানে একটা গুরুত্বপূর্ণ observation: pipelining-এর সুবিধা (RISC-এর প্রধান যুক্তি) এখানে অনুপস্থিত (প্রশ্নেই বলা আছে pipelining দরকার নেই), অথচ code density-র চাপ (CISC-এর মূল যুক্তি) প্রবল (মাত্র কয়েক KB flash)।
যুক্তিসঙ্গত সিদ্ধান্ত: একটা CISC-সদৃশ, বা অন্তত compressed/variable-length দিকে ঝোঁকা — কারণ:
- RISC-এর সবচেয়ে বড় সুবিধা (fixed-length instruction-এর pipelining-বান্ধবতা) এখানে অপ্রাসঙ্গিক, যেহেতু pipelining নেই।
- CISC-এর সবচেয়ে বড় সুবিধা (code density) এখানে সবচেয়ে প্রাসঙ্গিক, যেহেতু memory সবচেয়ে বেশি সীমাবদ্ধ সম্পদ।
বাস্তব-জগতের প্রমাণ এই যুক্তিকেই সমর্থন করে: এই কারণেই বাস্তব embedded জগতে RISC-V-র C extension (compressed, ২-বাইট instruction) আর ARM-এর Thumb mode জনপ্রিয় — এরা “pure RISC” এর fixed-length নীতি সামান্য শিথিল করে code density ফিরিয়ে আনে, ঠিক এই প্রশ্নের পরিস্থিতিতে যা দরকার। সম্পূর্ণ pure RISC (কোনো compression ছাড়া) এখানে memory-র অপচয় করবে অকারণে, আর সম্পূর্ণ pure CISC (VAX-স্তরের জটিলতা, microcode) মাইক্রোকন্ট্রোলারের সীমিত chip area/power বাজেটে অতিরিক্ত।
কেন্দ্রীয় trade-off: এই লেসনের “concept” সেকশনের insight callout অনুযায়ী — প্রশ্নটা জটিলতা কমানো-বাড়ানো না, বরং কোন সুবিধাটা আপনার প্রেক্ষাপটে সক্রিয়, আর কোনটা নিষ্ক্রিয় তা চিনতে পারা। এখানে pipelining সুবিধা নিষ্ক্রিয় বলেই বিশুদ্ধ RISC-এর মূল justification হারিয়ে যায়, আর code density-র সুবিধা সক্রিয় বলেই CISC-সদৃশ (বা compressed-RISC) সিদ্ধান্ত যুক্তিসঙ্গত।
এরপর কী
পরের প্রশ্ন — ISA-র vocabulary-র সবচেয়ে ব্যবহৃত অংশটা কাছ থেকে দেখা
এই লেসনে আমরা দেখলাম দুইটা ISA কীভাবে instruction-স্তরে ভিন্ন হতে পারে — কতটা কাজ একটা instruction করে, কত byte সেটা নেয়। কিন্তু প্রতিটা instruction-ই একটা জিনিসের উপর নির্ভর করে যা এখনও আমরা বিস্তারিত দেখিনি: register। Rd, Rs, Rt — গত দুই লেসনেই বারবার এসেছে এই নামগুলো, RISC-V-র x0-x31, x86-64-র rax-r15 — কিন্তু ঠিক কতগুলো register আছে বাস্তব ISA-তে, কেন সেই সংখ্যা, আর সাধারণ register ছাড়া আর কী কী বিশেষ-উদ্দেশ্য register CPU-র কাজে লাগে?
RISC-V-র লুপ উদাহরণে bnez-এর মতো instruction দেখেছেন যেটা compare-and-branch একসাথে করে, x86-64-র cmp+jl যেটা আলাদা flags পড়ে — এই পার্থক্যটাই পরের লেসনের কেন্দ্রীয় বিষয়। পরের লেসনে আমরা গত মডিউলের register file (Level 2-এর হার্ডওয়্যার) থেকে সরাসরি ISA-স্তরে উঠব — general-purpose register-এর সংখ্যা ও width, Program Counter (PC) কীভাবে পরবর্তী instruction ঠিক করে, stack pointer (SP) কীভাবে function call-এর জন্য memory-র একটা অংশ manage করে, আর সেই flags register (Zero, Carry, Overflow, Negative) — যেটা Level 2-এর subtractor/comparator লেসনের circuit থেকে সরাসরি আসে — কীভাবে conditional branch-এর ভিত্তি তৈরি করে।
আরও পড়ুন
- The Case for the Reduced Instruction Set Computer — David A. Patterson, David R. Ditzel · ১৯৮০ সালের সেই মূল পেপার যা RISC আন্দোলন শুরু করে — এই লেসনের ঐতিহাসিক বিতর্কের প্রাথমিক উৎস
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA · RV32I-র ৪৭টা instruction, আর compressed (C) extension-এর 16-bit encoding — fixed-length নীতির ব্যতিক্রম কীভাবে সাবধানে ডিজাইন করা যায় তার উদাহরণ
- Intel 64 and IA-32 Architectures Software Developer's Manual · x86-64-র variable-length encoding (prefix, opcode, ModRM, SIB, displacement, immediate)-এর সম্পূর্ণ, আনুষ্ঠানিক বর্ণনা
- Agner Fog's microarchitecture and instruction tables · x86 CPU-র ভেতরে micro-op decode, micro-op cache, আর প্রতিটা microarchitecture generation-এর বাস্তব পরিমাপ — hood সেকশনের দাবিগুলোর independent সূত্র
- Compiler Explorer (godbolt.org) · এই লেসনের experiment-এ একই কোড x86-64, ARM64, RISC-V-তে পাশাপাশি compile করে instruction sequence তুলনার জন্য ব্যবহৃত