লাইফটাইম ও স্টোরেজ অ্যালোকেশন
এই পাঠে যা শিখবেন
- স্কোপ ও লাইফটাইম-এর নির্ভুল পার্থক্য — কেন এরা প্রায়ই একসাথে ঘটে কিন্তু এক জিনিস নয়
- তিনটি স্টোরেজ অ্যালোকেশন ক্যাটাগরি — স্ট্যাটিক, স্ট্যাক-ডায়নামিক, হিপ-ডায়নামিক
- প্রতিটি ক্যাটাগরির লাইফটাইম প্যাটার্ন ঠিক কীভাবে ভিন্ন তা একটি toy মেমরি মডেলে দেখা
- কীভাবে একটি হিপ-অ্যালোকেটেড অবজেক্ট তার তৈরি-করা ফাংশন-কলকে outlive করতে পারে
১ · স্কোপ বনাম লাইফটাইম — একটি সাধারণ বিভ্রান্তি
লাইফটাইমLifetime / Extentপ্রোগ্রাম এক্সিকিউশনের যে সময়কালে একটি ভ্যারিয়েবলের স্টোরেজ বরাদ্দ ও বৈধ থাকে — একটি ডায়নামিক, রানটাইম প্রপার্টি। (aka extent) হলো প্রোগ্রাম এক্সিকিউশনের সেই সময়কাল যতক্ষণ একটি ভ্যারিয়েবলের স্টোরেজ বরাদ্দ ও বৈধ থাকে। এটি L38-এর স্কোপ থেকে আলাদা একটি ধারণা — এই পার্থক্যটি স্পষ্টভাবে মনে রাখা জরুরি —
- স্কোপ: প্রোগ্রাম টেক্সটে একটি নেম কোথায় দৃশ্যমান/valid — একটি স্ট্যাটিক, কম্পাইল-টাইমে নির্ধারণযোগ্য প্রপার্টি (L38 থেকে সরাসরি reuse)।
- লাইফটাইম: প্রোগ্রাম এক্সিকিউশনে সেই নেমের underlying স্টোরেজ কখন প্রকৃতপক্ষে বিদ্যমান থাকে — একটি ডায়নামিক, রানটাইম প্রপার্টি।
সাধারণত এই দুটো একসাথেই ঘটে (একটি লোকাল ভ্যারিয়েবলের স্কোপ ও লাইফটাইম দুটোই তার ফাংশনের সাথে বাঁধা), কিন্তু এরা genuinely আলাদা হতে পারে — L40-এ আমরা দেখব একটি ক্লোজার কীভাবে একটি ভ্যারিয়েবলের লাইফটাইমকে তার মূল স্কোপের চেয়েও দীর্ঘ করে দিতে পারে।
২ · তিনটি স্টোরেজ অ্যালোকেশন ক্যাটাগরি
স্টোরেজ পুরো প্রোগ্রাম এক্সিকিউশনের জন্য বাঁধা থাকে — যেমন গ্লোবাল ভ্যারিয়েবল, প্রোগ্রাম শুরুতে একবার বরাদ্দ হয়, শুধু প্রোগ্রাম শেষ হলে মুক্ত হয়।
স্টোরেজ বরাদ্দ হয় যখন একটি স্কোপ (সাধারণত একটি ফাংশন কল) প্রবেশ করা হয়, মুক্ত হয় exit করলে — বেশিরভাগ ইম্পারেটিভ ভাষার লোকাল ভ্যারিয়েবলের স্ট্যান্ডার্ড মেকানিজম, সরাসরি call stack-এর push/pop discipline-এর সাথে বাঁধা (M11/L48-এ বিস্তারিত)।
স্টোরেজ explicitly (বা M12/L54-এর গার্বেজ কালেকশন দিয়ে automatically) যেকোনো সময় বরাদ্দ/মুক্ত করা যায়, কোনো নির্দিষ্ট স্কোপের entry/exit-এর সাথে বাঁধা নয় — এমন ডেটার জন্য দরকার যার লাইফটাইম কোনো একটি ফাংশন কলের সাথে মেলে না।
যদি একটি ফাংশন এমন একটি অবজেক্ট তৈরি করে যা সেই ফাংশন রিটার্ন করার পরও বেঁচে থাকতে হবে (যেমন সেটি রিটার্ন করা হচ্ছে, অথবা অন্য কোনো লং-লিভড স্ট্রাকচারে সংরক্ষিত হচ্ছে), তাহলে সেই অবজেক্টটি স্ট্যাক-ডায়নামিক অ্যালোকেশন ব্যবহার করতে পারে না — কারণ স্ট্যাক স্টোরেজ ফাংশন রিটার্ন করার সাথে সাথেই মুক্ত হয়ে যায়। এই সমস্যার সমাধান হলো হিপ-ডায়নামিক অ্যালোকেশন, যার লাইফটাইম কোনো একটি নির্দিষ্ট ফাংশন কলের সাথে বাঁধা নয়।
৩ · কোড: একটি toy মেমরি মডেল
নিচের কোড সেলে আমরা তিনটি আলাদা স্টোরেজ রিজিয়ন সিমুলেট করব — static_memory,
stack_memory (ফাংশন-কল entry/exit-এর সাথে মেলা একটি push/pop লিস্ট), ও heap_memory
(explicit allocate/free সহ একটি ডিকশনারি) — এবং একটি ছোট ইভেন্ট সিকোয়েন্স চালিয়ে প্রতিটি ইভেন্টের পরে
তিনটি রিজিয়নের অবস্থা প্রিন্ট করব।
class MemoryModel:
def __init__(self):
self.static_memory = {} # প্রোগ্রামের পুরো জীবনকাল ধরে বাঁধা
self.stack_memory = [] # প্রতিটি এন্ট্রি একটি ফাংশন-কল ফ্রেম (dict)
self.heap_memory = {} # explicit allocate/free, কোনো একক স্কোপের সাথে বাঁধা নয়
self._next_heap_addr = 0
def static_declare(self, name, value):
self.static_memory[name] = value
def call_enter(self, frame_name, locals_dict):
self.stack_memory.append({"frame": frame_name, "locals": dict(locals_dict)})
def call_exit(self):
return self.stack_memory.pop()
def heap_allocate(self, value):
addr = f"heap@{self._next_heap_addr}"
self._next_heap_addr += 1
self.heap_memory[addr] = value
return addr
def heap_free(self, addr):
del self.heap_memory[addr]
def snapshot(self, label):
print(f"--- {label} ---")
print(f" static : {self.static_memory}")
print(f" stack : {self.stack_memory}")
print(f" heap : {self.heap_memory}\n")
mem = MemoryModel()
# ১. একটি গ্লোবাল ভ্যারিয়েবল -- একবার স্ট্যাটিকালি অ্যালোকেট, প্রোগ্রাম শেষ পর্যন্ত থাকবে
mem.static_declare("request_count", 0)
mem.snapshot("গ্লোবাল ভ্যারিয়েবল স্ট্যাটিকালি ডিক্লেয়ার হলো")
# ২. একটি সাধারণ ফাংশন কল -- এন্ট্রিতে স্ট্যাক-অ্যালোকেট, এক্সিটে মুক্ত
mem.call_enter("compute_total(items)", {"total": 0})
mem.snapshot("compute_total() কল হলো -- লোকাল total স্ট্যাকে পুশ হলো")
mem.call_exit()
mem.snapshot("compute_total() রিটার্ন করলো -- ফ্রেম স্ট্যাক থেকে পপ হলো (total আর নেই)")
# ৩. একটি হিপ-অ্যালোকেশন যা তৈরি-করা ফাংশন-কলকে outlive করে
mem.call_enter("build_report()", {})
report_addr = mem.heap_allocate({"title": "মাসিক রিপোর্ট"})
mem.snapshot(f"build_report() হিপে একটি অবজেক্ট অ্যালোকেট করলো ({report_addr})")
mem.call_exit() # ফাংশন রিটার্ন করলো, কিন্তু হিপ অবজেক্ট এখনো বেঁচে আছে
mem.snapshot("build_report() রিটার্ন করলো -- স্ট্যাক ফ্রেম গেছে, কিন্তু heap অবজেক্ট এখনো আছে")
# ৪. হিপ অবজেক্টটি অনেক পরে, explicitly মুক্ত করা হলো
mem.heap_free(report_addr)
mem.snapshot("রিপোর্ট অবজেক্টটি অবশেষে explicitly free হলো")
build_report() রিটার্ন করার (স্ট্যাক ফ্রেম পপ হওয়ার)
পরেও heap_memory-তে রিপোর্ট অবজেক্টটি বেঁচে থাকে। এটিই স্ট্যাক-ডায়নামিক ও হিপ-ডায়নামিক
অ্যালোকেশনের মূল পার্থক্য প্রমাণ করে — হিপ অবজেক্টের লাইফটাইম কোনো একক ফাংশন-কলের এন্ট্রি/এক্সিটের সাথে
বাঁধা নয়, বরং সম্পূর্ণ স্বাধীন, যতক্ষণ না explicitly heap_free কল হচ্ছে।
স্কোপ বলে একটি নেম কোথায় দৃশ্যমান; লাইফটাইম বলে তার স্টোরেজ কখন বিদ্যমান। তিনটি অ্যালোকেশন ক্যাটাগরি — স্ট্যাটিক, স্ট্যাক-ডায়নামিক, হিপ-ডায়নামিক — তিনটি genuinely ভিন্ন লাইফটাইম প্যাটার্ন তৈরি করে, এবং একটি ভাষা-ডিজাইনারকে ঠিক করতে হয় কোন ডেটা কোন ক্যাটাগরিতে যাবে। পরের পাঠে (L40) আমরা দেখব ক্লোজার কীভাবে এই মডেলটিকে আরও এক ধাপ এগিয়ে নিয়ে যায় — একটি স্ট্যাক-স্কোপড ভ্যারিয়েবলকে effectively হিপের মতো দীর্ঘ লাইফটাইম দিয়ে দেয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি ভ্যারিয়েবলের স্কোপ ও লাইফটাইম একই কেন সবসময় হয় না — একটি concrete উদাহরণ দিয়ে বোঝান কোথায় এরা আলাদা হতে পারে।
উপরের কোডেই একটি উদাহরণ আছে — report_addr-এর দিকে ইশারা করা হিপ অবজেক্টটি
build_report() ফাংশনের ভেতরে তৈরি হয়েছিল, তাই তার "প্রাকৃতিক" স্কোপ ছিল সেই ফাংশনের ভেতরেই।
কিন্তু ফাংশনটি রিটার্ন করার পরও (স্কোপ শেষ হয়ে যাওয়ার পরও) অবজেক্টটির লাইফটাইম চলতে থাকে — কারণ এটি হিপে
অ্যালোকেট করা হয়েছিল, যার লাইফটাইম কোনো স্কোপের সাথে বাঁধা নয়। এটিই সরাসরি প্রমাণ করে স্কোপ ও লাইফটাইম
একই জিনিস নয়।
প্র ০২ কেন স্ট্যাক-ডায়নামিক অ্যালোকেশন সাধারণত হিপ-ডায়নামিক অ্যালোকেশনের চেয়ে দ্রুত হয়?
স্ট্যাক-ডায়নামিক অ্যালোকেশন শুধু স্ট্যাক পয়েন্টার সরিয়ে বরাদ্দ করে এবং একইভাবে সরিয়ে মুক্ত করে — একটি O(1), predictable push/pop অপারেশন, কোনো সার্চ বা বুককিপিং লাগে না। হিপ-ডায়নামিক অ্যালোকেশনে সিস্টেমকে মুক্ত মেমরির একটি উপযুক্ত ব্লক খুঁজে বের করতে হয় (বরাদ্দের সময়), এবং কখন সেটি মুক্ত করা নিরাপদ তা ট্র্যাক করতে হয় (explicit free বা গার্বেজ কালেকশনের মাধ্যমে, M12/L54) — এই অতিরিক্ত বুককিপিং হিপ অ্যালোকেশনকে সাধারণত ধীরগতির করে তোলে।
প্র ০৩ একটি গ্লোবাল ভ্যারিয়েবল (স্ট্যাটিক অ্যালোকেশন) ও একটি হিপ-অ্যালোকেটেড অবজেক্ট — দুটোরই লাইফটাইম "প্রায় সবসময়" দীর্ঘ, তাহলে এদের মধ্যে মূল পার্থক্য কী?
পার্থক্যটি কীভাবে ও কখন লাইফটাইম নির্ধারিত হয় তাতে — স্ট্যাটিক অ্যালোকেশনে ঠিক কতগুলো
ইনস্ট্যান্স তৈরি হবে ও তাদের লাইফটাইম কী হবে তা কম্পাইল টাইমেই সম্পূর্ণ নির্ধারিত (সবসময় ঠিক একটি
ইনস্ট্যান্স, প্রোগ্রামের শুরু থেকে শেষ পর্যন্ত)। হিপ-অ্যালোকেশনে কতগুলো ইনস্ট্যান্স তৈরি হবে ও প্রতিটির
লাইফটাইম কতক্ষণ হবে তা সম্পূর্ণভাবে রানটাইম-এর সিদ্ধান্ত — একই কোড লাইন বহুবার এক্সিকিউট হয়ে বহু
স্বাধীন-লাইফটাইমের অবজেক্ট তৈরি করতে পারে (যেমন একটি লুপে বারবার heap_allocate কল করা)।
অনুশীলন
-
চিন্তা করুন: Python-এ প্রতিটি অবজেক্ট (এমনকি ছোট ইন্টিজারও, বাস্তবায়নের খুঁটিনাটি বাদ দিলে) মূলত হিপে অ্যালোকেট হয় ও রেফারেন্স কাউন্টিং/গার্বেজ কালেকশন দিয়ে ম্যানেজ হয়। এই ডিজাইন সিদ্ধান্তের একটি সুবিধা ও একটি খরচ কী হতে পারে?
সুবিধা: প্রোগ্রামারকে ম্যানুয়ালি মেমরি free করার কথা ভাবতে হয় না — যেকোনো অবজেক্ট প্রয়োজন যতক্ষণ, ঠিক ততক্ষণ বেঁচে থাকে, স্ট্যাক-স্কোপ দ্বারা সীমাবদ্ধ নয় (দীর্ঘ-লাইফটাইম ডেটা স্ট্রাকচার তৈরি করা সহজ)। খরচ: হিপ অ্যালোকেশন ও গার্বেজ কালেকশন/রেফারেন্স-কাউন্টিং বুককিপিং-এর কারণে C-এর স্ট্যাক-অ্যালোকেটেড লোকাল ভ্যারিয়েবলের তুলনায় সাধারণত ধীরগতি ও বেশি মেমরি ওভারহেড হয়।
-
পরীক্ষা করুন: উপরের কোড সেলে
mem.heap_free(report_addr)কলটি সরিয়ে দিয়ে Run চাপুন এবং শেষ স্ন্যাপশটেheap_memory-তে কী থাকে দেখুন।heap_freeকল না করা হলেreport_addrএন্ট্রিটিheap_memoryডিকশনারিতে চিরকাল থেকে যাবে — এটিই একটি বাস্তব "মেমরি লিক"-এর ছোট্ট, সরল একটি রূপক (real-world analogy): হিপ-অ্যালোকেটেড ডেটা যদি কখনো explicitly (বা গার্বেজ-কালেক্টেড ভাষায় automatically) মুক্ত না হয়, তাহলে এটি প্রোগ্রাম চলাকালীন অনির্দিষ্টকালের জন্য মেমরি দখল করে রাখে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠ — ক্লোজার ও লেক্সিক্যাল এনভায়রনমেন্ট — এই লাইফটাইম-বনাম-স্কোপ পার্থক্যেরই সবচেয়ে চমকপ্রদ ব্যবহারিক প্রয়োগ।
- ক্লোজার ও লেক্সিক্যাল এনভায়রনমেন্ট পাঠ ৪০ দেখুন কীভাবে একটি ক্লোজার একটি স্ট্যাক-স্কোপড ভ্যারিয়েবলের লাইফটাইমকে তার ফাংশন রিটার্ন করার পরও টিকিয়ে রাখে।
- Computer Architecture & Digital Logic কোর্স সহোদর কোর্স স্ট্যাক ও হিপ আসলে মেমরির বাস্তব রিজিয়ন — এই কোর্স মেমরি হায়ারার্কি ও অ্যাড্রেসিং হার্ডওয়্যার স্তরে কীভাবে কাজ করে তা শেখায়।