Управление памятью
Содержание:
Самая главная оптимизация
Важно помнить, что раскладывать поля из результатов запроса по полям представления на стороне бизнес-логики — это круто, но очень часто совершенно не нужно. Если нужно по результату из клиентского API базы данных напрямую сгенерировать JSON, то совершенно не обязательно создавать тьму объектов с кучей объектов в каждом
Пусть даже оптимизированное, это действие лишнее. Просто создай буфер на стеке, сложи результат запроса в виде JSON, XML, YAML или другого ожидаемого клиентом формата и отправь ему.
А вот если некий код ждет от тебя на обработку именно набор данных во внутреннем формате для сложной обработки, то здесь, вне всяких сомнений, нужно генерировать удобное представление. Если же от тебя ждут простого ответа типа bool, означающего, пришло от базы hello или нет, то совершенно излишне будет генерировать структуру , чтобы ответить true.
Не добавляй в алгоритм лишних шагов — это и есть самая главная оптимизация.
Необработанное выделение памяти (без инициализации)
Не используйте это самостоятельно. Это используется внутри новое-выражение (увидеть ниже).
- а также взять размер в байтах и вернуть в случае успеха.
- В случае неудачи, первый бросает исключение , последний возвращается ,
- использование для не замужем тип объекта (а также для выпуска), и за множественный объекты (и для выпуска).
Эти распределения не делайте инициализировать память, и, в частности, они не делайте вызовите конструктор по умолчанию для выделенных объектов. Поэтому ты ДОЛЖЕН инициализировать ВСЕ элементы вручную прежде чем освободить распределение с помощью либо или же ,
ЗаметкаЯ не мог подчеркнуть, что вы НЕ должны использовать это самостоятельно. Если вы должны использовать его, однако, убедитесь, что вы передаете указатель на вместо типизированного указателя при вызове или же на такие распределения (всегда после инициализации вручную). Я лично испытывал ошибки во время выполнения с не POD-типами с некоторыми компиляторами (возможно, моя ошибка).
JSON, XML и все-все-все
К работе с текстовыми протоколами следует подходить как можно аккуратнее. Как показывает предыдущий пример, малейший просчет при создании вспомогательных объектов может убить производительность совершенно безобидным на первый взгляд кодом.
Для каждого из общепринятых протоколов есть целый выводок библиотек. Но следует помнить простые истины.
- Первое и главное. Тебе почти никогда не стоит строить полное дерево для структуры XML/JSON/YAML при ее чтении откуда бы то ни было. Обычно все сводится к извлечению ряда однотипных значений.
- Второе: не гнушайся изобретать велосипед, если задача критична по производительности, а случай у тебя настолько частный, что отказаться от протоптанной тропинки будет верным решением.
- Третье: при сериализации, пожалуйста, постарайся обойтись генерацией в буфер на стеке. Если это невозможно, то пиши в с заблаговременным вызовом reserve. Код никогда не получится эффективным, если ты сначала мусоришь по всей оперативной памяти использованием , а затем еще и собираешь из него строку, дополнительно склеивая то, что можно сразу собрать в результат.
В качестве домашнего задания сравни по производительности генерацию большой текстовой конфигурации в тот же XML с использованием и без него. Оперативная память в виде сыра с кучей дырок фрагментации не располагает к быстродействию.
Контейнеры
В стандартной библиотеке языка C++ есть множество классов, которые облегчают работу с тем или иным видом объектов. И здесь это не реклама std, вы можете самостоятельно реализовать единожды необходимые вам контейнеры, а затем переиспользовать их неограниченное число раз
В первую очередь обратите внимание на следующие классы:
- Linked list — связный список
- Array — динамический/статический массив
- Queue — очередь
- Stack — стек
- Map — ассоциативный контейнер
- Set — множество
Почему я их упоминаю в статье про менеджмент памяти? Потому что они контролируют доступ к объектам внутри себя и исключают возможность memory corruption. Вызывая необходимые методы вы с легкостью сможете добавлять/удалять объекты, получать на них ссылки, итерироваться по ним, не задумываясь о том, где и как они лежат.
Общие идеи
Здесь мне бы хотелось уйти от конкретики к более абстрактным вещам, которые касаются не столько кода, сколько архитектуры и дизайна системы в целом. Ряд наблюдений, мыслей, которые мне удавалось увидеть/прочесть в том или ином источнике литературы за долгое время.
Общее определение операторов new и delete
Оператор new выделяет область памяти и возвращает указатель на ее первую ячейку. Оператор delete освобождает память, выделенную ранее с помощью оператора new.
- указатель = new тип (начальное_значение);
-
delete указатель
-
в случае массива в оператор delete используются скобки []: delete [] указатель
/*
* File:
* Author: darkfire
*
* Created on September 15, 2010, 6:32 PM
*/
#include <iostream>
#include <ctime>
#include <stdlib.h>
using namespace std;
const int SIZE = 7;
int main () {
srand(time(NULL));
int *parr = new int ; // Определили указатель. Выделили память для массива.
int *parr0 = parr; //сохранили для дальнейшего использования адресс массива.
for (int i=0;i<SIZE-1;i++){
*parr=rand()%100;
cout<<*parr<<"\n";
parr++;
}
parr = parr0;
for (int i=0;i<SIZE-1;i++){
cout<<*parr<<"\n";
parr++;
}
parr = parr0;/* если указателю не присвоить адресс первого элемента массива
* оператор делете будет выводить ошибку.
*/
delete [] parr;
}
Многомерные динамические массивы
Многомерный массив в C по своей сути одномерен. Операции new и delete позволяют создавать и удалять динамические массивы, поддерживая при этом иллюзию произвольной размерности. Деятельность по организации динамического массива требует дополнительного внимания, которое окупается важным преимуществом: характеристики массива (операнды операции new) могут не быть константными выражениями. Это позволяет создавать многомерные динамические массивы произвольной конфигурации.
Следующий пример иллюстрирует работу с динамическими массивами.
#include <iostream>
using namespace std;
void main()
{
int i, j;
// Переменные для описания характеристик массивов.
int m1 = 5, m2 = 5;
/*
Организация двумерного динамического массива производится в два этапа.
Сначала создаётся одномерный массив указателей, а затем каждому элементу
этого массива присваивается адрес одномерного массива. Для характеристик
размеров массивов не требуется константных выражений.
*/
int **pArr = new int*;
for (i = 0; i < m1; i++)
pArr = new int;
pArr = 100;
cout << pArr << "\n";
//Последовательное уничтожение двумерного массива…
for (i = 0; i < m1; i++)
delete[]pArr;
delete[]pArr;
}
Алгоритм изменения размера динамического массива
Например, мы хотим увеличить размер уже существующего динамического массива на 1.
- Создаем новый динамический массив с размерность на единицу больше от существующего массива.
- Копируем все данные из старого массива в новый массив.
- Добавляем значение нового элемента массива.
- Удаляем старый массив. (Может понадобиться изменить переменную размерности массива, например если размерность указывалась через глобальную переменную).
- Присваиваем старому указателю адрес нового массива.
Примеры на многомерные динамические массивы
Сначала создаётся одномерный массив указателей, а затем каждому элементу этого массива присваивается адрес одномерного массива. При этом размер (количество элементов) каждого нового массива на единицу меньше размера предыдущего. Включённая в квадратные скобки переменная, которая является операндом операции new, позволяет легко сделать это.
#include <iostream>
using namespace std;
void main()
{
int i, j;
// Переменные для описания характеристик массивов.
int m1 = 5, wm = 5;
int **pXArr = new int*;
for (i = 0; i < m1; i++, wm--)
pXArr = new int;
//Заполнение массива нулями и показ его на экран
for (i = m1 - 1; i >= 0; i--, wm++) {
for (j = 0; j < wm; j++){
pXArr=0;
cout<<pXArr<<"\t";
}
cout<<"\n\n";
}
//Последовательное уничтожение двумерного массива треугольной конфигурации…
for (i = 0; i < m1; i++)
delete[]pXArr;
delete[]pXArr;
}
Организация трехмерного динамического массива.
Создание и уничтожение трёхмерного массива требует дополнительной итерации. Однако здесь также нет ничего принципиально нового.
#include <iostream>
using namespace std;
void main()
{
int i, j;
// Переменные для описания характеристик массивов.
int m1 = 5, m2 = 5, m3 = 2;
// указатель на указатель на указатель :)
int ***ppArr;
// Создание массива
ppArr = new int**;
for (i = 0; i <m1; i++)
ppArr = new int*;
for (i = 0; i < m1; i++)
for (j = 0; j < m2; j++)
ppArr = new int;
ppArr = 750;
cout << ppArr << "\n";
// Удаление в последовательности, обратной созданию
for (i = 0; i < m1; i++)
for (j = 0; j < m2; j++)
delete[]ppArr;
for (i = 0; i < m1; i++)
delete[]ppArr;
delete[] ppArr;
}

