Какой алгоритм распределения памяти лучше всего подходит для приложений с быстродействием и временем, критичными для С++?
Я задаю этот вопрос, чтобы определить, какой алгоритм распределения памяти дает лучшие результаты с критическими для производительности приложениями, такими как игровые движки или встроенные приложения. Результаты фактически зависят от фрагментации памяти и времени-детерминизма запроса памяти.
В текстовых книгах есть несколько алгоритмов (например, распределение памяти Buddy), но есть и другие, такие как TLSF. Таким образом, в отношении доступных алгоритмов выделения памяти, один из которых является самым быстрым и вызывает меньшую фрагментацию. BTW, сборщики мусора не должны быть включены.
Пожалуйста, также обратите внимание, что этот вопрос не касается профилирования, он просто предназначен для определения оптимального алгоритма для данных требований.
Ответы
Ответ 1
Все зависит от приложения. Серверные приложения, которые могут очистить всю память, относящуюся к конкретному запросу в определенные моменты, будут иметь различную схему доступа к памяти, например, видеоигры.
Если бы был один алгоритм распределения памяти, который всегда был лучшим для производительности и фрагментации, не могли ли люди, реализующие malloc
и new
, выбрать этот алгоритм?
В настоящее время обычно лучше предположить, что люди, которые написали вашу операционную систему и библиотеки времени исполнения, не были мертвы мозгами; и если у вас нет необычного шаблона доступа к памяти, не пытайтесь победить их.
Вместо этого попытайтесь уменьшить количество распределений (или перераспределений), которые вы делаете. Например, я часто использую std::vector
, но если я заранее знаю, сколько элементов у него будет, я могу зарезервировать все за один раз. Это намного эффективнее, чем позволить "расти" естественным образом через несколько вызовов push_back()
.
Многие люди, приходящие с языков, где new
просто означает "gimme a object", будут распределять вещи без уважительной причины. Если вам не нужно класть его в кучу, не вызывайте new
.
Что касается фрагментации: это все еще зависит. К сожалению, я не могу найти ссылку сейчас, но я помню сообщение в блоге от кого-то из Microsoft, который работал над серверным приложением на С++, которое пострадало от фрагментации памяти. Команда решила проблему, выделив память из двух регионов. Память для всех запросов будет поступать из области A до тех пор, пока она не будет заполнена (запросы будут освобождать память как обычно). Когда область A заполнена, вся память будет выделена из области B. К тому времени, когда область B была заполнена, область A снова была полностью пустой. Это решило проблему их фрагментации.
Будет ли он решать ваши проблемы? Понятия не имею. Вы работаете над проектом, который обслуживает несколько независимых запросов? Вы работаете над игрой?
Что касается детерминизма: это все еще зависит. Какой у вас срок? Что происходит, когда вы пропустите крайний срок (космонавты, потерянные в космосе, воспроизводящаяся музыка начинает звучать как мусор?)? Есть распределители в реальном времени, но помните: "в реальном времени" означает "дает обещание о соблюдении сроков", не обязательно "быстро".
Я просто наткнулся на сообщение, описывающее различные вещи, которые Facebook сделал для ускорения и сокращения фрагментации в jemalloc. Вы можете найти эту дискуссию интересной.
Ответ 2
Барыш:
Ваш вопрос очень общий, но вот мой ответ/руководство:
Я не знаю об игровых механизмах, но для встроенных и приложений реального времени. Общие цели алгоритма распределения:
1- Ограниченное время выполнения: вы должны заранее знать наихудшее время распределения событий, чтобы вы могли соответственно планировать свои задачи в реальном времени.
2- Быстрое выполнение: ну, чем быстрее, тем лучше,
3 Всегда выделяйте: особенно для критически важных приложений в режиме реального времени все запросы должны быть удовлетворены. Если вы запросите некоторое пространство памяти и получите нулевой указатель: проблема!
4- Сокращение фрагментации: хотя это зависит от используемого алгоритма, как правило, менее фрагментированные распределения обеспечивают лучшую производительность по ряду причин, включая эффекты кеширования.
В большинстве критических систем вам не разрешается динамически выделять любую память для начала. Вы анализируете свои требования и определяете максимальное использование памяти и выделяете большой кусок памяти сразу после запуска вашего приложения. Если вы не можете, то приложение даже не запускается, если оно действительно запускается, во время выполнения не выделяются новые блоки памяти.
Если скорость вызывает беспокойство, я рекомендую следовать аналогичному подходу. Вы можете реализовать пул памяти, который управляет вашей памятью. Пул может инициализировать "достаточный" блок памяти в начале вашего приложения и обслуживать ваши запросы памяти из этого блока. Если вам требуется больше памяти, пул может сделать другое - возможно большое выделение (в ожидании большего количества запросов на память), и ваше приложение может начать использовать эту недавно выделенную память. Существуют различные схемы объединения памяти, а управление этими пулами - еще одна целая тема.
Что касается некоторых примеров: VxWorks RTOS использовала алгоритм распределения первого соответствия, в котором алгоритм анализировал связанный список, чтобы найти достаточно большой свободный блок. В VxWorks 6 они используют алгоритм наилучшего соответствия, где свободное пространство хранится в дереве, а выделение пересекает дерево для достаточно большого свободного блока. Там есть белая бумага под названием "t20", написанная Золтаном Ласло, которую вы можете найти в Гуглинг, которая имеет более подробную информацию.
Возвращаясь к вашему вопросу о скорости/фрагментации: это действительно зависит от вашего приложения. Необходимо рассмотреть следующие вопросы:
-
Собираетесь ли вы делать очень маленькие выделения или относительно большие?
-
Будут ли распределения попадать в пакеты или равномерно распределены по всему приложению?
-
Каково время жизни распределений?
Если вы задаете этот вопрос, потому что вы собираетесь реализовать свой собственный распределитель, вы должны его спроектировать таким образом, чтобы вы могли изменить базовый алгоритм распределения/освобождения, потому что если скорость/фрагментация действительно такова, критически важно в вашем приложении, вам захочется поэкспериментировать с разными распределителями. Если бы я рекомендовал что-то, не зная ни одного из ваших требований, я бы начал с TLSF, так как он имеет хорошие общие характеристики.
Ответ 3
Лучшая практика - использовать все, что вы можете использовать, чтобы сделать то, что было сделано вовремя (в вашем случае - распределитель по умолчанию). Если все это очень сложно - напишите тесты и образцы, которые будут эмулировать части всего этого. Затем запустите тесты производительности и тесты, чтобы найти шейки бутылки (возможно, они не будут иметь ничего общего с распределением памяти:).
С этого момента вы увидите, что именно замедляет ваш код и почему. Только на основе таких точных знаний вы когда-либо можете что-то оптимизировать и выбрать один алгоритм над другим. Без тестов это просто пустая трата времени, так как вы даже не можете измерить, насколько ваша оптимизация ускорит ваше приложение (на самом деле такие "преждевременные" оптимизации могут действительно замедлить ее).
Распределение памяти - очень сложная вещь, и это действительно зависит от многих факторов. Например, такой распределитель прост и прост, но может использоваться только в ограниченном числе ситуаций:
char pool[MAX_MEMORY_REQUIRED_TO_RENDER_FRAME];
char *poolHead = pool;
void *alloc(size_t sz) { char *p = poolHead; poolHead += sz; return p; }
void free() { poolHead = pool; }
Таким образом, "лучший алгоритм никогда не существует".
Ответ 4
Как уже писали другие, для каждого возможного приложения нет "оптимального алгоритма". Уже доказано, что для любого возможного алгоритма вы можете найти последовательность распределения, которая вызовет фрагментацию.
Ниже я пишу несколько советов из моего опыта разработки игр:
Избегайте выделения, если вы можете
Общими практиками в области разработки игр были (и до некоторой степени до сих пор), чтобы решить проблемы с динамическим распределением памяти, избегая выделения памяти, как чума. Чаще всего возможно использовать стек основанную на памяти - даже для динамических массивов вы часто можете получить оценку, которая будет охватывать 99% случаев для вас, и вам нужно выделять только тогда, когда вы находитесь над этой границей. Другим широко используемым подходом является "предварительное распределение": оценивайте, сколько памяти вам потребуется в какой-либо функции или для какого-либо объекта, создайте своего рода небольшую и упрощенную "локальную кучу", которую вы выделяете спереди и выполняете отдельные выделения из этой кучи.
Библиотеки памяти >
Другим вариантом является использование некоторых библиотек выделения памяти - они обычно создаются экспертами в этой области, чтобы соответствовать некоторым специальным требованиям, и если у вас есть аналогичные требования, они могут соответствовать вашим требованиям.
Многопоточность
Существует один частный случай, когда вы обнаружите, что "по умолчанию" распределитель OS/CRT работает плохо, и это многопоточность. Если вы ориентируетесь на Windows, то в настоящее время блокируются как распределенные ресурсы OS, так и CRT, предоставленные Microsoft (в том числе в отличной от этого отличной нижней части фрагмента фрагментации). Если вы хотите выполнить значительную потоковую передачу, вам нужно либо сократить выделение как можно больше, либо использовать некоторые альтернативы. См. Может ли многопоточность ускорить выделение памяти?
Ответ 5
Одно ограничение, о котором еще не упоминалось, является многопоточным: стандартные распределители должны быть реализованы для поддержки нескольких потоков, все одновременное распределение/деаллокация и передача объектов из одного потока в другой, чтобы он освобождался другой темой.
Как вы, возможно, догадались из этого описания, сложной задачей является реализация распределителя, который обрабатывает все это. И это требует больших затрат, поскольку невозможно удовлетворить все эти ограничения без межпоточной связи (= использование атомных переменных и блокировок), что довольно дорого.
Как таковой, если вы можете избежать concurrency в своих распределениях, у вас есть хорошая возможность реализовать свой собственный распределитель, который значительно превзойдет стандартные распределители: я как-то сделал это сам, и он спас меня примерно 250 циклов ЦП на выделение с довольно простым распределителем, который основан на множестве пулов памяти фиксированного размера для небольших объектов, укладывая свободные объекты с интрузивным связанным списком.
Конечно, избегать concurrency, скорее всего, не для вас, но если вы его вообще не используете, использование этого факта может задуматься.