আপনি লিখলেন:
Node চালালেন। Screen-এ 8 দেখলেন।
কিন্তু আগের আর্টিকেলগুলোতে দেখেছি — CPU JavaScript বোঝে না। CPU শেষ পর্যন্ত যে instruction চালায়, সেগুলো তার machine code-এর অংশ — memory-তে থাকা bit pattern, byte-এর ধারা। মানুষকে দেখানোর সুবিধার জন্য আমরা সেই byte-গুলো প্রায়ই hexadecimal-এ লিখি। যেমন x86-এর কিছু machine-code byte দেখতে এমন হতে পারে:
এখানে 89, E5 — এগুলো hexadecimal-এ লেখা byte। Hexadecimal নিজে CPU-র ভাষা নয়, শুধু আমাদের লেখার notation।
তাহলে মাঝখানে কী ঘটল? আপনার লেখা text কীভাবে এমন একটা রূপে পৌঁছাল, যেখান থেকে CPU শেষ পর্যন্ত machine instruction চালাতে পারে?
এটাই আজকের গল্প।
প্রথম কথা: মাঝখানে একটা machinery লাগবেই
একটা foreign language বই পড়তে চাইলে কোনো না কোনোভাবে ভাষাটা বুঝতে হবে। Computer-এর ক্ষেত্রেও তেমনই। আপনি JavaScript, Python বা C-তে source code লিখেছেন; CPU সেই text সরাসরি চালাতে পারে না। মাঝখানে এমন software machinery দরকার, যা সেই code-কে CPU-র চালানোর মতো রূপে নিয়ে যাবে। এই machinery নিজেও program — CPU-তেই চলে।
কিন্তু কাজটা সব ভাষায়, সব implementation-এ একইভাবে হয় না। কেউ পুরো বইটা আগেই অনুবাদ করে ছাপিয়ে রাখে। কেউ meeting চলার সময় অনুবাদ করে। কেউ আগে একটা common intermediate ভাষায় নামিয়ে দেয়, তারপর সেই ভাষা-জানা একজন reader সেটা চালায়। আর কেউ চলতে চলতে খেয়াল করে কোন অংশ বারবার আসছে, আর সেটুকুর জন্য দ্রুততর অনুবাদ বানিয়ে ফেলে।
এই approach-গুলোই আমরা দেখব:
- Compiler: চালানোর আগেই source code process করে executable/native রূপ তৈরি করতে পারে (C, Go, Rust)।
- Interpreter: runtime-এ source বা তার কোনো intermediate representation execute করে (Python, Bash)।
- Bytecode + VM: প্রথমে একটা intermediate form-এ নামানো হয়, তারপর সেটা VM-এর মাধ্যমে চলে (Java, Python, C#)।
- JIT (Just-In-Time): program চলার সময়কার information দেখে কিছু অংশ machine code-এ compile করে (V8, HotSpot)।
এখানেই একটা কথা গোড়াতেই বলে রাখা ভালো: "compiled" আর "interpreted" কোনো ভাষার স্থায়ী পরিচয় নয় — এগুলো বলে দেয় একটা নির্দিষ্ট implementation কীভাবে code চালায়। এই কথাটা শেষ section-এ কাজে লাগবে।
একটা একটা করে দেখা যাক।
Compiler: আগে থেকে অনুবাদক
Compiler এমন একটা program যেটা চালানোর আগেই source code process করে এমন একটা executable রূপ তৈরি করে, যা পরে target machine-এ চালানো যায়। এই process-এর নাম compilation।
কল্পনা করুন একজন professional book translator। সে পুরো বাংলা বই আগে থেকেই অনুবাদ করে ইংরেজি version ছাপিয়ে দেয়। এরপর যে পড়বে, তাকে পাশে একজন অনুবাদক নিয়ে বসতে হবে না।
Compiler-এর basic idea-টাও এমন। আপনি C-তে লিখলেন hello.c, তারপর চালালেন:
এখানে একটা ছোট clarification দরকার। gcc command-টা এক ধাপের কোনো জাদু নয় — ভেতরে সাধারণত কয়েকটা ধাপ আছে: preprocessing, compilation, assembly, তারপর linking। শেষে গিয়ে hello নামের native executable তৈরি হয়।
এরপর ./hello চালালে source code আবার নতুন করে compile করতে হয় না — CPU সরাসরি সেই তৈরি হয়ে থাকা machine instruction চালায়।
সুবিধা:
- Source থেকে native-এ যাওয়ার মূল কাজটা প্রতিবার চালানোর সময় আবার করতে হয় না।
- Compile করার সময় compiler পুরো code বিশ্লেষণ করতে পারে, তাই optimization-এর অনেক সুযোগ পায়।
- Native executable distribute করা যায় source code ছাড়াই।
অসুবিধা:
- Source বদলালে updated executable পেতে আবার কিছু compilation লাগে — তবে build system চাইলে শুধু বদলে যাওয়া অংশটুকুই rebuild করতে পারে।
- Native executable একটা নির্দিষ্ট পরিবেশের জন্য তৈরি — CPU architecture, operating system আর binary convention, সবই compatibility-তে জড়িত।
- Compile time-এ অনেক ভুল ধরা পড়ে, কিন্তু সব runtime behavior আগেই জানা সম্ভব নয়।
C, Go, Rust — এদের সাধারণ ব্যবহারে native executable-এর জন্য compile করা হয়। এই article-এ আমরা সেই common model-টাই ধরছি।
Interpreter: চলতে চলতে অনুবাদ
Interpreter আগে থেকে পুরো program-এর native executable বানিয়ে রাখে না। বরং program চালানোর সময়েই source code বা তার কোনো internal রূপ process করে execution এগিয়ে নেয়।
কল্পনা করুন UN meeting-এর live translator। সে আগে থেকে কোনো বই ছাপায়নি; meeting চলাকালীনই কথাগুলো অন্য ভাষায় পৌঁছে দিচ্ছে।
তবে analogy-টা এক জায়গায় থামানো দরকার। Interpreter মানেই "একটা করে source line পড়ে, সেটা অনুবাদ করে, চালায়, তারপর পরের line" — এমন নয়। অনেক interpreter আগে পুরো source parse করে একটা internal representation বানিয়ে নেয়, তারপর সেই representation চালায়। সেই representation নিজেই bytecode হতে পারে।
Python-এ python hello.py চালালে ঠিক এটাই হয় — CPython source থেকে bytecode তৈরি করে, তারপর সেই bytecode চালায়। (.pyc file-এ সেই bytecode cache-ও থাকতে পারে — পরের section-এ সে গল্প।)
সুবিধা:
- আলাদা native executable আগে থেকে না বানিয়েই code চালানো যায় — লিখলেন, চালালেন।
- একই source বিভিন্ন platform-এ চলে, যদি সেখানে উপযুক্ত runtime থাকে।
- Runtime-এর তথ্য হাতে থাকায় dynamic behavior সামলানো সহজ।
অসুবিধা:
- Execution-এর কিছু কাজ runtime-এ করতে হয়, তাই আগে থেকেই তৈরি native code চালানোর তুলনায় overhead থাকতে পারে।
- Target machine-এ উপযুক্ত runtime থাকা লাগে — শুধু code পাঠালেই হয় না।
- একই কাজ বারবার হলে interpreter-এর dispatch আর runtime check-এর খরচ জমতে থাকে।
আর "interpreter মানেই ধীর" — এটাও কোনো নিয়ম নয়। Modern interpreter অনেক optimized হতে পারে, আর তার ওপর JIT যোগ হলে ঘন ঘন চলা code আরও দ্রুত হয়ে যেতে পারে। সেই গল্প একটু পরেই।
নিচের যন্ত্রে একই ছোট program দুই কৌশলে চালিয়ে দেখুন — একদিকে আগেই একবার অনুবাদ, অন্যদিকে প্রতি run-এ আবার:
দুটোর মাঝামাঝি: Bytecode + Virtual Machine
Source code সরাসরি native machine code-এ না গিয়ে আগে একটা intermediate representation-এ নামতে পারে — যাকে বলে bytecode। Bytecode মানুষের source code-এর চেয়ে নিচের স্তরের, কিন্তু সাধারণত কোনো নির্দিষ্ট CPU-র native machine code নয়।
এই bytecode চালানোর জন্য থাকে একটা virtual machine (VM) — software-এ তৈরি একটা execution environment, যে জানে এই নির্দিষ্ট bytecode কীভাবে চালাতে হয়। (VirtualBox-এর মতো পুরো virtual computer-এর কথা এখানে বলা হচ্ছে না; সেটা আলাদা জিনিস।)
কল্পনা করুন — বাংলা বইটাকে সরাসরি ইংরেজিতে অনুবাদ না করে Esperanto-র মতো একটা common intermediate ভাষায় নামানো হলো। এখন যেকোনো জায়গার পাঠক সেটা পড়তে পারবেন, যদি তাঁর কাছে Esperanto-জানা একজন reader থাকে।
Java-র বিখ্যাত slogan মনে আছে? "Write once, run anywhere।" পথটা এমন:
Java source
↓
Java bytecode (.class)
↓
JVM
↓
executionউপযুক্ত JVM থাকলে একই bytecode Windows, Linux, Mac — সবখানেই চলে।
তবে VM চিরকাল শুধু bytecode interpret করবে, এমন কোনো বাধ্যবাধকতা নেই। Runtime চাইলে সেই bytecode-এর কিছু অংশ পরে machine code-এ compile-ও করতে পারে। এখানেই এই model-এর সঙ্গে JIT-এর যোগসূত্র।
Python-ও এই পথেই হাঁটে। আমি অনেক দিন Python-কে pure interpreted ভাষা মনে করতাম। যতদিন না একদিন project folder-এ __pycache__ folder দেখলাম — ভেতরে অনেকগুলো .pyc file। ভাবলাম, "এগুলো কী?" খুঁজে বার করলাম — CPython source code থেকে bytecode তৈরি করে, সেই bytecode cache করে রাখে, আর সেটাই চালায়।
এখানে পার্থক্যটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ: "compile হয়েছে" মানেই "native machine code-এ compile হয়েছে" নয়। Python-এর ক্ষেত্রে compile হয়েছে bytecode পর্যন্ত, তারপর সেটা VM চালায়।
তাহলে "Python compiled না interpreted?" — উত্তরটা implementation-এর ওপর নির্ভর করে। CPython bytecode বানিয়ে সেটা চালায়; PyPy-র মতো অন্য implementation আবার JIT compilation-ও ব্যবহার করে।
নিচের যন্ত্রে source থেকে bytecode থেকে VM — মাঝের স্তরটা এক ধাপ এক ধাপ করে দেখুন:
JIT: চলতে চলতে compile
Bytecode + VM model-এ execution শুরু হতে পারে interpreter দিয়ে। কিন্তু program-এর কোনো অংশ যদি বারবার চলে, প্রতিবার একই interpretation overhead দেওয়াটা অপচয়।
এখানেই আসে JIT (Just-In-Time compilation)। একে "smart interpreter" ভাবার চেয়ে ভাবুন program চলার মাঝখানেই চলতে থাকা একটা compiler হিসেবে — যে interpreter-কে সরিয়ে দেয় না, বরং তার পাশে বসে কাজ করে।
Program চলার সময় সে তথ্য জমায় — কোন function বা loop কতবার চলছে, কী ধরনের value নিয়ে চলছে, behavior কেমন। কোনো অংশ যথেষ্ট "hot" হলে (ঘন ঘন execute হচ্ছে) সে সেই অংশের জন্য machine code তৈরি করতে পারে, আর পরের বার সেই compiled রূপটাই চলে।
কল্পনা করুন — একজন live translator শুরুতে সব বাক্য অনুবাদ করছে। কিন্তু কয়েকটা phrase বারবার আসছে ("Ladies and gentlemen", "As I was saying")। কয়েকবার শোনার পর সে সেই phrase-এর জন্য একটা তৈরি অনুবাদ হাতে রেখে দিল। এখন সেটা শুনলেই সঙ্গে সঙ্গে বলে দেয়। JIT-এর "hot code" ধরার intuition-টা ঠিক এমন।
তবে JIT-এর বানানো machine code চিরস্থায়ী নয়। Optimizer runtime-এর কিছু অনুমানের ওপর ভিত্তি করে optimize করে; পরে সেই অনুমান ভুল প্রমাণিত হলে engine সেই optimized code ছেড়ে অন্য execution path-এ ফিরে যেতে পারে। এই কারণেই JIT-কে adaptive optimization হিসেবে ভাবা যায়।
এখানে Trade-off আছে — compilation নিজেই CPU time খায়, optimized code memory-ও নেয়। কিন্তু দীর্ঘক্ষণ চলা, বারবার execute হওয়া code-এ সেই বিনিয়োগ পরে পুষিয়ে যায়।
নিচের যন্ত্রে loop চালান — দেখুন কখন square() "hot" ধরা পড়ে আর compiled native-এ পরিণত হয়:
যখন আপনি node hello.js চালান
এবার সব একসাথে করে দেখা যাক। একটা CPU-heavy example নিলাম, যাতে JIT-এর ভূমিকাটা পরিষ্কার হয়:
node hello.js চালালেন। V8-এর আসল execution pipeline এর চেয়ে জটিল, আর version ভেদে বদলায়ও — তাই নিচেরটা একটা simplified mental model, কোনো instruction-by-instruction trace নয়:
- Node চালু হয় আর V8 engine load করে।
- V8 source parse করে তার গঠন বোঝে — সেই গঠনের রূপটাই AST।
- সেই representation থেকে V8 bytecode তৈরি করতে পারে।
- Execution শুরু হয় interpreter-এর মাধ্যমে।
- চলার সময় V8 code-এর behavior নিয়ে তথ্য জমায়।
- কোনো function বা loop যথেষ্ট hot আর optimization-উপযোগী হলে V8 তার জন্য compiled machine code তৈরি করতে পারে (square() আর loop body — দুটোই এই পথে যেতে পারে)।
- পরের execution সেই compiled রূপ ব্যবহার করতে পারে; আর optimization-এর অনুমান ভেঙে গেলে আবার অন্য path-এ ফিরে যেতে পারে।
পুরোটা conceptually এভাবে সাজানো যায়:
Source code
↓
parsing / internal representation
↓
bytecode ← in many systems
↓
interpretation
↓
runtime information
↓
JIT compilation ← where it pays off
↓
native machine code
↓
CPUএকটা কথা মনে রাখা জরুরি — JIT মানে "JavaScript C হয়ে যাওয়া" নয়। এর মানে হলো, runtime যেখানে লাভ দেখে, সেখানে JavaScript-এর execution-এর কিছু অংশ native machine instruction-এ পরিণত হয়। ঠিক কখন, কোন অংশে, কতটা — সেটা input, runtime behavior আর engine version-এর ওপর নির্ভর করে। "ঠিক ১০০০ বার call হলেই compile হবে" জাতীয় কোনো fixed নিয়ম নেই।
এই পুরো process আপনার কাছে invisible। আপনি শুধু output দেখছেন।
আধুনিক জটিলতা: সীমানা মুছে যাচ্ছে
এবার একটা গুরুত্বপূর্ণ conclusion-এ আসা যাক। আমরা প্রায়ই বলি — "C compiled language", "Python interpreted language", "JavaScript interpreted language"। Beginner-এর জন্য এই label-গুলো কাজে দেয়, কিন্তু এগুলো ভাষার স্থায়ী পরিচয় নয়। একই ভাষার ভিন্ন implementation ভিন্ন execution strategy ব্যবহার করতে পারে।
| ভাষা | কীভাবে চলে |
|---|---|
| Python | CPython source → bytecode, তারপর সেই bytecode execute করে। PyPy আবার JIT compilation যোগ করে। |
| JavaScript | V8-এর মতো engine bytecode/interpreter-এর সঙ্গে একাধিক compilation tier ব্যবহার করে। |
| Java | Source → JVM bytecode; JVM সেটা execute করে আর runtime-এ JIT-compile করতে পারে। |
| C# | Source সাধারণত intermediate representation-এ compile হয়, .NET runtime সেটা execute/JIT করে। |
| C, Go, Rust | সাধারণ ব্যবহারে ahead-of-time compilation — native machine code তৈরি হয়। |
তাই "এটা compiled না interpreted?" প্রশ্নের চেয়ে বেশি কাজে দেয় এই প্রশ্নটা — এই implementation code-টা আসলে কীভাবে চালায়? সেখান থেকে যা জানতে চাই:
- Native code কি আগে থেকেই তৈরি হচ্ছে?
- মাঝখানে কোনো intermediate representation আছে?
- Interpreter আছে?
- JIT compilation আছে?
- Runtime-এর তথ্য কি optimization-এ কাজে লাগানো হচ্ছে?
দুই প্রান্তের দুটো সাধারণ পথ পাশাপাশি রাখলে ছবিটা পরিষ্কার হয় — একদিকে runtime-নির্ভর পথ, অন্যদিকে C-র চেনা AOT পথ:
Source code
↓
compiler
↓
assembly / object code
↓
linking
↓
native executable
↓
CPUআর practical প্রশ্নগুলো তো থেকেই যায় — startup দ্রুত দরকার, নাকি দীর্ঘক্ষণ চলা কাজে গতি? একটা portable binary চাই, নাকি এমন কিছু যা runtime থাকলেই সব platform-এ চলবে?
- Machine code আর hexadecimal এক জিনিস নয়। Machine code হলো CPU-র executable instruction-এর encoding; hexadecimal সেই byte-গুলো মানুষের লেখার notation।
- Compiler চালানোর আগেই code process করে executable/native রূপ তৈরি করতে পারে।
- Interpreter runtime-এ source বা intermediate representation চালায় — "line ধরে ধরে source অনুবাদ" এর একমাত্র অর্থ নয়।
- Bytecode হলো source আর native machine code-এর মাঝের একটা intermediate representation, আর VM হলো সেই bytecode চালানোর software environment।
- JIT হলো program চলার মাঝখানে চলা compiler — runtime-এর তথ্য দেখে যেখানে লাভ, সেখানে machine code তৈরি করে; সেই optimization চিরস্থায়ীও নয়।
- Compiled বনাম interpreted একটা implementation strategy, কোনো ভাষার স্থায়ী পরিচয় নয়।