Exception Internals — throw থেকে catch পর্যন্ত সিলিকনের রাস্তা
Exception Handling Internals
try/catch দেখতে if-এর মতো, কিন্তু throw চলে metadata-driven stack-walk-এ — happy path শূন্য instruction, throw-এ মাইক্রোসেকেন্ড; এই অসম খরচের রূপরেখাই C++/Rust/Go-র error strategy-র ভিত্তি।
আগে এটা বুঝি
আপনি হাজারবার লিখেছেন:
try {
result = parse(input);
} catch (const ParseError &e) {
log(e.what());
}পড়তে দেখতে if/else-এর মতোই — condition ভুল হলে ওপাশের branch-এ যাও।
কিন্তু assembly module-এর stack-frames-and-function-calls লেসন মনে করুন:
machine-এ control flow মানে মাত্র তিনটা জিনিস — sequential fetch, conditional
branch, আর call/ret-এর PC manipulation। “পাঁচ frame নিচে থেকে সোজা
main-এর catch-এ লাফ” — machine language-এ এমন কোনো instruction-ই নেই।
তাহলে throw আসলে কী? উত্তরটা ভাষার runtime-এর সবচেয়ে সূক্ষ্ম কাজগুলোর একটা: compiler আর runtime মিলে এমন একটা machinery বানায় যেখানে সাধারণ execution-এ exception-এর কোনো অস্তিত্বই দেখা যায় না, কিন্তু throw-এর মুহূর্তে পুরো call stack উল্টে গিয়ে পথের সব cleanup ঠিকঠাক চালিয়ে দেয়। আজকের experiment-এ দেখবেন try-block আসলে একটা machine instruction-ও যোগ করে না — তবু throw একবারে মাইক্রোসেকেন্ড খায়।
আজকের প্রশ্ন তিনটা: (১) try-তে ঢোকাই মাত্র কী খরচ, আর কেন কোনো ভাষাই প্রতি-try register-snapshot আর নেয় না? (২) throw-এর মুহূর্তে runtime কোন metadata পড়ে, কোন দুই phase-এ কী করে? (৩) cleanup কে চালায় — ভাষা, runtime, নাকি programmer?
মূল ধারণা
দুইটা মডেল — snapshot বনাম মানচিত্র
Machine-এ non-local control transfer-এর দুইটা মৌলিক রাস্তা আছে, আর সব ভাষা এই দুটোর কোনো একটার variant:
| মডেল | try-তে ঢোকার খরচ | Normal exit | Throw-এর খরচ | কোথায় |
|---|---|---|---|---|
| State snapshot (setjmp/longjmp) | Register+SP+PC save — প্রতি try-এ ~১০–১০০ ns | ~শূন্য | Jump নিজে সস্তা, ~০.১–১ µs | C setjmp, legacy x86-32 C++, Lua pcall-সদৃশ |
| Table-based (“zero-cost”) | শূন্য instruction | শূন্য | ~১–১০ µs — table walk + দুই-phase unwind | C++ (Itanium ABI), Java, Rust, Python |
সংখ্যাগুলো ballpark — CPU ভেদে বদলাবে। কিন্তু অনুপাতটা বদলাবে না, এবং সেটাই design-এর মূল: প্রতিটা প্রোগ্রামে try হাজারবার, throw বছরে ক’বার। Snapshot মডেল common case-কে (try entry) দামি করে rare case-কে (throw) সস্তা করে; table-based মডেল ঠিক উল্টো। Error বিরল — তাই industry এক ভোটে table-based-এ গেছে।
Snapshot মডেল — setjmp/longjmp
jmp_buf saved; // register snapshot-এর পাত্র
if (setjmp(saved) == 0) {
risky(); // normal path — ভেতরের কেউ
// longjmp(saved, 1) ডাকতে পারে
} else {
// এখানে পৌঁছায় longjmp থেকে — "exception handler"
}setjmp দুইবার ফেরত আসে: direct call-এ (0), আর longjmp দিয়ে
(nonzero)। jmp_buf-এ থাকে callee-saved registers, stack pointer, mangled
return address — সম্পূর্ণ machine-state photo; longjmp photo restore করে
PC-কে setjmp-এর পরের লাইনে ফেলে দেয়। আজকের build-এ এটা নিজের হাতে বানাবেন।
দাম: প্রতিটা try-তে ঢোকাই মানে সম্পূর্ণ snapshot — hot loop-এ try ঘুরিয়ে দিলে performance ডোবে। আর intermediate frame-গুলো কিছুই জানে না — automatic destructor/cleanup এই মডেলে নেই।
Table-based মডেল — happy path শূন্য
আধুনিক C++/Rust (Linux/x86-64, Itanium C++ ABI) এমন চুক্তি করে:
Normal execution-এ exception machinery-র জন্য চলে ঠিক শূন্য instruction।
Try block একটা plain region — কোনো setup, কোনো flag, কিছুই না।
বিনিময়ে compiler প্রতিটা function-এর জন্য দুইটা নীরব metadata table রাখে:
.eh_frame — প্রতিটা frame কীভাবে ভাঙা যায়: caller-এর return
address কোথায়, callee-saved register-রা stack-এর
কোন offset-এ বসে (unwinder-এর ভাঙার-নকশা)
.gcc_except_table — এই function-এর কোন PC-range-এ exception উঠলে কোন
handler/cleanup চলবে (LSDA — Language Specific
Data Area)Throw হলে runtime এই টেবিল পড়ে stack নামে — বিস্তারিত hood section-এ। Happy path-এ কিছুই হয় না, তাই নাম zero-cost। মনে রাখুন: zero-cost-এর অর্থ “normal path free”, কখনোই “throw free” না।
RAII — cleanup কে চালায় কে?
C++-এ cleanup-এর দায়িত্ব ভাষার, programmer-এর না — এটাই RAII (Resource Acquisition Is Initialization): resource-এর lifetime = object-এর lifetime, আর object-এর lifetime শেষ হয় scope-এ — exception-এর ভয়েও না। Stack unwinding-এর phase 2 পথে পড়ে এমন প্রতিটা object-এর destructor চালিয়ে যায়। memory-models লেসনে দেখেছেন stack allocation কত সস্তা — RAII সেই সস্তাকে deterministic cleanup-এ রূপান্তর করে, GC-র finalizer-এর মতো “কবে হবে কে জানে” নয়।
Go-র উত্তর ভিন্ন: destructor নেই, তাই cleanup manual — defer। Panic হলে
deferred function-রা LIFO চলে, recover শুধু panicking goroutine-এর
deferred ফাংশনের ভেতরেই কাজ করে। Rust-এ destructor আছে (Drop), panic=
unwind হলে সেগুলোও unwinding-এ চলে — C++-এর RAII-র সমকক্ষ।
ভেতরে কী ঘটছে
Throw-এর সম্পূর্ণ যাত্রা — ধাপে ধাপে
throw std::runtime_error("boom") চাপলে ঘটে এই ক্রম (Itanium ABI, Linux):
1. __cxa_allocate_exception(size) — libstdc++: heap-এ emergency buffer
থেকে exception object allocate
2. copy-initialize — আপনার expression ওই memory-তে construct
3. __cxa_throw(obj, &type_info, dtor)
└─► _Unwind_RaiseException(context) — libgcc: আসল unwinder
PHASE 1 — SEARCH (handler কোথায়?)
4. বর্তমান PC থেকে শুরু করে প্রতিটা frame-এ:
a. .eh_frame entry decode → এই PC-র CFA (canonical frame address),
return address, saved-register location
b. ঐ function-এর LSDA (.gcc_except_table) আছে? না থাকলে frame-টা
exception-transparent — পরের frame
c. personality routine (__gxx_personality_v0) ডাকো — LSDA-র call-site
table-এ দেখো: এই PC-range-এ catch-clause আছে, type match করে?
5. match পেলে → সেই frame-এর landing pad নোট করে phase 2
root frame পর্যন্ত না পেলে → std::terminate (catch ছাড়া throw-এর ভাগ্য)
PHASE 2 — UNWIND (ফেরত নামো, পথে সাফ করো)
6. আবার নিচ থেকে handler-frame পর্যন্ত, প্রতিটা frame-এ:
a. LSDA-য় cleanup action আছে? (catch না থাকলেও destructor-guard
থাকতে পারে!) → landing pad-এ jump করে destructor চালিয়ে resume-unwind
b. handler frame-এ পৌঁছে: catch parameter-টা exception object-এর সাথে
type-match করে bind করো
7. landing pad-এ jump — এখন থেকে আবার ordinary code
8. catch scope শেষ হলে __cxa_end_catch → exception object freeলক্ষ্য করুন stack দুইবার traverse হয় — একবার খুঁজতে, একবার ভাঙতে। Phase separation-এর কারণ: search-এর সময় stack এখনো intact, তাই কোনো state নষ্ট না করে “handler আদৌ আছে কি না” আগে জেনে নেওয়া যায়।
- Your codethrow expr; — দেখতে statement, আসলে runtime-library call-এর sugar
- libstdc++ (__cxa_*)exception object allocate, type-info attach, unwinder-কে hand-off
- libgcc (_Unwind_*)দুই-phase engine: .eh_frame binary-search per frame, personality callback
- Metadata tables.eh_frame (ভাঙার নকশা) + .gcc_except_table/LSDA (handler/cleanup map) — executable-এর নীরব অংশ
- Your destructorsphase 2 পথে প্রতিটা frame-এর landing pad সংক্ষেপে active হয়ে RAII cleanup চালায়
কেন exception FFI boundary পার হয় না
C function-এর জগতে “exception” বলে কোনো contract নেই — সে LSDA লেখে না,
personality routine-ও জানে না। C++ exception C callback-এর ভেতর দিয়ে
propagate করার চেষ্টায়: C frame unwind হবে কি না undefined (C-র cleanup
concept-ই নেই), আর non-C++ toolchain-এ compile হওয়া frame-এ মানচিত্রই থাকে
না। তাই নিয়ম কঠোর: boundary-তে exception convert — error code-এ (C++),
Result-এ (Rust), নয়তো boundary-তেই abort। Rust 1.81 থেকে extern "C" fn-এর
ভেতরে panic হলে সরাসরি abort — language-ই crossing panic-কে UB থেকে
crash-এ নামিয়ে এনেছে।
panic=unwind বনাম panic=abort — Rust-এর দুই মোড
Unwind মোডে (default) panic ঠিক আজকের দুই-phase machinery-ই চালায় — Drop
চলে, thread মরে, process বাঁচে; catch_unwind দিয়ে ধরাও যায়। Abort মোডে
(panic = "abort" Cargo profile-এ) panic মানেই process death — unwind table
দরকার নেই, binary ছোট, embedded/firmware-friendly, fail-fast দর্শনে মানানসই।
Trade-off এক লাইনে: unwind recovery দেয়, abort determinism আর আকার দেয়।
Go আবার নিজের রাস্তা: panic কোনো shared unwinder ব্যবহার করে না — goroutine
নিজের deferred-function chain ধরে নামে (runtime-systems লেসনের G-M-P জগতের
ভেতরেই সব); unrecovered panic পুরো process মারে, আর recover শুধু panicking
goroutine-এর সরাসরি deferred ফাংশনের ভেতরেই কাজ করে।
উদাহরণ
Destructor-রা ঠিক কোন ক্রমে চলে — একটা সম্পূর্ণ trace
// unwind.cpp — RAII meets unwinding
#include <cstdio>
#include <stdexcept>
struct Guard {
const char *name;
explicit Guard(const char *n) : name(n) { printf(" open %s\n", n); }
~Guard() { printf(" close %s\n", n); } // unwind phase 2 এটাই ডাকে
};
void level3() {
Guard conn("level3/connection");
printf(" level3: throw!\n");
throw std::runtime_error("boom");
}
void level2() {
Guard txn("level2/transaction");
level3();
printf(" level2: never runs\n"); // throw লাফ দিয়ে পার হয়ে গেছে
}
void level1() {
Guard session("level1/session");
level2();
}
int main() {
try {
level1();
} catch (const std::runtime_error &e) {
printf(" main: caught '%s'\n", e.what());
}
}Output — প্রতিটা লাইনের পাশে কোন মেকানিজম লিখল:
open level1/session normal construction
open level2/transaction normal construction
open level3/connection normal construction
level3: throw! ── PHASE 1: search ──
level3/2/1: কোনো catch নেই (শুধু cleanup)
main: catch (runtime_error) match!
── PHASE 2: unwind ──
close level3/connection landing pad → destructor (LIFO!)
close level2/transaction landing pad → destructor
close level1/session landing pad → destructor
main: caught 'boom' handler frame-এ landing pad-এ jumpতিনটা observation:
- Destructor-রা construction-এর ঠিক উল্টো ক্রমে (LIFO) — কারণ তারা stack-এ বসে, আর unwind stack-ই ভাঙে। Nested transaction rollback এভাবেই নিজে নিজে ঠিক হয়।
printf("never runs")কখনো চলে না — throw একটা frame-কে পুরোপুরি স্কিপ করে; শুধু destructor চলে, বাকি কোড না। এটাই “invisible goto”-র ভয়।- level1/level2-তে catch clause না থাকলেও তাদের destructor চলেছে — phase 2 প্রতিটা frame-এর cleanup map পড়ে, শুধু handler map না।
ছবিতে
দুই-phase stack walk
উঁচু address (main-এর দিক)
┌────────────────────────────────┐
│ main │ ◄── phase 1: STOP — catch found!
│ try { level1(); } │ phase 2: শেষ landing pad এখানে
│ catch (runtime_error&) {...} │
├────────────────────────────────┤ ▲
│ level1 frame │ │ phase 2 — UNWIND
│ Guard session [cleanup!] │ │ প্রতি frame: LSDA পড়ো,
├────────────────────────────────┤ │ cleanup থাকলে landing pad-এ
│ level2 frame │ │ jump → destructor চালাও →
│ Guard txn [cleanup!] │ │ resume unwinding
├────────────────────────────────┤ │
│ level3 frame throw! │ │
│ Guard conn [cleanup!] │ │
└────────────────────────────────┘ │
নিচু address │
phase 1 — SEARCH: throw বিন্দু থেকে উপরে ──► ──►
প্রতি frame: .eh_frame decode → LSDA? → personality routine:
"এই PC-range-এ এই type-এর handler আছে?" — না → next framephase 1: throw ●──search──►●──search──►●──search──►★ handler
(stack intact — শুধু টেবিল পড়ছে)
phase 2: throw ●◄──cleanup─●◄──cleanup─●◄──────────★ jump into catch
(পথে প্রতিটা frame destructed, SP/PC restore হয়ে যায়)নিজে চালিয়ে দেখুন
Zero-cost যাচাই — try ফ্রি, throw দামি
ধাপ ১ — দুই build, একই সোর্স:
// eh_size.cpp
#include <cstdio>
#ifdef USE_THROW
#include <stdexcept>
#endif
static long work(long x) {
#ifdef USE_THROW
try {
if (x == 42) throw std::runtime_error("answer");
#endif
return x * 3 + 1;
#ifdef USE_THROW
} catch (const std::runtime_error &) {
return -1;
}
#endif
}
int main() {
long acc = 0;
for (long i = 0; i < 100000000; ++i) acc += work(i);
printf("%ld\n", acc);
return 0;
}g++ -O2 -o eh_plain eh_size.cpp
g++ -O2 -DUSE_THROW -o eh_throw eh_size.cpp
g++ -O2 -DUSE_THROW -fno-exceptions -o eh_noex eh_size.cpp 2>&1 || \
echo "(throw ব্যবহার করা হয়েছে, তাই -fno-exceptions compile fail — এটাও একটা ডেটা)"
ls -la eh_plain eh_throw | awk '{print $5, $9}'
readelf -S eh_throw | grep -E "eh_frame|gcc_except"
readelf -S eh_plain | grep -E "eh_frame" | head -2
time ./eh_plain
time ./eh_throwTypical pattern:
eh_plain: ~16 KB, .eh_frame ছোট, .gcc_except_table নেই
eh_throw: ~17 KB, .eh_frame + .gcc_except_table দৃশ্যমান
time eh_plain ≈ time eh_throw (±noise — happy-path overhead: শূন্য)Size difference-টাই exceptions-এর দাম: নীরব metadata। Speed difference absent — এটাই “zero-cost” শব্দটার empirical অর্থ।
ধাপ ২ — throw-rate benchmark:
// throw_rate.cpp — কত ঘন ঘন throw করলে কী হয়
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
static void may_throw(long i, long rate) {
if (rate > 0 && i % rate == 0) throw std::runtime_error("boom");
}
int main(int argc, char **argv) {
long rate = argc > 1 ? atol(argv[1]) : 0; // 0 = কখনো throw না
long acc = 0;
for (long i = 0; i < 10000000; ++i) {
try {
may_throw(i, rate);
acc += 1;
} catch (const std::runtime_error &) {
acc -= 1;
}
}
printf("%ld\n", acc);
return 0;
}g++ -O2 -o throw_rate throw_rate.cpp
./throw_rate 0 && time ./throw_rate 0
./throw_rate 1000000 && time ./throw_rate 1000000
./throw_rate 10000 && time ./throw_rate 10000Typical pattern (১ কোটি iteration):
rate=0 : ~30 ms — try/catch নিজে ফ্রি
rate=1M : ~35 ms — ১০টা throw: প্রায় অলেখযোগ্য
rate=10K : ~120 ms — ১,০০০ throw: দৃশ্যমান
rate=100 : ~2 s — ১ লাখ throw: throw-প্রতি ~২০ µs স্তরেthrow-প্রতি খরচ বের করুন: (t_rate − t_0) / throws — আপনার machine-এর
unwind-এর unit price। এই সংখ্যাটাই “exceptions দিয়ে control flow বানানো
যাবে না” নীতির quantitative ভিত্তি।
যা প্রমাণিত: happy path-এ exception machinery অদৃশ্য (speed identical), কিন্তু throw একটা দামি runtime protocol — ভাষাগত সিদ্ধান্তগুলো (error codes বনাম exceptions) এই অসমতার উপরেই দাঁড়িয়ে।
exceptions enable করলে binary-তে .eh_frame/.gcc_except_table section জন্মায় কিন্তু try-containing hot loop-এর speed-এ পার্থক্য হয় না (happy path শূন্য instruction); throw-এর rate বাড়ালে সময় শত-গুণ বেড়ে যায় — cost asymmetry-র সরাসরি মাপ।
নিজে বানান
Mini exception system — jmp_buf stack + TRY/CATCH macros
- Frame struct বানান — jmp_buf + prev pointer দিয়ে try-frame-দের linked stack
- TRY macro: নিজের frame push করে setjmp করে; 0 ফেরত এলে protected region
- THROW macro: top frame pop করে longjmp — message গ্লোবাল slot-এ রেখে
- CATCH macro: nonzero return-এর পথ — এখানে cleanup আর rethrow-এর জায়গা
- দুই frame গভীর থেকে throw করে unwind path আর cleanup order যাচাই করুন
লক্ষ্য: আজকের concept-এর snapshot মডেলটা নিজের হাতে — আর দেখা — এই মডেলে intermediate frame কেন নীরব (কোনো automatic cleanup নেই), তাই cleanup programmer-কেই লিখতে হয়।
/* mini_exc.c — longjmp-ভিত্তিক mini exception system */
/* compile: gcc -O0 -o mini_exc mini_exc.c */
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
struct Frame {
jmp_buf jb; /* এই try-এর machine-state snapshot */
struct Frame *prev; /* বাইরের try — unwind-এর পরের স্টপ */
};
static struct Frame *g_top = NULL;
static const char *g_msg = NULL;
#define TRY \
do { \
struct Frame _frame; \
_frame.prev = g_top; \
g_top = &_frame; \
int _exc = setjmp(_frame.jb); \
if (_exc == 0) {
#define CATCH \
} else { \
/* THROW frame pop করে গেছে; এখানে */ \
/* cleanup, log, rethrow — যা খুশি */ \
#define END_TRY \
} \
if (_exc == 0) g_top = _frame.prev; \
} while (0)
#define THROW(msg) \
do { \
if (g_top == NULL) { \
fprintf(stderr, "FATAL unhandled: %s\n", msg); \
exit(1); \
} \
g_msg = (msg); \
struct Frame *_f = g_top; \
g_top = _f->prev; /* pop FIRST */ \
longjmp(_f->jb, 1); /* then jump */ \
} while (0)
/* ── তিন স্তরের কল-চেইন ──────────────────────────────────────── */
static void level2(void) {
printf(" level2: enter\n");
THROW("disk full");
printf(" level2: never runs\n");
}
static void level1(void) {
printf(" level1: enter, resource খোলা\n");
TRY {
level2();
printf(" level1: never runs\n");
} CATCH {
/* unwind এই frame-এ এসে থামেছে — cleanup এখানে manual */
printf(" level1: [cleanup] resource বন্ধ\n");
THROW("db error: disk full"); /* rethrow → main পর্যন্ত */
} END_TRY;
}
int main(void) {
printf("== scenario 1: দুই frame পার হয়ে unwind ==\n");
TRY {
level1();
} CATCH {
printf(" main: caught '%s'\n", g_msg);
printf(" main: [cleanup] buffer flush\n");
} END_TRY;
printf("== scenario 2: nested try, swallow ==\n");
TRY {
TRY {
THROW("inner panic");
} CATCH {
printf(" inner caught: '%s'\n", g_msg); /* rethrow না */
} END_TRY;
printf(" outer continues normally\n");
} CATCH {
printf(" outer caught (never runs)\n");
} END_TRY;
printf("== execution resumes ==\n");
return 0;
}প্রত্যাশিত output:
== scenario 1: দুই frame পার হয়ে unwind ==
level1: enter, resource খোলা
level2: enter
level1: [cleanup] resource বন্ধ
main: caught 'db error: disk full'
main: [cleanup] buffer flush
== scenario 2: nested try, swallow ==
inner caught: 'inner panic'
outer continues normally
== execution resumes ==যা ঘটল: level2-এর THROW নিজের frame pop করে এক লাফে level1-এর
setjmp-এ — level2-এর পরের কোড চলেই না, এবং level2-তে কোনো cleanup
হয়ও না (snapshot মডেলে সে কিছুই জানে না — C++-এর RAII-র এই absence-ই
মডেল দুটোর সবচেয়ে বড় পার্থক্য)। level1-এর CATCH cleanup চালিয়ে rethrow
করে — THROW তখন main-এর frame pop করে। Pop-before-jump ordering ঠিক না
রাখলে infinite loop বা stack corruption হতো — নিজে ভেঙে দেখুন।
নিজে বাড়ান:
-O2দিয়ে compile করে ভাঙুন: setjmp-longjmp-এর মাঝে modify হওয়া local variablevolatileনা হলে UB — documentation-এর কোন বাক্যটা কাজ করছে?- Exception type system যোগ করুন:
THROW(code)আরCATCH_IF(code_eq)— personality routine-এর type-matching-এর হাতে-লেখা রূপ। strace ./mini_excচালান — পুরো machinery-তে একটা syscall-ও নেই; সব user-space registers আর memory-তে।
বাস্তব সিস্টেমে
Exception machinery production-এ যেখানে দেখা যায়
Game engines আর HFT। Unreal Engine আর বেশিরভাগ console engine
-fno-exceptions-এ build হয়: frame budget deterministic রাখতে throw-এর
µs-scale spike অগ্রহ্য; binary size আর compile-time বাড়তি কারণ।
Chrome/Chromium। বহু বছর exceptions disabled — Result-স্টাইল error
handling; বিশাল codebase-এ exception-safety audit অসম্ভব, আর unwinding-এর
hidden control flow প্রতিটা লাইনের অর্থ বদলে দেয়।
Embedded/ARM। ARM EABI-র নিজস্ব unwind format (EHABI) — একই দর্শন,
ভিন্ন encoding; flash-size-sensitive target-এ -fno-exceptions +
-fno-unwind-tables KB-র হাজার বাঁচায়।
JVM আর Python — উল্টো দর্শন। Java-তে exception যথেষ্ট সস্তা (metadata +
JIT), control-flow-ish ব্যবহারও চলে। Python এক ধাপ এগিয়ে: StopIteration —
generator-এর termination signal নিজেই raised exception; প্রতিটা next() call
ভেতরে raise/handle-ই করে। “Error বিরল” assumption-টাই ভাষা ভেদে বদলায়।
Rust ecosystem। Default unwind রেখেও panic = "abort" embedded/WASM
target-এ popular; catch_unwind মূলত FFI boundary-তে (C callback-এর ভেতরের
Rust panic বাইরে ছিটকে না যাক) আর test harness-এ।
যে ভুলগুলো সবাই করে
“'Zero-cost exceptions' মানে exception ফ্রি।”
Free শুধু happy path-এ। Throw-এ ঘটে: exception object allocation, দুইবার stack walk, প্রতি frame-এ metadata decode, personality callback — একসাথে মাইক্রোসেকেন্ড-স্তর, ordinary function call-এর হাজার গুণ। নামটার পূর্ণ বাক্য: “zero-cost when not thrown”। এই অসমতাই বলে দেয় exception শুধু truly exceptional ঘটনার জন্য।
“try block লিখলেই ওই কোড ধীর হয়ে যায়।”
Table-based মডেলে try কোনো instruction generate করে না — আজকের experiment দেখিয়েছে identical timing। Legacy 32-bit x86 আর setjmp-style implementations-এ সত্যিই per-try cost ছিল, আর সেই যুগের পরামর্শ এখনো folklore হয়ে ঘোরে। Hot loop-এ try রাখা নিয়ে আজকে একমাত্র বাস্তব concern হলো optimizer-এর code-layout সিদ্ধান্ত, মেকানিজম নিজে না।
“Error codes সবসময় exceptions-এর চেয়ে দ্রুত।”
Happy path-এ error-code checking প্রতি call-এ compare+branch যোগ করে — branch predictor well-predicted হলে খরচ ছোট কিন্তু শূন্য না; আর code size বাড়ে, I-cache pressure বাড়ে। Zero-cost exceptions-এ happy path-এ ঠিক শূন্য। Rare-error workload-এ exceptions আসলে দ্রুতই হতে পারে। সিদ্ধান্তের metric performance একা না — propagation ergonomics, type safety, ABI constraint সব ঢোকে।
“Go-র panic/recover C++ exception-এর মতোই — শুধু নাম আলাদা।”
Mechanism আলাদা, philosophy আলাদা। Panic প্রতি-goroutine deferred chain
ধরে নামে (shared unwinder/table না), recover শুধু সরাসরি deferred ফাংশনে
কাজ করে, আর Go-র API culture panic-কে programmer-error (nil deref, index
out of range)-এর জন্য রাখে — expected failure-এ error value। C++-এ বিপরীতে
exception হলো expected-failure-র primary channel। তাই Go-তে if err != nil
হাজারবার, C++-এ try হাজারবার — দুই community-র cost-model বিশ্বাসের ফল।
বুঝেছেন কি না দেখুন
1Unwind-এর দুই phase-এর নাম, ক্রম আর কাজ লিখুন। Phase 1-এ stack-এর state
বদলায় কি?
স্মরণ
Phase 1 — SEARCH: throw বিন্দু থেকে root frame পর্যন্ত প্রতিটা frame-এর .eh_frame decode করে LSDA পড়া, personality routine দিয়ে handler type-match check; পেলে stop-target নোট। Phase 2 — UNWIND: নিচ থেকে handler frame পর্যন্ত আবার নেমে পথের সব cleanup/destructor চালানো, শেষে landing pad-এ jump। Phase 1-এ state বদলায় না — pure read-only walk; এজন্যই “handler আদৌ আছে?” আগে নিশ্চিত হওয়া যায়, না থাকলে terminate (অর্ধভাঙা stack ছাড়াই)।
2তিনটা nested function A (catch আছে), B (Guard object), C (Guard object +
throw) — C থেকে throw হলে destructor আর catch-এর চূড়ান্ত ক্রম লিখুন। B-তে
catch না থাকলেও কি B-র destructor চলে?
প্রয়োগ
ক্রম: C-র Guard destructor (unwind phase 2, সবার নিচের frame আগে) → B-র Guard destructor → A-র catch clause execute। হ্যাঁ, B-র destructor চলবেই — phase 2 প্রতিটা frame-এর cleanup entries দেখে, handler-entry না। Catch clause শুধু handler-frame-এ চলে, আর সেটা সবার শেষে।
3setjmp/longjmp মডেলে throw তুলনামূলক সস্তা, তবু সব modern ভাষা table-based-এ
গেছে। Frequency-asymmetry argument-টা পূর্ণ বাক্যে লিখুন।
যুক্তি
Try-এ ঢোকা আর throw করা — দুই event-এর frequency প্রায় সব প্রোগ্রামে হাজার-গুণ ভিন্ন: প্রতিটা risky operation একটা try দাবি করে (প্রতি request, প্রতি parse — millions/sec), কিন্তু throw বিরল (failures/sec অনেক কম)। Snapshot মডেল দাম বসায় high-frequency event-এ (প্রতি try-এ register-save), table-based মডেল low-frequency event-এ (প্রতি throw-এ table-walk)। খরচ যেখানে rare সেখানে সরানোই optimal — এই amortized reasoning-ই migration-এর কারণ। Counter-case: কিছু Lisp/Scheme runtime যেখানে “exceptional” ঘটনা আসলে frequent (continuation-heavy) — সেখানে snapshot-style আবার যুক্তিসঙ্গত।
4আপনি MCU firmware লিখছেন (256 KB flash, hard real-time ISR deadline)।
Exceptions enable রাখবেন না -fno-exceptions? চারটা স্বাধীন কারণ দিয়ে
decision justify করুন।
ডিজাইন
-fno-exceptions? চারটা স্বাধীন কারণ দিয়ে
decision justify করুন।-fno-exceptions: (১) Determinism — throw-এর µs-scale, data-dependent
latency ISR deadline ভাঙতে পারে; error path-ও bounded হতে হয়। (২) Size —
unwind tables + personality routines + landing pads বাদ দিয়ে কয়েক-KB flash
বাঁচে, 256 KB-তে প্রাসঙ্গিক। (৩) Heap — Itanium ABI exception object heap
চায়; firmware-এ heap প্রায়ই নিষিদ্ধ (fragmentation, non-determinism)।
(৪) Toolchain surface — পুরো call-graph জুড়ে exception-safety guarantee
audit করা embedded codebase-এ অবাস্তব; নিষেধাজ্ঞা সরাসরি সম্পত্তিটা দেয়।
Compensating pattern: error-code + assert + watchdog, আর critical cleanup-এ
scope-guard macro।
5C++ program একটা C callback ডাকে; callback-এর ভেতর থেকে একটা C++ function
call হয় যেটা throw করে, আর কেউ ধরে না — throw-টা C callback-কে পার হয়ে
উপরে যেতে চায়। ঠিক কোথায়, কেন সমস্যা?
যুক্তি
C callback-এর frame পার হওয়ার মুহূর্তেই। Phase 1 সেই frame-এ পৌঁছে .eh_frame decode করবে (C-ও সাধারণত টেবিল রাখে — debugger-দের জন্য), কিন্তু LSDA থাকবে না — C compiler কোনো language-specific handler map জানে না। Frame-টা “transparent” ধরে উপরে ওঠা technically possible, কিন্তু semantics undefined: C-র নিজস্ব cleanup expectation নেই, আর ওপাশের C++ caller-এর দৃষ্টিতে মাঝখানে একটা foreign frame “অলসভাবে পার হয়ে গেল” — কোনো standard এই composition define করে না। বাস্তবে ভিন্ন toolchain/linker combination-এ ভিন্ন ফল। তাই ABI নিয়ম: boundary-তে convert (error code) বা abort; Rust-ও 1.81 থেকে extern “C”-এর ভেতরের panic-কে forced-abort করে ঠিক এই গর্ত বন্ধ করেছে।
এরপর কী
এরপর কী
Control flow-র দুইটা চরম আমরা দেখলাম: গণনা কখন হবে (evaluation strategy), আর control কীভাবে উল্টোপথে পালায় (unwinding)। কিন্তু এতক্ষণের সব কাহিনি একটা মাত্র execution thread-এর। এখন সেই প্রশ্ন: একই সমস্যা — শত শত কাজ একসাথে — ভাষারা চার রকম উত্তর দিয়েছে: OS threads + locks, async/await state machines, M:N green threads, আর channels/actors। কোনটা কোথায় জেতে, deadlock-এর class কীভাবে বদলায়, আর একটা cooperative scheduler নিজের হাতে ৮০ লাইনে বানিয়ে interleaving determinism প্রমাণ করা যায় কি না — পরের লেসনে।
পরের লেসন: Concurrency Model — threads, async/await, actors, CSP: একই সমস্যার চার উত্তর।
আরও পড়ুন
- Itanium C++ ABI — Exception Handling sections — Christophe de Dinechin (editor), Intel et al. · Zero-cost EH, two-phase unwind, LSDA আর personality routine-এর primary specification
- Programming Language Pragmatics, Exception Handling chapters — Michael L. Scott · setjmp/longjmp বনাম table-based model-এর তুলনামূলক survey
- The DWARF Debugging Standard — Call Frame Information — DWARF Standards Committee · .eh_frame যে DWARF CFI format-এর ভিত্তিতে দাঁড়িয়ে তার spec
- Go Blog — Defer, Panic, and Recover — Andrew Gerrand, The Go Blog · Go-র panic/recover semantics-এর canonical বর্ণনা