Learn C++ STL

lesson ১ / ৯ · ভেতরের কথা: memory, cache, allocator আর standard-এর guarantee

Module ১৭ · ভেতরের কথা: memory, cache, allocator আর standard-এর guarantee

6 রকম layout, মাপের 2 রকম প্রশ্ন

Freeপড়া

এই lesson-এ যা শিখবে

  • আলাদা করো container object আর তার নিজের রাখা জায়গা।
  • মিলিয়ে দেখো 6-টা layout, GCC-এর রীতিকে সব library-র নিয়ম না ধরে।
  • বাছাই করো কখন পাশাপাশি value, কখন স্থির node দরকার।

Alice একই library record 6 রকম বাক্সে রেখেছে। গুনে সবখানে সমান record পাও। তবু জায়গা আর নতুন record ঢোকানোর খরচ এক না। Maria জানতে চায়, একই value রাখতে এত আলাদা ব্যবস্থা কেন লাগছে।

Object আর নিজের রাখা জায়গা

Object হলো type আর আয়ু আছে এমন একটা value। container object প্রায়ই অন্য জায়গায় রাখা element সামলায়। নিজের রাখা জায়গা বলতে সেই memory বোঝাচ্ছি, যেটা শেষে তাকে ছাড়তে হবে। sizeof শুধু object-এর নিজের মাপ দেয়। pointer ধরে ধরে তার বাইরের memory যোগ করে না।

অনেক record-এর vector আর খালি vector-এর object-এর মাপ সমান হতে পারে। বড় হয় বাইরের element block। খালি deque-ও আগে থেকে কিছু জায়গা নিতে পারে। node-ভিত্তিক container-এ নতুন value এলে আলাদা allocation যোগ হয়। তাই sizeof(container)-কে element সংখ্যা দিয়ে গুণ করে মোট memory ধরো না।

Implementation detail মানে library-র নিজের বেছে নেওয়া রীতি। অন্য library সেটা বদলাতে পারে। Guarantee হলো C++ standard যে আচরণ চায়। এখানে GCC 12-এর libstdc++ ধরে Module 2 থেকে 10-এর ছবিগুলো একসাথে পড়ব। আগের module-এর বাড়ার পরীক্ষাগুলো আবার করার দরকার নেই। মিল আর পার্থক্যটাই এবার কাজের।

পাশাপাশি রেখে layout পড়ো

vector-এর 3-টা সীমানা: শুরু, জীবিত element-এর শেষ, আর বরাদ্দ জায়গার শেষ। প্রথম দুই সীমানা থেকে size বোঝা যায়। শেষ সীমানা বলে আর কতটা বাড়লে জায়গা বদলাবে। GCC-র এই রূপে এগুলো 3-টা pointer। standard element পাশাপাশি রাখার কথা বলে, এই field-গুলো রাখতেই হবে বলে না।

string-এর বেছে নেওয়া সুবিধাটা আলাদা। ছোট লেখা object-এর ভেতরের buffer-এই থাকতে পারে। বড় হলে বাইরের জায়গা লাগে। Module 3-তে এই small-string রীতি দেখেছ। vector-এর সাথে মিলিয়ে দেখো: ছোট string বাইরের allocation এড়াতে পারে, সাধারণ vector-এর element-এর জায়গা বাইরে থাকে।

deque block-এর pointer-এর একটা map রাখে। এখানে map মানে index-এর array, std::map না। এক block-এ কয়েকটা element থাকে। index দিয়ে আগে block, তারপর ঘর পাও। মাথায় বাড়াতে map বদলালেও আগের element সরতে হয় না। পরে দেখবে, এ কারণেই deque-এর iterator-এর নিয়ম আলাদা।

list-এ value আলাদা linked node-এ ছড়ানো। node-এ value আর পাশের node-এর link থাকে। object-এ সীমানা আর হিসাবের অংশ থাকে। নতুন value এলে একটা node যোগ হয়, পুরো element block লম্বা করতে হয় না। কিন্তু নম্বর ধরে মাঝখানে যেতে link বেয়ে হাঁটতে হয়। ঠিকানা স্থির মানেই index সস্তা না।

