Вопрос об эффективном перераспределении памяти

Допустим, у меня есть программа (например, C++), которая выделяет несколько объектов, никогда не превышающих заданный размер (назовем ее MAX_OBJECT_SIZE).

У меня также есть регион (я назову его «страницей») в куче (выделенный, скажем, malloc(REGION_SIZE), где REGION_SIZE >= MAX_OBJECT_SIZE).
Я продолжаю резервировать место на этой странице до тех пор, пока не будет заполнено пробел равен PAGE_SIZE (или, по крайней мере, > PAGE_SIZE - MAX_OBJECT_SIZE).

Теперь я хочу выделить больше памяти. Очевидно, моей предыдущей "страницы" будет недостаточно. Так что у меня как минимум два варианта:

  1. Используйте realloc(page, NEW_SIZE), где NEW_SIZE > PAGE_SIZE;
  2. Выделите новую «страницу» (page2) и поместите туда новый объект.

Если бы я хотел иметь пользовательскую функцию выделения, то:

  1. Используя первый метод, я бы увидел, сколько я заполнил, а затем поместил туда свой новый объект (и добавил размер объекта к моей заполненной переменной памяти).
  2. Используя второй метод, я бы получил список (вектор? Массив?) страниц, затем искал текущую страницу, а затем использовал метод, аналогичный 1 на выбранной странице.

В конце концов, мне также понадобится метод для освобождения памяти, но я могу понять эту часть.

Итак, мой вопрос: Каков наиболее эффективный способ решения подобной проблемы? Это вариант 1, вариант 2 или какой-то другой вариант, который я здесь не рассмотрел? Требуется ли/достаточен ли небольшой контрольный показатель, чтобы делать выводы для реальных ситуаций? Я понимаю, что разные операции могут выполняться по-разному, но мне нужен общий показатель.


person luiscubal    schedule 29.06.2010    source источник


Ответы (6)


По моему опыту, вариант 2 намного проще в работе, с минимальными накладными расходами. Realloc не гарантирует увеличения размера существующей памяти. А на практике почти никогда не бывает. Если вы используете его, вам нужно будет вернуться и переназначить все старые объекты. Это потребует, чтобы вы помнили, где был каждый выделенный объект... Это может быть тонна накладных расходов.

Но трудно назвать «наиболее эффективным», не зная точно, какие показатели вы используете.

Это менеджер памяти, который я всегда использую. Он работает для всего приложения, а не только для одного объекта.

выделяет:

для каждого выделения определить размер выделенного объекта.

1 просмотрите список ссылок на освобождение объектов такого размера, чтобы увидеть, было ли что-либо освобождено, если да, возьмите первое свободное

2 ищите в справочной таблице и если не найдёте

2.1 выделить массив из N объектов выделенного размера.

3 вернуть следующий свободный объект нужного размера.

3.1 если массив заполнен добавить новую страницу.

N объектов могут быть настроены программистом. Если вы знаете, что у вас есть миллион 16-байтовых объектов, вы можете захотеть, чтобы это значение N было немного больше.

для объектов более некоторого размера X не сохраняйте массив, просто выделяйте новый объект.

освобождает:

определить размер объекта, добавить его в список бесплатных ссылок.

если размер выделенного объекта меньше размера указателя, список ссылок не должен нести какие-либо накладные расходы памяти. просто используйте уже выделенную память для хранения узлов.

Проблема с этим методом заключается в том, что память никогда не возвращается в операционную систему до тех пор, пока приложение не завершит работу или программист не решит дефрагментировать память. дефрагментация это другой пост. это может быть сделано.

person madmik3    schedule 29.06.2010
comment
Взгляните на slab allocator: en.wikipedia.org/wiki/Slab_allocation. - person the_void; 29.06.2010

Из вашего вопроса непонятно, почему вам нужно заранее выделить большой блок памяти, а не выделять память для каждого объекта по мере необходимости. Я предполагаю, что вы используете его как непрерывный массив. В противном случае имело бы смысл malloc выделять память каждого объекта по мере необходимости.

Если он действительно действует как массив, malloc другой блок дает вам еще один кусок памяти, к которому вы должны получить доступ через другой указатель (в вашем случае page2). Таким образом, он больше не находится в непрерывном блоке, и вы не можете использовать два блока как часть одного массива.

realloc, с другой стороны, выделяет один непрерывный блок памяти. Вы можете использовать его как единый массив и выполнять всевозможные арифметические операции с указателями, которые невозможны, если есть отдельные блоки. realloc также полезен, когда вы действительно хотите сжать блок, с которым работаете, но, вероятно, это не то, что вы пытаетесь сделать здесь.

Итак, если вы используете это как массив, realloc в основном лучший вариант. В остальном ничего плохого в malloc нет. На самом деле, вы можете захотеть использовать malloc для каждого объекта, который вы создаете, вместо того, чтобы отслеживать блоки памяти и управлять ими на микроуровне.

person MAK    schedule 29.06.2010
comment
Я предполагаю, что он/она ожидает много маленьких (максимум max_obj_size больших) запросов на выделение, так что иметь страницу и избегать настоящего malloc — неплохая идея. - person ShinTakezou; 29.06.2010

Вы не дали никаких подробностей о том, на какой платформе вы экспериментируете. Например, есть некоторые различия в производительности для realloc между Linux и Windows.

В зависимости от ситуации realloc может потребоваться выделить новый блок памяти, если он не может увеличить текущий и скопировать старую память в новый, который дорогой. Если вам действительно не нужен непрерывный блок памяти, вам следует избегать использования realloc.

