Mempool

Честото заделяне и освобождаване на памет оказва съществено отрицателно влияние върху поведението на приложението. Забавянето на скоростта на изпълнението на приложението се дължи на факта, че стандартните функции и оператори за алокиране на памет са с много широко приложение. Те алокират блокове от памет с произволен размер, работят безпроблемно в еднонишкова и многонишкова среда, обработват ситуации при възникване на грешки и т.н..

    За да покажем колко ефективни биха могли да бъдат Memory Pool алгоритмите ще разгледаме една опростена програма, която извъшва многократно заделяне и освобождаване на памет.

  1.  
  2. #include <iostream>
  3. #include <ctime>
  4.  
  5. using namespace std;
  6.  
  7. class People
  8. {
  9. private:
  10.     char _name[20];
  11.     int  _age;
  12. public:
  13.     People(char *name = "", int age = 0):_age(age)
  14.     {
  15.         strncpy(_name, name, 20);
  16.     }
  17. };
  18.  
  19. int main()
  20. {
  21.     clock_t start, end;
  22.  
  23.     start = clock();
  24.     People *data[1000];
  25.     for(int i=0; i<10000; i++)
  26.     {
  27.         for(int j=0; j<1000; j++)
  28.             data[j] = new People();
  29.         for(int k=0; k<1000; k++)
  30.             delete data[k];
  31.     }
  32.     end = clock();
  33.  
  34.     cout << end - start;
  35.  
  36.     return 0;
  37. }
  38.  
Всяка итерация на главния for цикъл предизвиква заделянето и освобождаването на 1000 People обекта. Цялата програма се изпълнява за 3671 мили секунди.

    Разглеждайки внимателно кода на приложението ние виждаме, че не се нуждаем от цялата гъвкавост на операторите new и delete. Приложението работи само в една нишка и винаги заделя и освобождава обекти People, който са с фиксиран размер. За да направим управлението на паметта ефективно ще създадем статичен метод алокатор в класа People. Този метод ще подържа опашка от свободни обекти People. При пойскване на нов обект той ще изважда обект от опашката, а при неговото освобождаване ще го връща отново в нея. Ако опашката е празна новопойскания обект ще се заделя чрез оператора new.

    За да реализираме опашката ще използваме класа MemoryList със следната дефиниция:
  1.  
  2. class MemoryList
  3. {
  4. public:
  5.     MemoryList *next;
  6. }
При всяко извикване за алокиране на памет ще заделяме памет достатъчна за побирането на един People обект. Указателя към тази памет ще кастнем към тип MemoryList. Блогодарение на този каст ние ще можем да третираме паметта като обект от тип MemoryList, което ще позволи навързването и в списък.
 
Статичния метод alocate на класа People реализира описаното по горе по следния начин:
  1. static People *alocate()
  2. {
  3.     People *obj;
  4.  
  5.     if(list == 0)
  6.     {
  7.           MemoryList *newElem = (MemoryList*) new char[sizeof(People)];
  8.         newElem->next = list;
  9.         list = newElem;
  10.     }
  11.        
  12.     obj = (People*)list;
  13.     list = list->next;
  14.  
  15.     return obj;
  16. }
Първо проверяваме статичния член list на класа People дали има нулева стойност. Ако стойноста е нула това означава, че опашката е изчерпана и трябва да заделим един обект от тип People. Задаляме достатъчно памет
  1. MemoryList *newElem = (MemoryList*) new char[sizeof(People)];
и я добавяме към списъка list.
  1.  
  2. newElem->next = list;
  3. list = newElem;
  4.  
Следва взимането на обект от пашката
  1.  
  2.     obj = (People*)list;
  3.     list = list->next;
и връщането му като резултат от извикването на функцията. Метода е изключително опростен за целите на тази статия. Могат да бъдат направени множество подобрения.

Метода който връща свободните обекти в опашкате се нарича free и е реализиран по следния начин:
  1. static void free(People *obj)
  2. {
  3. MemoryList *elem = (MemoryList*)obj;
  4.     elem->next = list;
  5.     list = elem;
  6. }
Ето как изглежда пълния код на нашата програма работеща с Memory Pool алгоритъм.
  1.  
  2. #include <iostream>
  3. #include <ctime>
  4.  
  5. using namespace std;
  6.  
  7. class MemoryList
  8. {
  9. public:
  10.     MemoryList *next;
  11. };
  12.  
  13. class People
  14. {
  15. private:
  16.     char _name[20];
  17.     int  _age;
  18. public:
  19.     People(char *name = "", int age = 0):_age(age)
  20.     {
  21.         strncpy(_name, name, 20);
  22.     }
  23.  
  24.     void init(char *name = "", int age = 0)
  25.     {
  26.         strncpy(_name, name, 20);
  27.         _age = age;
  28.     }
  29.  
  30. public:
  31.     static MemoryList *list;
  32. public:
  33.     static People *alocate()
  34.     {
  35.         People *obj;
  36.  
  37.         if(list == 0)
  38.         {
  39.             MemoryList *newElem = (MemoryList*) new char[sizeof(People)];
  40.             newElem->next = list;
  41.             list = newElem;
  42.         }
  43.        
  44.         obj = (People*)list;
  45.         list = list->next;
  46.  
  47.         return obj;
  48.     }
  49.  
  50.     static void free(People *obj)
  51.     {
  52.         MemoryList *elem = (MemoryList*)obj;
  53.         elem->next = list;
  54.         list = elem;
  55.     }
  56.  
  57. };
  58.  
  59. MemoryList *People::list = 0;
  60.  
  61. int main()
  62. {
  63.     clock_t start, end;
  64.  
  65.     start = clock();
  66.     People *data[1000];
  67.     for(int i=0; i<10000; i++)
  68.     {
  69.         for(int j=0; j<1000; j++)
  70.         {
  71.             data[j] = People::alocate();
  72.             data[j]->init();
  73.         }
  74.         for(int k=0; k<1000; k++)
  75.             People::free(data[k]);
  76.     }
  77.     end = clock();
  78.  
  79.     cout << double(end - start)/CLOCKS_PER_SEC;
  80.  
  81.     return 0;
  82. }
Новото приложение използващо Memory Pool се изпълнява за 312 мили секунди. Както се вижда от графиката по долу приложението е около 10 пъти по бързо.
 
Цялото това подобрение се дължи на хилядите спестени извиквания към new и delete. Трябва да отбележем, че така реализирания Memory Pool може да бъде използван само в еднонишкови приложения. Създаването на многонишков Memory Pool  изисква доста повече усилия за синхронизация и в повечето случай е много малко по ефективен от стандартната имплевентация на new и delete.
 

Автор: Сергей Георгиев [adviser@cpp-examples.com]