এই library-তে ordered set আর map tree node ব্যবহার করে। node-এ parent, left, right link আর colour থাকে। গাছের ভারসাম্য ঠিক করতে link বদলায়, value তার node-এই থাকে। তাই ordered খোঁজা আর স্থির element handle একসাথে পাও। standard খরচের সীমা চায়, এই colour-ওয়ালা গাছটাই চাই এমন না।

unordered container-এ bucket-এর array আর linked node দুটোই থাকে। hash দিয়ে bucket বাছা হয়। GCC-তে node-গুলো এক shared chain-এ যুক্ত; bucket entry দিয়ে তার অংশ খুঁজে পাও। Module 10-তে এটা খুলে দেখেছ। rehash-এ bucket-এর ব্যবস্থা বদলায়, element বেঁচে থাকে। তাই element-এর ঠিকানা ঠিক থেকেও হাঁটার iterator বাতিল হতে পারে।

এই layout-গুলো মূলত পাশাপাশি জায়গা, link আর বাড়তি ঘরের মধ্যে খরচ ভাগ করে। vector আগে থেকে কিছু ঘর রেখে একসাথে বড় হয়। list link-এর খরচ দিয়ে node আলাদা রাখে। hash table-এ node আর bucket দুটোই লাগে। string নিজের object একটু বড় রেখে ছোট allocation বাঁচায়। একটা sizeof দিয়ে সবগুলোর দাম বোঝা যাবে না।

record নিজেও বাইরের memory রাখতে পারে। vector<string>-এ string object পাশাপাশি থাকে, কিন্তু বড় লেখার buffer অন্যখানে হতে পারে। পাশাপাশি থাকার কথা element object-এর, তার সব মালিকানার জায়গার না। অনেক লেখা রাখলে দুই স্তরের খরচ ধরো।

allocation-এ চাওয়া byte আর process-এর ধরে রাখা memory-ও এক না। memory manager মাপ গোল করতে পারে, হিসাবের অংশ রাখতে পারে, ছাড়া block জমিয়ে রাখতে পারে। sizeof এগুলো মাপে না। এখানে node-এর type-এর মাপ, process-এর পুরো memory না।

একটা value এলে কী হয় মিলিয়ে দেখো। vector খালি ঘর নেয় বা block সরায়। deque নতুন block নিতে পারে। list node যোগ করে। tree-তে link-ও ঠিক হয়। hash-এ bucket-এর ব্যবস্থা বদলাতে পারে। এই update-গুলো দিয়ে handle-এর আলাদা নিয়ম বোঝা যায়।

Module 2-তে vector বাড়ার হিসাব হয়েছে। Module 4-এ deque-এর block-এর অঙ্ক দেখেছ। আবার একই প্রমাণ লাগবে না। এবার দুইটার ফল মেলাও: দুই layout-এই random access constant, তবু ঠিকানার স্থিরতা আর মাঝের pointer পড়ার কাজ আলাদা। খরচের O চিহ্ন মিললেই গঠন এক না।

array-এর element count type-এর অংশ। move-এ দিয়ে দেওয়ার মতো বাড়তে থাকা বাইরের block নেই। adaptor-এ আবার ভেতরের container-এর storage-এর নিয়ম লাগে। sizeof দেখে সিদ্ধান্তের আগে কোন স্তর element-এর মালিক বোঝো।

শেষে ছবি আর প্রমাণ আলাদা রাখো। ছবি field আর value-এর সম্পর্ক বোঝায়। program এই compiler-এর সত্যিকারের type মাপে। public API-এর নিয়ম বলে app কী ভরসায় করবে। একটা দিয়ে সব বোঝাতে না গিয়ে তিনটা মিলাও।

আগে object মাপো, তারপর বাইরের অংশের নাম বলো