Дизайн из ограничений
На GDC компания CD Project Red представила интересный доклад, в котором дизайнер уровней рассказал, как они создавали архитектуру игровых локаций «The Witcher: Blood and Wine» в условиях четких (количественных) ограничений на расход памяти и время подгрузки локаций. Им приходилось выверять количество геометрии, которая будет загружена в памяти, выстраивать здания таким образом, что движок будет способен подгрузить все необходимые данные налету, пока игрок идет из точки А в точку Б.
В одной из статей дизайнер локаций из Naughty Dog также упомянул, что им приходилось при разработке «Uncharted 4: A Thief’s End» оптимизировать мир игры таким образом, что в любой момент времени вся обозримая часть (то, что находиться в кадре) локации могла быть успешно обработана движком игры.
Заключение
В конце хотелось бы отметить, что управление памятью программы, как и управление други ресурсами, выходит далеко за рамки простых менеджеров памяти и структур данных. В рамках игровых движков существует множество аспектов, которые так или иначе связаны с темой этой статьи. Это может быть и загрузка/хранение ресурсов, отслеживание времени жизни игровых объектов, обеспечение безопасности много-поточного доступа и т.д. Со всеми моментами можно и необходимо познакомиться, прочитав специальную литературу по каждой из тем (конечно, если вам это интересно).
Литература и полезные ссылки
- Хотелось бы еще раз упомянуть книгу Джейсона Грегори «Game Engine Architecture». В ней он рассматривает многие аспекты создания игр и игровых движков, включая звук, графику, физику, математику и т.д. Часть моментов, касающихся работы с памятью, также детально освещается в этой книге.
- Custom memory allocators — здесь вы может ознакомиться с деталями реализации аллокаторов, посмотреть исходный код на C++ и замеры. Это отличный пример для тех, кто желает погрузиться в рутину реализации собственных менеджеров памяти.
- Smart pointers — можно ознакомиться с умными указателями, деталями использования и полным набором функциональности.
- Start Pre-allocating And Stop Worrying — еще пара размышлений на тему менеджмента памяти
Умные указатели
Smart pointer — это некоторая разновидность указателей в стандартной библиотеке C++ начиная с С++11 (не говорите про boost, разговор только о стандарте). Представляет собой класс-обертку, который хранит собственно сам указатель, осуществляет операции над ним и содержит дополнительную мета-информацию о том, как и когда освободить память и удалить объект. В этом месте мы немного отходим от непосредственно выделения памяти к скорее отслеживанию времени жизни объекта.
Так что же позволяют делать умные указатели? В самом простом случае это:
- Автоматическое удаление объекта и освобождение памяти
- Контроль доступа (уникальность/раздельность)
- Больший контроль типа
Следующие виды умных указателей являются основными и самыми распространенными:
-
Unique pointer
Позволяет иметь в программе только 1 указатель на объект (уникальность доступа).
Как только unique pointer удаляется, сразу же удаляется используемый объект и освобождается занятая им память. Хорошо подходит для создания указателей на файловые дескрипторы, т.к. чаще всего только 1 участник может читать/писать файл.
Если вы передаете указатель на объект из uniquePtr1 в uniquePtr2, то значение в uniquePtr1 инвалидируется, т.к допускается только 1 владелец объекта. -
Shared pointer
Указатели раздельного доступа с автоматическим подсчетом ссылок (reference counting). Позволяют нескольким участникам ссылаться на один объект, не беспокоясь о том, кто в итоге должен удалить его. Поскольку подсчет ссылок автоматический, то, как только последний ссылающийся указатель удален, объект также будет удален.
Однако такой вид указателя не лишен недостатков. Во-первых, он не способен бороться с циклическими ссылками, когда объекты ссылаются друг на друга. В таком случае вы гарантированно получите утечку памяти. Во-вторых, в много-поточной среде работа с указателями раздельного доступа накладывает свои ограничения на консистентность памяти и раздельность доступа потоками. -
Weak pointer
Слабая ссылка на объект. Позволяет хранить указатель на объект раздельного доступа, но не удерживать его. Что это значит? Вы можете создать объект и сохранить указатель на него в shared pointer. До тех пор, пока последний shared pointer не удален, объект будет оставаться в памяти. Однако, вы можете создать из shared pointer weak pointer. Таким образом, если сильные (shared) указатели остались на объект, то из weak pointer вы можете получить shared pointer. А если нет — то weak pointer инвалидируется, и вы узнаете о том, что объект, на который вы хранили указатель уже удален.
Недостатком как shared, так и weak pointer является необходимость хранить дополнительно meta-data для каждого объекта. Здесь храниться и кол-во ссылок на объект, способ его удаления и т.д. В общем случае, это O(N) overhead по памяти, где N — кол-во создаваемых объектов. Однако такую жертву можно считать допустимой, т.к в проекте на тысячи строк кода вам будет тяжело договориться с другим программистом о том, кто ответственен за удаление того или иного объекта. В противном случае висячие ссылки и утечки памяти гарантированы.
Финальная мысль в этой части: использовать умные указатели необходимо с также некоторой долей осторожности. Например, используя shared pointer, вы неявно соглашаетесь с тем, что ваш объект (на который вы ссылаетесь через этот указатель) будет где-то когда-то кем-то удален
И это может быть не самый удачный момент работы вашей программы. Кроме это вы должны отдавать себе отчет о расходуемой памяти на meta-info и о том, как данные, на которые вы ссылаетесь через умный указатель будут попадать в кэш. Пример:
Пример достаточно вымученный. Однако во многих ситуация мы можем отказаться от использования тяжеловесных умных указателей. Об этом далее.