Reference Counting — গোনা রাখা, cycle-এর ফাঁদ, আর weak reference
Reference Counting and Cycles
প্রতিটা assignment-এ গুনে রাখা — deterministic আর simple, কিন্তু cycle তাকে চিরতরে ফাঁসে ফেলে; এই লেসনে RC-র গণিত, ফাঁদ, আর weak reference-এর মুক্তিপথ।
আগে এটা বুঝি
গত লেসনের collector ছিল জরিপকর্তা — মাঝে মাঝে এসে পুরো heap traverse করে যায়। এই লেসনের mechanism সম্পূর্ণ ভিন্ন দর্শনের: জরিপ নেই, শুধু হিসাব। প্রতিটা object নিজের মাথায় একটা counter বহন করে — “আমার দিকে এখন কয়টা reference আছে”। কেউ নতুন alias বানালে counter বাড়ে, কেউ reference ছাড়লে কমে, আর শূন্যে পৌঁছালেই object মৃত — সাথে সাথে, deterministic ভাবে।
দর্শনগত পার্থক্যটা এক লাইনে:
Tracing: "মরণ ঘোষণা করব জরিপে — periodic, batch, deferred।"
Counting: "মরণ ঘোষণা করব হিসাবে — incremental, per-event, immediate।"CPython-এর প্রতিটা object, Swift-এর প্রতিটা class instance, PHP-র প্রতিটা zval, Windows COM-এর প্রতিটা interface — এই সবাই আজও এভাবেই বাঁচে-মরে। অর্থাৎ RC কোনো historical foot-note না, production-এর সবচেয়ে বেশি ব্যবহৃত automatic memory management পরিবারগুলোর একটা।
কিন্তু এই সরলতার একটা ভয়ংকর গর্ত আছে — cycle। দুইটা object একে অপরকে ধরে রাখলে কেউই কখনো শূন্যে পৌঁছায় না, যতক্ষণ না বাইরে কোনো root থেকে তাদের দিকে পৌঁছানো যায় — tracing-এ এরাই প্রথম শিকার, counting-এ এরা চিরকালের বন্দি। আজকের লেসনের দ্বিতীয়ার্ধ এই ফাঁদ, তিনটা মুক্তিপথ (weak reference, cycle-collecting fallback, acyclic-by-construction ownership) আর নিজের হাতে leak বানিয়ে-ঠিক করার উপর।
মূল ধারণা
Mechanics — তিনটা নিয়ম
প্রতিটা heap object-এর header-এ একটা integer field (refcnt)। তারপর:
১. ALIAS (copy a pointer): refcnt++ — "retain", "inc_ref", Py_INCREF
২. DESTROY a reference (a dies): refcnt--
৩. refcnt == 0: object-এর destructor চলে,
তারপর সে নিজের সব field-এর reference
ছেড়ে দেয় (প্রতিটার refcnt--) —
chain reaction হতে পারে — তারপর memory freeChain reaction-টা খেয়াল করুন — এখানেই RC-র সবচেয়ে সুন্দর property:
root ──► A(3) ──► B(2) ──► C(1)
B-র reference ছাড়লে:
A-- → A(2); B-- → B(1) (শুধু direct effect)
A-ও ছাড়লে:
A-- → A(1)... A-- → A(0) → A মরে!
A মরার সময় B-র reference ছাড়ে → B-- → B(0) → B মরে!
B মরার সময় C-র reference ছাড়ে → C-- → C(0) → C মরে!পুরো subtree cascade-এ মরে গেল, ঠিক সেই microsecond-এ যখন শেষ external reference গেল। Java-তে এই subtree-র মৃত্যুর জন্য collector-এর রান পর্যন্ত অপেক্ষা; RC-তে timing-টা আপনার কোডের structure-এই লেখা। Destructor-এ side-effect (file close, log flush) লিখলে সেটা predictable সময়ে ঘটে — RAII-র deterministic সুবিধাটা automatic management-এর সাথেও পাওয়া গেল।
দাম ১ — প্রতিটা assignment-এ traffic
Counter update “ফ্রি” না। x = y একটা pointer-copy মাত্র হলে ভালো হতো, কিন্তু RC-জগতে সেটা:
x = y ⇒ INCREF(y->target); old = x->target; x = y; DECREF(old);
↑ read+write counter ↑ আরেকটা read+write (old target-এ)চারটা extra memory access প্রতি pointer-assignment-এ — আর counter আর object ভিন্ন cache line-এ থাকলে প্রতিটাই potential cache miss। Hot loop-এ pointer reassignment থাকলে এই tax throughput-এ সরাসরি দেখা যায়। এজন্যই optimization জন্ম নিয়েছে: deferred RC (stack-local reference-এর count ধরে না — stack নিজেই root), coalescing (একই object-এর inc/dec জোড়া merge)।
দাম ২ — multithreading-এ atomicity
দুই Thread একসাথে একই counter-এ ++ চালালে lost update — মানে premature free, use-after-free। তাই shared object-এর counter atomic হতে হয় (std::atomic<int>, Rust-এর Arc = atomic Rc):
single-threaded INCREF: load; add; store (~1–2 ns)
atomic INCREF: lock cmpxchg / fetch_add (~5–20 ns uncontended)
+ contended হলে cache-line
দুই core-এর মধ্যে ping-pong (~40–100+ ns)Worst case সুন্দর ফাঁদে: একটা hot object যার counter সব thread বারবার ধরে-ছাড়ে (যেমন shared config node) — counter-টার cache line পুরো system-এ ঘুরতে থাকে, আসল data কেউ স্পর্শই করে না। Swift-এর এই কারণে value types (struct) প্রচার করে — class instance-ই শুধু RC-tax দেয়।
Trade-off টেবিল — RC বনাম tracing
| Reference Counting | Tracing GC | |
|---|---|---|
| Reclaim timing | Immediate, deterministic | Deferred, collector-dependent |
| Worst-case pause | ছোট, তবে deep cascade-এ destructor-chain লম্বা হতে পারে | বড় STW pause সম্ভব (modern concurrent-এ sub-ms) |
| Throughput overhead | প্রতি assignment-এ (constant, spread out) | প্রতি collection-এ (batched) |
| Cycle handling | ব্যর্থ — বাড়তি machinery লাগে | Native |
| Root set দরকার? | না — counter-ই সত্যের উৎস | হ্যাঁ — scan শুরুর ঠিকানা |
| Real-time friendliness | ভালো (no surprise batch) | concurrent হলে ভালো |
| Implementation জটিলতা | সহজ (per-op rule) | কঠিন (barrier, pacer, safepoint) |
ভেতরে কী ঘটছে
Cycle — ফাঁদটা ঠিক কোথায় পড়ে
Concrete scenario: parent-child doubly-linked structure। Parent child-কে চেনে, child উল্টো parent-কে চেনে (back-pointer — doubly-linked list, DOM tree, observer registration — সবখানে এটা):
শুরুর অবস্থা (দুটোই root-reachable):
root ──► PARENT(2) ◄──────────┐
│ │ back-pointer
▼ │
CHILD(2) ──────────┘
এখন root-এর reference মুছে গেল (del parent_var):
PARENT-- → PARENT(1) ... আর কিছুই না!
CHILD-এর কাছে এখনো PARENT-এর reference আছে → count ১
PARENT-এর কাছে এখনো CHILD-এর reference আছে → count ১
∞ ────────────────────────
কেউই কখনো ০-তে পৌঁছাবে না। দুটোই unreachable —
কিন্তু counter-দের কাছে দুটোই "live"।Tracing-এ এই জুটির মৃত্যু নিশ্চিত — roots থেকে traversal তাদের ছোঁয়ই না, sweep মুছে দেয়। Counting-এর কাছে graph-এর global view নেই; প্রতিটা object শুধু নিজের local হিসাব জানে। এই ফাঁদ থেকে মুক্তির তিনটা পথ, production তিনটাই ব্যবহার করে:
পথ ১ — Weak reference (manual, explicit)। Non-owning pointer: যে reference count বাড়ায় না। Child-এর back-pointer-টা weak করে দিন — ফাঁদই তৈরি হবে না:
root ──► PARENT(2) ◄┄┄┄┄┄┄┄┄┄┐ ┄┄ = weak (count-এ ভাগ না)
│ ┊
▼ ┊
CHILD(1) ─────────┘
root গেলে: PARENT(0) → মরে → ছাড়ে CHILD → CHILD(0) → মরে ✓Weak reference-এর শর্ত: ব্যবহারের আগে upgrade করতে হয় — “owner এখনো বেঁচে আছে?” Python-এ weakref.ref(), Rust-এ Weak::upgrade() -> Option<Rc<T>>। Option ফেরত পাঠানোর কারণই এই: owner মধ্যপথে মরে যেতে পারে।
পথ ২ — Cycle-collecting fallback (automatic, batched)। CPython-এর মূল বুদ্ধি: RC-কে fast path রেখে, কালেভদ্রে একটা generational tracing pass চালিয়ে শুধু container-object-দের মধ্যে cycle খোঁজা। এই hybrid-এর বিস্তারিত নিচে hood-এ।
পথ ৩ — Acyclic by construction (type-system)। Rust-এ Rc<T> clone করলে count বাড়ে, কিন্তু ভাষাটা আপনাকে বলে দেয় কোনটা owning (Rc<T>) কোনটা non-owning (Weak<T>) — type signature-ই documentation। Cycle এখনো লেখা যায় (Rc↔Rc রাখলেই), তাই এটা prevention না, visibility: ownership graph আপনার types-এ লেখা থাকে, code review-তে ধরা পড়ে।
CPython-এর hybrid — production-এর সবচেয়ে interesting compromise
CPython object lifecycle:
প্রতিটা PyObject: [refcnt][type ptr][data...]
INCREF/DECREF inline macro — প্রায় প্রতিটা op-এ ঘটে (fast path)
BUT: container objects (list/dict/set/class instance) আরেকটা
আলাদা linked list-এ নিবন্ধিত ("tracked")।
threshold cross হলে → gc.collect():
generation-wise traversal শুধু tracked containers-এর মধ্যে —
reachability হিসাব করে "external refs ছাড়া শুধু ভেতরেই
একে অপরকে ধরে আছে" এমন cluster খোঁজে (tentatively unreachable),
finalizer-safe ভাবে মুছে দেয়।
ফল: scalar/int/str — কখনো cycle হয় না — শুধু RC-তেই চলে;
cycle-prone containers — fallback পাহারায়।Experiment section-এ আপনি ঠিক এই দুই mechanism-কে আলাদা করে দেখবেন: sys.getrefcount দিয়ে RC-র pulse, gc.collect() দিয়ে fallback-এর intervention।
- Read attrobj-এর dict lookup → target pointer পাওয়া গেল (target->refcnt এখনো অপরিবর্তিত)
- INCREF targettarget->refcnt += 1 — single-threaded-এ plain increment, shared-এ atomic RMW
- Store to xx-এর পুরনো value save করে নতুন pointer বসানো
- DECREF oldপুরনো target->refcnt -= 1; শূন্য হলে deallocate path — যা আবার তার field-দের DECREF করে (cascade!)
চার ধাপ, প্রতিটা potential cache miss, প্রতিটা store atomic হলে আরও ভারী — একটা নিরীহ assignment-এর hidden price, আর এটাই RC-র পুরো throughput-story এক ছোট্ট sequence-এ।
উদাহরণ
Python-এ দুইটা graph — একটা ফাঁসে, একটা মুক্ত
import sys, weakref, gc
class Node:
def __init__(self, name):
self.name = name
self.other = None
def __repr__(self):
return f"<Node {self.name}>"
# ---- কেস ১: acyclic chain — RC একাই যথেষ্ট ----
a = Node("a")
b = Node("b")
a.other = b # b: refcnt 2 (b var + a.other)
print(sys.getrefcount(b)) # 3 — getrefcount নিজে একটা temp ref যোগ করে!
del a # a.other গেল → b: refcnt 1
print(sys.getrefcount(b)) # 2
# ---- কেস ২: cycle — ফাঁদ ----
p = Node("parent")
c = Node("child")
p.other = c # c: 2
c.other = p # p: 2 — cycle complete
probe = weakref.ref(p) # weak নজরদার — count বাড়ায় না
del p, c # দুই strong reference-ই গেল!
print(probe()) # <Node parent> — এখনো বেঁচে!!! ফাঁদে
print(gc.get_count())
gc.collect() # fallback intervene করল
print(probe()) # None — এবার সত্যিই মরেছেOutput (counts environment-ভেদে সামান্য বদলাতে পারে):
3
2
<Node parent>
(489, 4, 12)
Noneলক্ষ্য করুন probe() প্রথমবার object-টাই ফেরত দিল — দুইটা variable del করা সত্ত্বেও। এই এক লাইনই cycle-এর প্রমাণ: strong reference শূন্যে নামেনি, কারণ দুই object একে অপরকে জীবন দিয়ে যাচ্ছে। gc.collect()-ই শুধুমাত্র fallback tracing pass — এবং সেটা চলার পর probe() None। আর কেস ১-এ কোনো gc.collect() লাগেনি — pure RC কাজ করেছে।
ছবিতে
Refcount lifecycle — তিনটা অবস্থার মানচিত্র
allocate (rc=1)
│
┌──────────────▼──────────────┐
│ │
INCREF (alias) DECREF (alias মরল)
│ │
▼ ▼
rc=2,3,4... ◄────INCREF──── rc=n−1 ────DECREF──► ...
│ │
│ rc == 0?
│ │
│ ▼
│ destructor চলে (__del__, drop)
│ │
│ fields-দের DECREF (cascade!)
│ │
│ memory freed
│ │
└── cycle থাকলে: rc কখনো ০-এ আসে না ──► zombie pair
(fallback gc / weak ref ছাড়া মুক্তি নেই)Weak reference upgrade-এর race-টাও মানচিত্রে রাখুন:
weak ──upgrade──► [owner বেঁচে?]
│yes→ strong ref পাওয়া গেল (rc++, নিশ্চিত ব্যবহারযোগ্য)
└no ─→ None/Nil — caller-কে মোকাবিলা করতে হবে
মজা: upgrade-এর মুহূর্তে rc==0 হলেও একটা সূক্ষ্ম জায়গা আছে —
proper implementation (Rust Weak::upgrade) check-and-inc এক
atomic ধাপে করে, নাহলে "check করলাম বেঁচে আছে → ঠিক তখনই অন্য
thread-এ মরে গেল" race।নিজে চালিয়ে দেখুন
Refcount-এর stethoscope — sys.getrefcount, gc.get_referrers, আর RSS growth
# cycles.py
import gc, sys, weakref
class Node:
def __init__(self, name, cyclic_peer=None):
self.name = name
self.peer = cyclic_peer
def __repr__(self):
return f"<{self.name}>"
def show(tag, ref):
print(f"{tag:<28} weak→ {ref()}")
# ---------- ১. Acyclic: pure RC ----------
n = Node("solo")
w = weakref.ref(n)
del n
show("acyclic after del:", w) # None — RC-ই মেরেছে, gc লাগেনি
# ---------- ২. Cyclic: ফাঁদ ----------
a = Node("A")
b = Node("B")
a.peer, b.peer = b, a # cycle!
wa, wb = weakref.ref(a), weakref.ref(b)
print("refcounts:", sys.getrefcount(a), sys.getrefcount(b)) # 2, 2
del a, b # দুই external ref গেল
show("cyclic after del:", wa) # এখনো <A>! RC ব্যর্থ
show("peer's referrer:", wb)
# কে ধরে আছে? gc.get_referrers — fallback-এর সাথে একই traversal knowledge
refs = gc.get_referrers(wa())
print("referrer types:", [type(r).__name__ for r in refs])
# ---------- ৩. Fallback intervene ----------
collected = gc.collect()
print("gc.collect() returned:", collected) # কতটা object মরল
show("after gc.collect():", wa) # None
# ---------- ৪. gc.disable() + churn = leak দেখুন ----------
import tracemalloc
class Big:
def __init__(self):
self.data = bytearray(1024)
tracemalloc.start()
gc.disable()
for _ in range(50_000):
x = Big(); y = Big()
x.peer, y.peer = y, x # প্রতি iteration-এ একটা cycle
cur, peak = tracemalloc.get_traced_memory()
print(f"gc OFF : current={cur/1e6:.1f} MB")
gc.enable(); gc.collect()
cur2, _ = tracemalloc.get_traced_memory()
print(f"collect: current={cur2/1e6:.1f} MB ← সব ফেরত এল")python3 cycles.pyTypical output:
acyclic after del: weak→ None
refcounts: 2 2
cyclic after del: weak→ <A>
peer's referrer: weak→ <B>
referrer types: ['Node']
gc.collect() returned: 2
after gc.collect(): weak→ None
gc OFF : current=151.8 MB
collect: current=0.0 MB ← সব ফেরত এলপড়ার নিয়ম: লাইন ১-এ acyclic object del-এই মরেছে (pure RC)। লাইন ৪-৫-এ cycle-এর জুটি বেঁচে আছে — আর get_referrers বলছে কার হাতে আটকে (Node peer)। শেষ দুই লাইন সবচেয়ে dramatic: gc.disable() অবস্থায় ৫০ হাজার cycle ~150 MB জমিয়ে ফেলেছে — leak, RC-র জগতের permanent ফাঁদ — আর একটা gc.collect()-এ শূন্যে নেমেছে — fallback tracing-এর সরাসরি intervention।
Cycle-এ থাকা object-দের refcount del-এর পরেও শূন্যে নামে না আর gc.disable() করলে memory বাড়তেই থাকে — অর্থাৎ CPython-এর reclaim আসলে দুই mechanism-এর কাজ, আর cycle-অংশটা pure RC-তে অসম্পূর্ণ।
নিজে বানান
Retain/release নিজ হাতে — আর একটা deliberate cycle leak
- RefCounted struct: refcnt + payload, retain()/release() helper
- release()-এ refcnt==0 হলে free — cascade নিজে থেকেই ঘটবে
- Doubly-linked pair বানাও: next strong, prev strong (ভুল ভার্সন!)
- Root release করে দেখাও কেউই মরে না — ASan/valgrind-এ leak confirm
- prev-কে weak বানাও (retain ছাড়া raw pointer + validity flag) — leak শেষ
/* rc.c — refcounted nodes + deliberate cycle leak + weak fix */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node {
int refcnt;
char name;
struct Node *next; /* strong (retained) */
struct Node *prev; /* WEAK: retained না — non-owning back-ptr */
int prev_alive; /* weak pointer-এর validity flag */
} Node;
static Node *node_new(char name) {
Node *n = calloc(1, sizeof(Node));
n->refcnt = 1; n->name = name;
return n;
}
static void retain(Node *n) { if (n) n->refcnt++; }
static void release(Node *n) {
if (!n || --n->refcnt > 0) return;
/* cascade: strong references ছাড়ি — প্রতিটা নিজের release ডাকবে */
printf("free %c\n", n->name);
if (n->next) {
n->next->prev_alive = 0; /* weak দিককে জানাই — owner মরলে */
release(n->next);
}
free(n);
}
int main(int argc, char **argv) {
int weak_fix = argc > 1 && strcmp(argv[1], "fix") == 0;
/* A ⇄ B: দুই দিকেই strong = CYCLE (buggy version) */
Node *a = node_new('A'); /* refcnt 1 — main-এর ref */
Node *b = node_new('B'); /* refcnt 1 — creation ref এখনই transfer */
a->next = b; /* A owns B (ref transfer, retain লাগে না) */
b->next = a; retain(a); /* B owns A ← ফাঁদ! a: refcnt 2 */
if (weak_fix) { /* fix mode: B→A edge-টা weak করি */
release(a); /* undo B-র retain — a: refcnt ফিরে 1 */
b->next = NULL; /* B আর A-কে own করে না */
b->prev = a; b->prev_alive = 1; /* non-owning জানা মাত্র */
}
printf("releasing root...\n");
release(a); /* বাইরের শেষ reference */
printf("done\n");
return 0;
}gcc -g -fsanitize=address -o rc rc.c
./rc # buggy: cycle
./rc fix # fixed: weak back-pointer
valgrind --leak-check=full ./rc$ ./rc
releasing root...
done ← কোনো "free" লাইনই নেই! (A-র refcnt 2→1, B-কে ধরে আছে;
B আবার A-কে — mutual pinning)
valgrind: definitely lost: 32 bytes in 2 blocks
$ ./rc fix
releasing root...
free A
free B
done ← cascade complete — valgrind: no leaks are possibleFix-mode-এর জাদুটা খেয়াল করুন: B→A retain-টা সরালেই release(a) এ A-র count শূন্যে নামে → A free → A নিজের strong next (B) ছাড়ে → B free → B তার weak prev-কে ছোঁয়ই না (own করে না, শুধু prev_alive=0 করতে পারত)। Cycle ভাঙার মানে extra code না — ownership এক দিকে রাখা।
নিজে বাড়ান:
Node_get_prev(n)লিখুন — weak pointer safe-access: alive না হলে NULL। এটাই manualWeak::upgrade()।- Atomic করুন:
__atomic_add_fetch(&n->refcnt, 1, __ATOMIC_RELAXED)দিয়ে retain/release বানিয়ে দুই-thread stress test চালান — তারপর plain++-এর ভুল সংস্করণে race ধরুন (TSan:-fsanitize=thread)। - একটা তৃতীয় node
Cযোগ করেA→B→C→Atriangle cycle বানান — fix করতে কোন edge-টা weak করবেন এবং কেন (tree-shaped ownership রাখার নিয়মটা মনে করুন)?
বাস্তব সিস্টেমে
Production RC — চারটা রাজ্য
CPython: সবকিছুই refcounted। PyObject header-এ counter, Py_INCREF inline macro। পুরো ভাষার semantic — variable assignment, function call argument passing — এই counters-এর নাচে। Cycle fallback (generational, container-only) 2001 থেকে। Recent CPython version-গুলোতে immortal objects (interned strings, small ints-এর কিছু অংশ) — counter আর নড়ে না, multi-interpreter sharing-এর জন্য।
Swift: ARC ভাষার নিজস্ব স্তরে। Compiler প্রতিটা class-reference-এ retain/release call insert করে — programmer দেখে না। SIL optimizer অনেক জোড়া elide করে (একই scope-এ retain+release = শুরুতেই cancel)। Value types (struct/enum) RC-free — performance-sensitive API-তে struct প্রচারের মূল কারণ। weak/unowned reference delegate patterns-এর মানসিক অভ্যাস (delegate সবসময় weak — retain cycle-এর ক্লাসিক উৎস)।
PHP আর Windows COM: PHP-র zval refcounted (copy-on-write এর উপরে), COM-এর AddRef/Release — API-তে explicitly exposed, কারণ COM binary boundary পার করা object-দের lifetime contract এটাই।
Rust-এর Rc/Arc: opt-in RC। Default ownership zero-cost; shared mutable ownership দরকার হলে Rc<RefCell<T>> (single-thread) বা Arc<Mutex<T>> (shared) — কোন জায়গায় RC-tax দিচ্ছেন তা type signature-এ লেখা। Linux kernel-ও kref দিয়ে একই দর্শন: hot path-এ tracing অসম্ভব, কিন্তু সুশৃঙ্খল counting সম্ভব।
Performance folklore-এর সত্য অংশ: Objective-C/Swift iOS apps-এ autorelease pool drain, Python-এ large cyclic datastructure teardown-এর spike — সব RC-র জীবন্ত বাণিজ্য-ঘাটগুলো। আপনি এখন ওই folklore-এর পেছনের গণিতটা জানেন।
যে ভুলগুলো সবাই করে
“Inc/dec জোড়া ঠিকমতো মিলিয়ে লিখলে RC-তে কোনো leak হয় না।”
Perfect pairing-ও cycle আটকাতে পারে না — ফাঁদটা pairing-bug না, model-bug: দুই object একে অপরকে own করলে কারো count-ই শূন্যে নামার পথ নেই, কোনো ভুল ছাড়াই। এজন্যই solution গঠনগত: weak/non-owning দিক, বা cycle collector। Pairing discipline (RAII guard, defer) অন্য শ্রেণির bug (premature free, double release) ঠেকায়।
“Python-এ object scope থেকে বেরোলেই সাথে সাথে মুক্ত হয় — always।”
CPython-এ সাধারণত হ্যাঁ — refcount শূন্য হলেই instant dealloc, এটা ভাষার প্রায়-guarantee (CPython-specific behavior, language spec না)। কিন্তু exception: cycle-এ থাকা object fallback collector-এর turn-এর অপেক্ষায় থাকে, আর __del__-সহ legacy cases-এ deferral আরও জটিল। Determinism-টা default, absolute না।
“ARC মানে Swift-এ cycle সমস্যা নেই — Apple-এরা সমাধান করে দিয়েছে।”
ARC = Automatic Reference Counting — automatic অংশটা শুধু retain/release insertion। Cycle-এর গণিত একই: mutually-strong references কখনো মরে না। তাই Swift-এ weak var delegate এত standard idiom — ভাষা সমাধান দেয়নি, সরঞ্জাম দিয়েছে (weak/unowned)।
“Atomic refcounting-এর খরচ নগণ্য — modern CPU তো fast।”
Uncontended atomic op ~5–20 ns — একা নিলে সস্তা। কিন্তু RC-তে op-টা প্রতি assignment-এ, আর shared-hot-object-এ cache-line contention-এ 10× খরচ। Deep call-stack জুড়ে প্রতি frame-এ retain/release হলে compound effect মাপা যায় (Swift team নিজেরাই value-type migration এর জন্য প্রচার করে)। “নগণ্য” বলার আগে profile — এটা constant-spread tax, ব্যাচ-এর না।
বুঝেছেন কি না দেখুন
1RC-তে একটা object-এর মৃত্যু ঠিক করে কোন তিনটা ঘটনা-ক্রম? আর tracing GC-তে সেই মৃত্যু কীসের উপর নির্ভর করে?
স্মরণ
RC: (১) শেষ strong reference-এর DECREF → refcnt==0, (২) destructor/finalizer চলে, (৩) object নিজের held references ছেড়ে (cascade) তারপর memory free — সব সাথে সাথে, deterministic।
Tracing: মৃত্যুর সময় ঠিক হয় collector-এর schedule-এ — object dead হওয়া আর reclaim হওয়ার মধ্যে arbitrary gap (next GC cycle)।
2Doubly-linked list: প্রতিটা node next আর prev দুটোকেই strongly retain করলে কী হয় পুরো list delete করার সময়? Fix-টা বলুন আর justify করুন কেন ঠিক ওই দিকটা weak হবে।
যুক্তি
next আর prev দুটোকেই strongly retain করলে কী হয় পুরো list delete করার সময়? Fix-টা বলুন আর justify করুন কেন ঠিক ওই দিকটা weak হবে।প্রথম node release করলে সে তার next ছাড়ে, কিন্তু next-এর prev তাকে ধরে আছে — পুরো list একটা giant cycle, একটাও free হবে না (head-এর external ref গেলেও)।
Fix: prev weak (non-owning)। Justify — ownership tree-এর নিয়ম: list-এর canonical ownership হেড থেকে tail-এর দিকে (next দিক), prev শুধু navigation convenience। Tree-direction রাখলে every object-এর ঠিক একজন owner — cascade সবসময় terminate করে। উল্টোটা করলেও technically চলে, কিন্তু নিয়ম ভাঙলে পরের developer-এর জন্য ফাঁদ।
3sys.getrefcount(x) কেন actual count-এর চেয়ে এক বেশি দেখায়? আর একটা object-এর count যদি 1 থেকে বাড়েই না কোনো প্রোগ্রামে, সে object কি cycle-এ আছে?
প্রয়োগ
sys.getrefcount(x) কেন actual count-এর চেয়ে এক বেশি দেখায়? আর একটা object-এর count যদি 1 থেকে বাড়েই না কোনো প্রোগ্রামে, সে object কি cycle-এ আছে?getrefcount(x) call-এ x একটা temporary argument-reference পায় — সেই temp ref-ই +1; ফেরার সময় ছেড়ে যায়।
Count 1-এ আটকে থাকা মানে cycle না — cycle-এ থাকলে minimum 2 হতো (cycle-partner-এর reference + possibly self)। Count 1 মানে ঠিক একজন holder — সাধারণ একক ownership, healthy state। Cycle detection-এর signal হলো: external refs শেষ হওয়ার পরেও count > 0।
4আপনি একটা document editor লিখছেন: Document → Paragraph → Run hierarchy, plus একটা global StyleCache যেখানে paragraph-রা style lookup করে, আর Style object-রা paragraph-দের জানে কোথায় ব্যবহৃত (usage tracking)। Strong/weak কোথায় বসাবেন — প্রতিটা edge ধরে বলুন, আর কোন edge-ই cycle বানাত?
ডিজাইন
Ownership tree হওয়া উচিত: Document →(strong) Paragraph →(strong) Run। StyleCache →(strong) Style (cache owns its entries — eviction তার একার সিদ্ধান্ত)।
Non-owning edges: Paragraph →(weak) Style (style মরলে paragraph জানবে, miss-এ re-resolve); Style →(weak) Paragraph usage-list (paragraph মরলে style আটকে রাখবে না — নইলে cache কখনো খালি হবে না)।
Cycle danger zone ঠিক Style↔Paragraph: দুই দিকই strong করলে paragraph মুছলেও style তাকে ধরবে, style evict-ও হবে না (paragraph ধরে আছে) — mutual pinning। এক দিক weak = সমাধান; কোন দিক তা policy-তে: cache-ই owner হওয়া natural, কারণ cache নিয়ন্ত্রণ করে lifecycle।
5CPython কেন “pure RC” না হয়ে hybrid বেছে নিয়েছে — fallback collector-কে শুধু container-দের মধ্যে সীমাবদ্ধ রেখে? Pure-tracing-এর চেয়ে এই hybrid-এর লাভ কী?
যুক্তি
Pure RC-র ব্যর্থতা cycle; pure tracing-এর ব্যর্থতা determinism হারানো + সব allocation-এ nursery bookkeeping। Hybrid দুটোর best-of: ভাষার ৯৯% object (scalar, string, tuple-of-scalars) কখনো cycle গড়ে না — তাদের instant RC dealloc-ই যথেষ্ট, কোনো tracing overhead নেই। Cycle-সম্ভব population (container) ছোট আর identifiable — শুধু সেদের মধ্যে batched traversal চালালে fallback-এর খরচও ছোট, আর ঘন ঘন লাগে না।
লাভ tracing-over: average-case latency deterministic (instant reclaim), implementation সরল থাকে (কোনো write barrier সব pointer-store-এ না), আর small-object-heavy workload-এ footprint কম (garbage জমে না)। এজন্যই এই design ২০+ বছর ধরে টিকে আছে।
এরপর কী
এরপর কী
তিনটা লেসন ধরে আমরা দেখলাম memory কে manage করে — programmer, compiler, proof, collector, counter। কিন্তু একটা বড় প্রশ্ন এখনো ঝুলে আছে: আপনার main() চলার আগে কে malloc-এর arena-টা সাজায়? Collector-টা কোথা থেকে spawn হয়? Go-র goroutine scheduler, JVM-এর JIT, CPython-এর bytecode interpreter — এরা কি process-এর ভেতরেই থাকে? উত্তর: হ্যাঁ — runtime system, সেই অদৃশ্য স্তর যা execve থেকে _start হয়ে __libc_start_main পর্যন্ত আপনার কোডের আগে দৌড়ে মঞ্চ সাজায়, আর main return করার পরেও exit-handlers চালায়। C-এর “কোনো runtime নেই” কথাটা কতটা মিথ্যা, Go/JVM/CPython-এর runtime-রা ঠিক OS-এর কোন কাজগুলো নিজের ভেতরে নিয়ে নেয় — আর নিজের হাতে -nostdlib দিয়ে bare-metal hello world বানিয়ে syscall স্তরে নামা — সবই পরের লেসনে।
পরের লেসন: Runtime System — main() এর আগে আর পরে কী চলে।
আরও পড়ুন
- The Garbage Collection Handbook: The Art of Automatic Memory Management, 2nd ed., chapter 4 — Richard Jones, Antony Hosking, Eliot Moss · RC family-র সম্পূর্ণ taxonomy — deferred, coalescing, cyclic RC সহ
- A Method of Overlapping and Erasure of Lists — George E. Collins · CACM 1960 — reference counting-এর জন্মপত্র, mark-sweep-এর McCarthy paper-এর সমসাময়িক
- Concurrent Cycle Collection in Reference Counted Systems — David F. Bacon, V. T. Rajan · ESOP/FLOPS 2001 — cycle collection without global pauses, Swift-era design-দের পূর্বসূরি
- CPython source: Modules/gcmodule.c ও Include/refcount.h — Python Software Foundation · Production hybrid — refcount primary + generational cycle collector fallback; এই লেসনের experiment এটাই probe করে