sizeof(vector<int>)  // The manager object, not its element block.
sizeof(int)          // One payload object.
v.capacity()         // Element slots owned by this vector.
উদাহরণ 1: দুই sequence object

এখানে object-এর মাপ আর 3-pointer model-এর মাপ print হবে। private field পড়ছে না। লেখার দৈর্ঘ্য পাল্টালেও string-এর type বদলায় না।

#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
struct Boundaries { int *start,*finish,*limit; };
    vector<int> v(100); string shortText="book",longText(100,'x');
    cout<<"vector-object "<<sizeof(v)<<'\n';
    cout<<"three-pointer-model "<<sizeof(Boundaries)<<'\n';
    cout<<"string-object "<<sizeof(shortText)<<'\n';
    cout<<"long-string-object "<<sizeof(longText)<<'\n';
    cout<<"live-vector-payload "<<v.size()*sizeof(int)<<'\n';
}
vector-object 24
three-pointer-model 24
string-object 32
long-string-object 32
live-vector-payload 400
Run in Compiler
উদাহরণ 2: block আর node

নিচের private node-এর নাম GCC libstdc++-এর। এই lesson-এ দেখার জন্য, নিজের app-এ ব্যবহারের জন্য না। sizeof(node)-এ memory দেওয়ার ব্যবস্থার নিজের খরচ ধরা নেই।

#include <deque>
#include <iostream>
#include <list>
using namespace std;
int main() {
deque<int> blocks; list<int> nodes;
    cout<<"deque-object "<<sizeof(blocks)<<'\n';
    cout<<"list-object "<<sizeof(nodes)<<'\n';
    cout<<"list-node-gcc-only "<<sizeof(std::_List_node<int>)<<'\n';
    cout<<"deque-iterator "<<sizeof(deque<int>::iterator)<<'\n';
    cout<<"int-payload "<<sizeof(int)<<'\n';
}
deque-object 80
list-object 24
list-node-gcc-only 24
deque-iterator 32
int-payload 4
Run in Compiler
উদাহরণ 3: tree আর hash object

GCC 12-এর private node type দিয়ে tree আর hash-এর তুলনাটা দেখা যাবে। map node-এ mapped value-ও থাকে। key-এর type বদলালে padding বা hash রাখার খরচ বদলাতে পারে।

#include <iostream>
#include <map>
#include <set>
#include <unordered_set>
using namespace std;
int main() {
cout<<"set-object "<<sizeof(set<int>)<<'\n';
    cout<<"map-object "<<sizeof(map<int,int>)<<'\n';
    cout<<"tree-node-gcc-only "<<sizeof(std::_Rb_tree_node<int>)<<'\n';
    cout<<"unordered-object "<<sizeof(unordered_set<int>)<<'\n';
    cout<<"hash-node-gcc-only "<<sizeof(std::__detail::_Hash_node<int,false>)<<'\n';
}
set-object 48
map-object 48
tree-node-gcc-only 40
unordered-object 56
hash-node-gcc-only 16
Run in Compiler
vector: GCC 12-তে জায়গার সম্পর্ক start finish end_of_storage v[0] v[1] v[2] uninitialized uninitialized uninitialized [start, finish): T objects [finish, end_of_storage): raw slots
vector: object-এর অংশ আর element-এর জায়গা
string: GCC 12-তে জায়গার সম্পর্ক s.data() s.size() s.local_chars: "book\0" t.data() t.size() t.heap_capacity heap_chars: "xxxxx..."
string: object-এর অংশ আর element-এর জায়গা
deque: GCC 12-তে জায়গার সম্পর্ক map[0]: T* d[0] ... d[B-1] map[1]: T* d[B] ... d[2*B-1] map[2]: T* d[2*B] ...
deque: object-এর অংশ আর element-এর জায়গা
list: GCC 12-তে জায়গার সম্পর্ক sentinel: prev | next prev | value | next prev | value | next prev | value | next
list: object-এর অংশ আর element-এর জায়গা
tree: GCC 12-তে জায়গার সম্পর্ক parent | colour | value left right parent | colour | value parent | colour | value
tree: object-এর অংশ আর element-এর জায়গা
hash: GCC 12-তে জায়গার সম্পর্ক bucket[0] bucket[1] bucket[2]: null before_begin: next key_a | next key_b | next: null
hash: object-এর অংশ আর element-এর জায়গা

