[Autumn Series] Управление на паметта

Всякакви уроци свързани със системно програмиране.
Autumn Shade
Newbie
Мнения: 13
Регистриран: 27 юли 2019, 10:15
Баланс: Locked

27 юли 2019, 10:35

Съдържание:
  • Какво е allocator?
  • Интерфейсът
  • C++17
  • Защо контейнерите изискват allocator?
  • Какво следва?
Какво е allocator?
Какво е общото между всичките контейнери в стандартната библиотека(STL) ? Те имат параметър от тип Allocator, който по подразбиране е std::allocator. Работата на allocator-а е да управлява живота на неговите елементи. Това означава да заделя и освобождава памет за своите елементи, както и да задава начална стойност и да ги изтрива.

Като цяло, тук пиша за контейнерите на STL, но това включва и std::string. За опростяване, ще използвам терминът контейнер и за двете.

Защо е толкова специален std::allocator?
От една страна, прави разлика между своите елементи, например ако std::allocator задели елементи за std::vector или двойки от std::map.

Код: Трябва да си влязъл в системата, за да можеш да виждаш линковете

template<
    class T,
    class Allocator = std::allocator<T>
> class vector;


template<
    class Key,
    class T,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<std::pair<const Key, T> >
> class map;
От друга страна, allocator-а има нужда от голям брой атрибути, методи и операции, за да си върши работата.

Интерфейсът

Код: Трябва да си влязъл в системата, за да можеш да виждаш линковете

// Атрибути
value_type                               T
pointer                                  T*
const_pointer                            const T*
reference                                T&
const_reference                          const T&
size_type                                std::size_t
difference_type                          std::ptrdiff_t
propagate_on_container_move_assignment   std::true_ty
rebind                                   template< class U > struct rebind { typedef allocator<U> other; };
is_always_equal                          std::true_type

// Методи
constructor
destructor
address
allocate
deallocate
max_size
construct
destroy

// Операции
operator==
operator!=
Накратко, това са най-важните членове в std::allocator<T>.

Вътрешният шаблонен клас rebind(на десети ред) е един от най-важните членове. Благодарение на този шаблонен клас, можете повторите функцията присвояване от тип A към тип B. Сърцето на std::allocator са двата метода наречени allocate(на седемнадесети ред) и deallocate(на осемнадесети ред). Тези два метода управляват паметта, в която обектът е създаден чрез construct(на двадесети ред) и изтрит чрез destroy(двадесет и първи ред). Методът max_size(деветнадесети ред) връща максималният брой от обекти от тип A, за които std::allocate може да задели памет.

Разбира се, можете и директно да използвате std::allocator

Код: Трябва да си влязъл в системата, за да можеш да виждаш линковете

// amxxbg.cpp

#include <memory>
#include <iostream>
#include <string>
 
int main(){
  
  std::cout << std::endl;

  std::allocator<int> intAllocator; 

  std::cout << "intAllocator.max_size(): " << intAllocator.max_size() << std::endl;
  auto intArray = intAllocator.allocate(100);

  std::cout << "intArray[4]: " << intArray[4] << std::endl;
 
  intArray[4] = 2019;

  std::cout << "intArray[4]: " << intArray[4] << std::endl;
 
  intAllocator.deallocate(intArray, 100);

  std::cout << std::endl;
 
  std::allocator<double> doubleAllocator;
  std::cout << "doubleAllocator.max_size(): " << doubleAllocator.max_size() << std::endl;
  
  std::cout << std::endl;

  std::allocator<std::string> stringAllocator;
  std::cout << "stringAllocator.max_size(): " << stringAllocator.max_size() << std::endl;
 
  std::string* myString = stringAllocator.allocate(3); 
 
  stringAllocator.construct(myString, "Hello");
  stringAllocator.construct(myString + 1, "World");
  stringAllocator.construct(myString + 2, "!");
 
  std::cout << myString[0] << " " << myString[1] << " " << myString[2] << std::endl;
 
  stringAllocator.destroy(myString);
  stringAllocator.destroy(myString + 1);
  stringAllocator.destroy(myString + 2);
  stringAllocator.deallocate(myString, 3);
  
  std::cout << std::endl;
  
}
В програмата използвах три allocator-а. Един за int(единадесети ред), един за double(двадесет и шести ред) и един за std::string (тридесет и първи ред). Всеки от тези allocator-и знае максималният брой елементи, които може да задели( четиринадесети, двадесет и седми и тридесет и втори ред).

Нека погледнем allocator-а за int - std::allocator<int> intAllocator (единадесети ред). Чрез intAllocator можете да заделяте памет за масив от цели числа, до сто на брой. Достъпът до петият елемент не е дефиниран, защото първо трябва да му бъде зададена начална стойност. Това се променя на двадесети ред. Благодарение на intAllocator.deallocate(intArray, 100) аз изтривам заделената памет за обекта.

При std::string е малко по-сложно. stringAllocator.construct се извиква от тридесет и шести до тридесет и осми ред включително. Създават се три извиквания към конструктора за std::string. Трите извиквания на stringAllocator.destroy (от четиредесет и втори до четиредесет и четвърти ред включително) правят обратното - извикват се деструкторите. Накрая( тридесет и четвърти ред) паметта на myString се освобождава.

Ето го и изходът от програмата:

Изображение

C++ 17
В C++17 интерфейсът на std::allocator става доста по-улеснен. Доста от членовете му са премахнати.

Код: Трябва да си влязъл в системата, за да можеш да виждаш линковете

// Атрибути
value_type                               T
propagate_on_container_move_assignment   std::true_ty
is_always_equal                          std::true_type

// Методи
constructor
destructor
allocate
deallocate

// Операции
operator==
operator!=
Но отговорът все още липсва.

Защо контейнерите изискват allocator?
Имам всъщност три отговора:
  1. Един контейнер трябва да бъде независим от основния модел на паметта. Например, моделът на Intel за x86 архитектурата използва шест различни варианта: tiny, small, medium, compact, large и huge. Искам изрично да подчертая въпроса, говоря от името на Intel модела, а не от модела на паметта като база за multithreading.
  2. Един контейнер може да разделя заделянето и изтриването на паметта от създаването и изтриването на обектите си. Следователно, извикването на метода std::vector::reserve(n) на един std::vector заделя само толкова памет, за колкото ще му е нужно за тези n на брой обекта. Конструктурът за всеки елемент няма да бъде изпълнен.
  3. Можете да намествате allocator-а на контейнера точно както ви трябва. Следователно, allocator-ите по подразбиране са оптимизирани за не толкова честа употреба и обемност. Реално, C функцията std::malloc ще бъде извикана по подразбиране. Следователно, allocator, който използва предварително заделена памет може да постигне огромна скорост при използване. С allocator-а по подразбиране на контейнера, вие нямате никаква гаранция колко дълго ще отнеме заделянето на паметта. Разбира се, можете да използвате allocator, който сам да си пипнете и настроите, така че да ви дава повечко debug информация.


Какво следва?
Стратегии за извикване на памет

Отговори

Върни се в “Уроци”

  • Информация
  • Кой е на линия

    Потребители, разглеждащи този форум: Няма регистрирани потребители и 0 госта