Стоимость распределения статической памяти и распределения динамической памяти в C - программирование

Стоимость распределения статической памяти и распределения динамической памяти в C

Мне очень интересно узнать, какой предпочтительный метод распределения памяти static vs dynamic хорош для производительности (например, время работы), когда вы знаете точное количество объектов/элементов в C на Linux. Стоимость для небольшого количества объектов (небольшой объем памяти), а также для большого количества объектов (огромный объем памяти).

e.g., type A[N] vs type *A = malloc(sizeof(type) * N)

Пожалуйста, дайте мне знать. Спасибо.

Примечание. Мы можем сравнить это и, возможно, знать ответ. Но я хотел бы знать понятия, объясняющие различия в производительности между этими двумя методами распределения.

4b9b3361

Ответ 1

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

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

В локальной области статические распределения выделяются в стеке. Это предполагает просто резервирование фиксированного количества байтов в стеке и происходит в течение постоянного времени на выделение. Размер стека очень ограничен.

Динамическая память должна быть выделена из кучи, и даже в лучшем случае для большинства распределений потребуется время, которое масштабируется более чем линейно с каждым распределением, например, n log n time или что-то еще.

Также практически говоря, динамическое распределение будет во много раз медленнее, чем статическое распределение.

@update: Как было указано в комментариях ниже: распределения стека не являются технически статичными распределениями (но они являются выделениями с синтаксической формой, используемой в вопросе ОП).

Также, говоря о "времени распределения", я рассматриваю общее время для управления памятью (alloc и free).

В некоторых динамических распределениях распределение времени распределения быстрее, чем время освобождения.

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

Динамические распределители должны быстро компрометировать значительную эффективность памяти (например, распределяющие ресурсы при параллельном подключении к следующей мощности блока с двумя размерами, например 33k alloc будут использовать 64k)

Ответ 2

Истинное статическое распределение (глобальные переменные и локальные переменные, помеченные static) приклеиваются к разделам и загружаются вместе с остальной частью раздела во время загрузки (execve) с помощью mmap. Они по сути бесплатны, уже покрываются стоимостью загрузки программы.

Автоматические массивы со статически известными размерами могут быть склеены вместе с другими локальными переменными и распределены путем корректировки указателя стека. Это по существу одно целочисленное вычитание (стек растет вниз) или что-то близкое к 1 нс на современных процессорах.

Массивы переменной длины не могут быть приклеены к другим переменным, поэтому они должны стоить около 1 нс каждый.

Я попытался измерить malloc с разными размерами в однопоточном процессе, и я получил это, что означало бы, что пары malloc+free стоят около 50-100 раз столько же, сколько переменные стека для распределений < 16MiB. После этого он поднимается вверх (32MiB и 64MiB обойдется примерно в сто раз больше, чем распределения <= 16MiB do):

Malloc times

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

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

Ответ 3

Глобальная память обычно выделяется, когда ваша программа (или общий модуль /dll ) загружается и предварительно заполняется ее начальными значениями. Обычно это имеет собственную секцию памяти.

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

Когда используется malloc (в C) или new (С++), память выделяется в куче /free -store. Это любой блок памяти с номером. Когда требуется больше памяти, чем было уже выделено, malloc переходит к ядру, чтобы запросить больше памяти из системы. Обычно malloc будет использовать освобожденную память, уже полученную из системы.

Вызовы в malloc следует считать медленными и их следует избегать для любых критически важных для производительности или в режиме реального времени процедур. Это по двум причинам.

  • Куча должна выделять и освобождать любой размер памяти в любом порядке. Старые алгоритмы, используемые для перемещения списка освобожденных блоков до тех пор, пока не будет найден подходящий размер. Поскольку память может быть сильно фрагментирована, это может быть медленным. Если куча используется в нескольких потоках, необходимо обеспечить некоторую блокировку. Многое было сделано для улучшения этой ситуации, и современные кучи jemalloc и tcmalloc действительно улучшают ситуацию. Однако не рассчитывайте на это.
  • Если требуемая память не может быть выделена из того, что уже управляется распределителем кучи, кустарник-распределитель должен будет запросить ядро ​​для большей памяти. (В unix это делается при системных вызовах mmap или sbrk). Ядру нужно найти некоторые нераспределенные страницы памяти, и они должны сопоставить эти страницы в пространство памяти процессов). Любая форма кэширования выходит из окна). Поэтому ожидайте, что это будет слишком медленно (для компьютера).

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

Ответ 4

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

int data[SOME_NUMBER];

void foo( /* some list of parameters here */ )
{
  static int some_more_data[SOME_OTHER_NUMBER];
  ...
}

Оба data и some_more_data существуют на протяжении всего жизненного цикла программы, но some_more_data видна только в функции foo 1.

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

Очевидным недостатком является то, что ваш исполняемый файл будет иметь больший отпечаток. Еще один недостаток заключается в том, что ваш код не re-entrant; каждый вызов foo работает над одним и тем же блоком памяти при чтении или записи some_more_data. В зависимости от того, что делает ваш код, это может быть или не быть большим делом.

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

void foo( /* some list of parameters here */ )
{
  int some_more_data[SOME_SMALLISH_NUMBER];
  ...
}

В этом случае some_more_data существует только для времени жизни функции foo; каждый раз, когда вы вызываете foo, он автоматически выделяет другой объект some_more_data. Независимо от накладных расходов при отмене памяти это часть накладных расходов на вызов функции в первую очередь (по крайней мере, в моем опыте). Указатель стека по-прежнему настраивается независимо от того, используете ли вы локальные переменные или нет, поэтому использование локальных переменных не замедлит работу. Основная проблема заключается в том, что доступная память для автоматических объектов относительно невелика; для объектов выше определенного размера этот подход просто не сработает.

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


1. Правила видимости применяются во время перевода с исходного на машинный код; они действительно не применяются во время выполнения.