তীর দিয়ে সম্পর্ক দেখানো, ঠিকানা বা byte-এর অবস্থান না। deque-এর map-এর entry নিচের নিজের block-টা দেখায়। tree-তে node থেকে child-এর দিকে link যায়। hash-এ bucket shared chain-এর অংশে ঢোকার রাস্তা দেয়। প্রতিটা ছবির সাথে তার মাপার program-টা মিলিয়ে পড়ো।

একই ধাপগুলো table-এ, প্রতি ধাপে একটা row।

ধাপOperationValuesSizeCapacityReallocMovedকী হলো
1push_back111true0প্রথম value রাখার জায়গা তৈরি হলো।
2push_back1 222true1আগের capacity ছাড়ানোয় পুরোনো handle বাতিল।
3push_back1 2 334true2আরও ঘর নিতে আগের element সরল।
4push_back1 2 3 444false0বাকি ঘরেই নতুন value ধরল।
5push_back1 2 3 4 558true4ভরা জায়গার বদলে নতুন block লাগল।
6push_back1 2 3 4 5 668false0এখনকার block-এই ঘর ছিল।
7push_back1 2 3 4 5 6 778false0এবারও বাড়তি ঘর কাজে লাগল।

প্রতিটা ধাপে capacity vector থেকেই পড়া। moved-এ জায়গা বদলালে আগের কয়টা int সরাতে হলো সেটা আছে। processor-এর instruction গোনা হয়নি। এই ধাপগুলো এই library-তে চালানোর ছবি। অন্য library-তে একই গুণে বাড়বে ধরে code লিখো না।

এটা কোথায় কাজে লাগে

  • GCC libstdc++: vector-এর source-এ জায়গার সীমানা আর element তৈরির কাজ আলাদা।
  • LLVM: container বাছার guide-এ ছোট জায়গা object-এর ভেতরে রাখার সুবিধা আছে।

যে ভুলগুলো সবাই করে

1. সব container-এ data() খোঁজা

#include <list>
using namespace std;
int main() {
list<int> a{2,5}; auto p=a.data(); (void)p;
}

GCC 12 এখানে থামে: error: 'class std::__cxx11::list<int>' has no member named 'data' linked node-এর জন্য একটানা element block নেই। C interface-এ পরপর int দিতে হলে vector রাখো। আগের sequence-এ data কাজ করেছিল বলে এখানেও লিখে ফেলতে পারো।

2. গুচ্ছ bit-কে bool& ধরা

#include <vector>
using namespace std;
int main() {
vector<bool> flags(3); bool& first=flags[0]; (void)first;
}

GCC 12 এখানে থামে: error: cannot bind non-const lvalue reference of type 'bool&' to an rvalue of type 'bool' vector<bool>-এ proxy থাকে, মানে bit-এর হয়ে কাজ করা helper। প্রতি bit-এর জন্য আলাদা bool object দিতে পারে না। সত্যিকারের element reference চাইলে byte-ভিত্তিক storage নাও।

মাথা খাটাও

Object সমান মাপের হলেই খরচ সমান?

এক library-তে vector আর list object-এর মাপ সমান এলো। দশ লাখ int রাখলেও কি একই জায়গা লাগবে?

ইঙ্গিত: object, value, link আর বাড়তি ঘর আলাদা করো।

অনুশীলন 1: সিল করা সারি দেখোসহজ

Zara n-টা নমুনা এক সারিতে রেখে সিল করে। এরপর সারি বদলাবে না। শুরুতে x = seed। t নম্বর দেখার আগে x = (37*x+11) modulo n করো। ওই অবস্থানের digit-কে t দিয়ে গুণ করে মোটে যোগ করো। অবস্থান 0 থেকে, দেখার নম্বর 1 থেকে।