Я бы предложил использовать второй подход или использовать собственный распределитель (вы можете реализовать простой распределитель друзей [2]).

Вы также можете использовать более продвинутые распределители памяти, например

person the_void    schedule 29.06.2010
comment
Я использую Linux, но я также ожидаю портировать код для Windows. - person luiscubal; 29.06.2010
comment
Как я уже сказал, было бы проще использовать multiplatform memory allocator. Если вы собираетесь также использовать threads, все станет очень сложным, чтобы управлять им самостоятельно. Выберите APR в качестве проверенного решения (это библиотека, используемая веб-сервером Apache и многими другими) или используйте что-то более новое, специально разработанное для параллельных сред, например tcmalloc или nedmalloc (nedprod.com/programs/portable/nedmalloc). - person the_void; 30.06.2010

В худшем случае вариант 1 может вызвать «перемещение» исходной памяти, что требует дополнительной работы. Если память не перемещается, все равно инициализируется «лишний» размер, что тоже является другой работой. Таким образом, realloc будет "побежден" методом malloc, но чтобы сказать, сколько, вы должны провести тесты (и я думаю, что есть предвзятость в отношении того, как работает система, когда выполняются запросы к памяти).

В зависимости от того, сколько раз вы ожидаете, что realloc/malloc придется выполнять, это может быть полезной или бесполезной идеей. Я бы все равно использовал malloc.

Бесплатная стратегия зависит от реализации. Чтобы освободить все страницы целиком, достаточно их «пройти»; вместо массива я бы использовал связанные «страницы»: добавьте sizeof(void *) к размеру «страницы», и вы можете использовать дополнительные байты для хранения указателя на следующую страницу.

Если вам нужно освободить один объект, расположенный в любом месте на одной из страниц, это становится немного сложнее. Моя идея состоит в том, чтобы сохранить список непоследовательных свободных «блоков»/«слотов» (подходящих для хранения любого объекта). Когда запрашивается новый «блок», сначала вы выбираете значение из этого списка; если он пуст, то вы получаете следующий «слот» на последней используемой странице, и в конечном итоге запускается новая страница. Освобождение объекта означает просто поместить адрес пустого слота в стек/список (в зависимости от того, что вы предпочитаете использовать).

person ShinTakezou    schedule 29.06.2010

В Linux (и, возможно, в других системах POSIX) есть третья возможность, а именно использование области отображения памяти с shm_open. Такой регион инициализируется нулями после того, как вы получите к нему доступ, но страницы AFAIK, к которым вы никогда не обращаетесь, предоставляются бесплатно, если это не просто диапазон адресов в виртуальной памяти, который вы резервируете. Таким образом, вы можете просто зарезервировать большой кусок памяти в начале (больше, чем вам когда-либо понадобится) вашего выполнения, а затем постепенно заполнить его с самого начала.

person Jens Gustedt    schedule 29.06.2010

Каков наиболее эффективный способ решения такой проблемы? Это вариант 1, вариант 2 или какой-то другой вариант, который я здесь не рассмотрел? Нужен/достаточен ли небольшой тест, чтобы делать выводы для реальных ситуаций?

Вариант 1. Для эффективности NEW_SIZE должен нелинейно зависеть от старого размера. В противном случае вы рискуете столкнуться с производительностью O(n^2) realloc() из-за избыточного копирования. Обычно я делаю new_size = old_size + old_size/4 (увеличение на 25%), так как теоретически лучший new_size = old_size*2 может в худшем случае зарезервировать слишком много неиспользуемой памяти.

Вариант 2. Он должен быть более оптимальным, так как большинство современных ОС (благодаря C++ STL) уже хорошо оптимизированы для потока небольших выделений памяти. А у небольших выделений меньше шансов вызвать фрагментацию памяти.

В конце концов, все зависит от того, как часто вы выделяете новые объекты и как вы справляетесь с освобождением. Если вы выделяете много с # 1, у вас будет избыточное копирование при расширении, но освобождение очень просто, поскольку все объекты находятся на одной странице. Если вам нужно будет освободить/повторно использовать объекты, с # 2 вы потратите некоторое время на просмотр списка страниц.

Из моего опыта № 2 лучше, так как перемещение больших блоков памяти может увеличить скорость фрагментации кучи. # 2 также позволяет использовать указатели, поскольку объекты не меняют свое местоположение в памяти (хотя для некоторых приложений я предпочитаю использовать пары pool_id/index вместо необработанных указателей). Если прохождение страниц становится проблемой позже, оно может быть слишком оптимизировано.

В конце концов, вы также должны рассмотреть вариант № 3: libc. Я думаю, что функция malloc() в libc достаточно эффективна для многих задач. Пожалуйста, протестируйте его, прежде чем тратить больше своего времени. Если вы не застряли на каком-то отсталом *NIX, не должно возникнуть проблем с использованием malloc() для каждого маленького объекта. Я использовал пользовательское управление памятью только тогда, когда мне нужно было поместить объекты в экзотические места (например, shm или mmap). Не забывайте и о многопоточности: malloc()/realloc()/free() обычно уже оптимизированы и готовы к MT; вам придется заново реализовать оптимизацию, чтобы избежать постоянных столкновений потоков при управлении памятью. И если вы хотите иметь пулы памяти или зоны, для этого уже есть множество библиотек.

person Dummy00001    schedule 29.06.2010