Способы избежать использования malloc? - программирование

Способы избежать использования malloc?

Со всем последним шумом C, я прочитал, что есть несколько способов свести к минимуму использование malloc в C и что это очень хорошая практика. Я когда-либо понятия не имел, когда и как, или что такое такая практика. Таким образом, мой вопрос будет, возможно, некоторые опытные программисты C могли бы привести некоторые примеры, где можно (или нужно) написать что-то без malloc, но что было бы действительно неочевидным способом для программиста-новичка C (и, таким образом, сказал, что новичок просто использует malloc)? Возможно, у вас есть опыт от факторинга malloc во что-то еще.

P.S. некоторые сообщения, которые я читал, ссылались на исходный код Quake 3 и то, как он избегает использования malloc, поэтому, если кто-то знает об этом, было бы интересно узнать, что там делается, поскольку, по крайней мере, для того, чтобы знать, я бы хотел избежать копания в код землетрясения бесцельно. (так как хорошо, если они избегают использования malloc-поиска для malloc, я не буду давать много результатов, я полагаю, также кодовая база, скорее всего, не такая простая, как могут быть отдельные примеры)

4b9b3361

Ответ 1

Я не знаю, как полностью избежать malloc, но вы можете, конечно, уменьшить его.

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

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

Ответ, конечно, состоит в том, чтобы нарисовать эти объекты событий из пула. Если вам нужно, вы можете даже использовать части буфера пула, чтобы сформировать список, чтобы вы могли быстро индексировать память, которая была возвращена в пул. Они обычно известны как свободные списки.

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


Не волнуйся об этих понятиях. Пул не должен быть таким сложным. Рассмотрим это:

int ** matrix = malloc( rows * sizeof(int*) );
for( int i = 0; i < rows; i++ ) {
    matrix[i] = malloc( cols * sizeof(int) );
}

Я все время вижу это, и это мое домашнее животное. Зачем вам это делать, когда вы можете это сделать:

int ** matrix = malloc( rows * sizeof(int*) );
matrix[0] = malloc( rows * cols * sizeof(int) );
for( int i = 1; i < rows; i++ ) {
    matrix[i] = matrix[i-1] + cols;
}

И, конечно, это сводится к этому (остерегайтесь потенциальных проблем выравнивания в вашей первой строке, хотя - я проигнорировал его здесь для ясности)

int ** matrix = malloc( rows * sizeof(int*) + rows * cols * sizeof(int) );
matrix[0] = (int*)matrix + rows;
for( int i = 1; i < rows; i++ ) {
    matrix[i] = matrix[i-1] + cols;
}

Замечательная вещь в этом последнем примере - это то, как легко удалить вашу матрицу =)

free( matrix );

О, и обнуление матрицы так же просто...

memset( matrix[0], 0, rows * cols * sizeof(int) );

Ответ 2

В сценарии, где вам нужны небольшие массивы динамического размера в локальной области, есть alloca(), который выделяется из стека, вам нужно явно освободить память (она освобождается при возврате функции), и массивы переменной длины (VLA):

void meh(int s) {
    float *foo = alloca(s * sizeof(float));
    float frob[s];
} // note: foo and frob are freed upon returning

Ответ 3

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

Ответ 4

Основная причина не использования malloc в некоторых конкретных случаях - это, вероятно, тот факт, что он использует общий подход с одним размером для всех разделов памяти.

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

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

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

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

Здесь все говорят родственники, но вопросы, которые вы должны задать перед выбором пользовательской схемы распределения, следующие:

  • Вам нужно выделить и освободить большое количество объектов с одинаковым размером? (Выделение Slab)

  • Могут ли эти объекты удаляться сразу без накладных расходов на отдельные вызовы? (Пулы памяти)

  • Существует ли логическая группировка отдельно выделенных объектов? (Доступ к кэшу)

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

Ответ 5

Есть несколько причин, чтобы избежать malloc - самый большой из моих соображений заключается в том, что "no malloc, no free" перефразировать Боба Марли... Таким образом, никакая память не течет из "забывания", чтобы позвонить free.

И конечно, вы всегда должны проверять NULL при динамическом распределении памяти. Избегая этого, вы уменьшите количество кода и сложность кода.

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

Другая часть - это, конечно, что вызов malloc может быть значительно дорогим по сравнению с локальными переменными. Это особенно характерно, когда вы нажимаете malloc/free calls в "hot-paths" - части кода, который вызывается очень часто. Кроме того, при использовании malloc на небольших разделах памяти также накладные расходы памяти - накладные расходы из прошлого опыта в Visual Studio составляют около 32 байт "заголовка" и округлены до 16 или 32 байтов - поэтому распределение 1 байта фактически занимает 64 байт. Выделение 17 байтов также займет 64 байта...

Конечно, как и все разработки в области проектирования/разработки программного обеспечения, это не "вы НЕ ДОЛЖНЫ ИСПОЛЬЗОВАТЬ malloc", но "избегайте malloc, если есть простая/подходящая альтернатива". Неправильно использовать все глобальные переменные, которые в несколько раз больше, чем они должны быть, только для того, чтобы избежать malloc, - но в равной мере неправильно вызывать malloc/free для каждого кадра или каждого объекта цикла рисования графики.

Я не смотрел код Quake, но я работал над некоторым кодом в 3DMark 2000 [я думаю, что мое имя все еще находится в титрах продукта]. Это написано на С++, но оно избегает использования new/delete в коде рендеринга. Все это сделано при настройке/отключении фрейма, за очень немногими исключениями.

Ответ 6

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

Вы можете реализовать специализированный распределитель памяти поверх malloc() или независимо от того, что предоставляет ваша ОС, выделить один раз один кусок памяти и затем вырезать его без вызова стандартных функций malloc(). Плюсы: быстро. Минусы: не совсем тривиально реализовать право.

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

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

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