Input. আগে n q seed পড়ো। এরপর n-টা decimal digit-এর string। মোট q বার দেখতে হবে।

Output. হিসাবের মোট print করো।

Constraints. 1 <= n <= 200000; 1 <= q <= 200000; 0 <= seed < n; 0 <= digit <= 9.

Sample.

4 3 0
2581
32
#include <iostream>
#include <string>
using namespace std;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
int n,q; long long seed; string digits; cin>>n>>q>>seed>>digits;
    // Follow the inspection schedule and accumulate its weighted digits.
}
Run in Compiler

sealed-row-sampling নামে grade হয়।

অনুশীলন 2: জীবিত payload আর reserve-এর জায়গা আলাদামাঝারি

Alice নির্দিষ্ট মাপের record-এর vector রাখবে। Input: s c b, জীবিত record, capacity আর প্রতি record-এর byte। Output: জীবিত payload আর reserve করা payload-এর byte। Constraints: 0 <= s <= c <= 1000000; 1 <= b <= 1000000। object আর allocation-এর বাড়তি খরচ ধরবে না।

নমুনা

3 8 12
#include <iostream>
using namespace std;
int main() {
long long s,c,b; cin>>s>>c>>b;
    // Print the two payload budgets.
}

Sample-এর output.

36 96
Run in Compiler
অনুশীলন 3: কম খরচের model বাছোকঠিন

Maria দেওয়া model মিলিয়ে দেখবে। Input: n c b p, node সংখ্যা, vector capacity, payload-এর byte আর pointer-এর byte। linked record-এ b+2*p, vector-এ c*b byte লাগবে। Output: কম জায়গার জন্য VECTOR, LIST বা TIE। Constraints: 0 <= n <= c <= 1000000; 1 <= b,p <= 1024। এই model-এ padding আর object-এর মাপ বাদ।

নমুনা

5 8 4 8
#include <iostream>
using namespace std;
int main() {
long long n,c,b,p; cin>>n>>c>>b>>p;
    // Compare the two stated memory models.
}

Sample-এর output.

VECTOR
Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • ছোট object হলেই কি ভালো?

    না। ছোট object বাইরেও অনেক allocation রাখতে পারে। কাজের সময় মোট জীবিত জায়গা আর allocation-এর আচরণ মিলিয়ে দেখো।

  • সব library-তে কি এই field থাকে?

    না। ছবি GCC 12 libstdc++ ধরে। অন্যখানে code চালাতে public member আর standard-এর নিয়মে ভরসা করো।

  • Padding কি অকারণ জায়গা নষ্ট?

    Padding দিয়ে type-এর দরকারমতো alignment রাখা হয়। field-এর সাজানো আর machine অনুযায়ী খরচ বদলায়। শুধু payload গুনলে এটা বাদ পড়ে।

  • স্থির মানেই কি sorted?

    না। ঠিকানা স্থির হওয়া object-এর অবস্থানের কথা। sorted হওয়া value-এর ক্রমের কথা। list-এ একটা থাকতে পারে, অন্যটা না-ও থাকতে পারে।

মূল কথা

  • আলাদা করে মাপো object আর তার নিজের রাখা জায়গা।
  • GCC-এর layout দিয়ে বড় সিদ্ধান্তগুলোর মিল ধরো।
  • link, bucket, বাড়তি capacity আর padding-এর খরচ রাখো।
  • পাশাপাশি থাকা আর element-এর পরিচয় স্থির থাকা আলাদা বোঝো।
  • আরও গভীরে যেতে চাইলে: Allocator, একটা pool আর vector ফেরত দেওয়া (Pro)।

এরপর কাজের খরচের নিয়ম আর average-এর আড়ালের বড় খরচ দেখবে।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: প্রতি কাজের খরচ, সাথে তার শর্ত