Короткая версия: всегда используйте calloc()
вместо malloc()+memset()
. В большинстве случаев они будут одинаковыми. В некоторых случаях calloc()
будет выполнять меньше работы, поскольку может полностью пропустить memset()
. В других случаях calloc()
может даже обмануть и не выделять никакой памяти! Однако malloc()+memset()
всегда будет выполнять всю работу.
Понимание этого требует краткого обзора системы памяти.
Быстрый тур по памяти
Здесь четыре основных части: ваша программа, стандартная библиотека, ядро и таблицы страниц. Вы уже знаете свою программу, так что ...
Распределители памяти, такие как malloc()
и calloc()
, в основном используются для того, чтобы занимать небольшие выделения (от 1 байта до 100 КБ) и группировать их в большие пулы памяти. Например, если вы выделите 16 байтов, malloc()
сначала попытается получить 16 байтов из одного из своих пулов, а затем запросит больше памяти у ядра, когда пул начнет работать без нагрузки. Однако, поскольку программа, о которой вы спрашиваете, выделяет сразу большой объем памяти, malloc()
и calloc()
будут просто запрашивать эту память непосредственно из ядра. Порог для этого поведения зависит от вашей системы, но я видел 1 МБ, использованный в качестве порога.
Ядро отвечает за выделение фактической оперативной памяти каждому процессу и следит за тем, чтобы процессы не мешали памяти других процессов. Это называется защита памяти, это было обычным делом с 1990-х годов, и это причина, по которой одна программа может аварийно завершить работу, не разрушая всю систему. Поэтому, когда программе требуется больше памяти, она не может просто взять память, а вместо этого запрашивает память у ядра, используя системный вызов, такой как mmap()
или sbrk()
. Ядро отдает ОЗУ каждому процессу, изменяя таблицу страниц.
Таблица страниц отображает адреса памяти в фактическую физическую память. Адреса вашего процесса, от 0x00000000 до 0xFFFFFFFF в 32-разрядной системе, не являются реальной памятью, а вместо этого являются адресами в виртуальной памяти. Процессор делит эти адреса на страницы по 4 КиБ, и каждая страница может быть назначена другой кусок физической оперативной памяти путем изменения таблицы страниц. Только ядру разрешено изменять таблицу страниц.
Как это не работает
Вот как работает выделение 256 Мбайт не работает:
Ваш процесс вызывает calloc()
и запрашивает 256 МБ.
Стандартная библиотека вызывает mmap()
и запрашивает 256 МБ.
Ядро находит 256 МБ неиспользуемой оперативной памяти и передает ее вашему процессу путем изменения таблицы страниц.
Стандартная библиотека обнуляет ОЗУ с помощью memset()
и возвращает с calloc()
.
Ваш процесс в конечном итоге завершается, и ядро освобождает ОЗУ, чтобы его мог использовать другой процесс.
Как это на самом деле работает
Вышеописанный процесс будет работать, но просто так не происходит. Есть три основных различия.
Когда ваш процесс получает новую память от ядра, эта память, вероятно, использовалась ранее другим процессом. Это угроза безопасности. Что если в этой памяти есть пароли, ключи шифрования или секретные рецепты сальсы? Чтобы предотвратить утечку конфиденциальных данных, ядро всегда очищает память, прежде чем передать ее процессу. Мы могли бы также очистить память, обнуляя ее, и если новая память обнуляется, мы могли бы также сделать это гарантией, поэтому mmap()
гарантирует, что новая память, которую она возвращает, всегда обнуляется.
Существует множество программ, которые выделяют память, но не используют ее сразу. Иногда память выделяется, но никогда не используется. Ядро это знает и ленится. Когда вы выделяете новую память, ядро вообще не касается таблицы страниц и не отдает ОЗУ вашему процессу. Вместо этого он находит некоторое адресное пространство в вашем процессе, записывает, что должно быть там, и дает обещание, что он поместит туда оперативную память, если ваша программа когда-либо действительно ее использует. Когда ваша программа пытается прочитать или записать данные с этих адресов, процессор вызывает сбой страницы , и ядро начинает выделять ОЗУ этим адресам и возобновляет работу вашей программы. Если вы никогда не используете память, сбой страницы никогда не происходит, и ваша программа фактически никогда не получает ОЗУ.
Некоторые процессы выделяют память и затем читают из нее, не изменяя ее. Это означает, что многие страницы в памяти разных процессов могут быть заполнены нетронутыми нулями, возвращаемыми из mmap()
. Поскольку все эти страницы одинаковы, ядро заставляет все эти виртуальные адреса указывать на одну общую 4-килобайтную страницу памяти, заполненную нулями. Если вы попытаетесь записать в эту память, процессор вызовет еще одну страницу сбоя, и ядро войдет, чтобы дать вам новую страницу с нулями, которая не используется другими программами.
Окончательный процесс выглядит примерно так:
Ваш процесс вызывает calloc()
и запрашивает 256 МБ.
Стандартная библиотека вызывает mmap()
и запрашивает 256 МБ.
Ядро находит 256 МБ неиспользуемого адресного пространства, записывает, для чего теперь используется это адресное пространство, и возвращает.
Стандартная библиотека знает, что результат mmap()
всегда заполняется нулями (или будет , когда он действительно получит немного ОЗУ), поэтому он не касается памяти, поэтому нет ошибки страницы, и ОЗУ никогда не отдается вашему процессу.
Ваш процесс в конечном итоге завершается, и ядру не нужно восстанавливать ОЗУ, поскольку оно никогда не выделялось.
Если вы используете memset()
для обнуления страницы, memset()
вызовет сбой страницы, приведет к выделению ОЗУ, а затем обнулит ее, даже если она уже заполнена нулями. Это огромный объем дополнительной работы и объясняет, почему calloc()
быстрее, чем malloc()
и memset()
. Если в конечном итоге использовать память в любом случае, calloc()
все еще быстрее, чем malloc()
и memset()
, но разница не так уж и смешна.
Это не всегда работает
Не все системы имеют выгружаемую виртуальную память, поэтому не все системы могут использовать эти оптимизации. Это относится к очень старым процессорам, таким как 80286, а также к встроенным процессорам, которые слишком малы для сложного блока управления памятью.
Это также не всегда будет работать с меньшими выделениями. При меньшем распределении calloc()
получает память из общего пула, а не направляется напрямую в ядро. В общем случае в общем пуле могут храниться ненужные данные из старой памяти, которая использовалась и освобождалась с помощью free()
, поэтому calloc()
может взять эту память и вызвать memset()
для ее очистки. Общие реализации будут отслеживать, какие части общего пула являются нетронутыми и по-прежнему заполнены нулями, но не все реализации делают это.
рассеивание неправильных ответов
В зависимости от операционной системы, ядро может обнулять или не обнулять память в свободное время, в случае, если вам понадобится немного обнулить память позже. Linux не обнуляет память раньше времени, и Dragonfly BSD недавно также удалила эту функцию из своего ядра . Однако некоторые другие ядра делают нулевую память раньше времени. В любом случае, нулевых страниц в ожидании простоя недостаточно, чтобы объяснить большие различия в производительности.
Функция calloc()
не использует какую-либо специальную версию memset()
с выравниванием по памяти, и это в любом случае не сделает ее намного быстрее. Большинство реализаций memset()
для современных процессоров выглядят примерно так:
function memset(dest, c, len)
// one byte at a time, until the dest is aligned...
while (len > 0 && ((unsigned int)dest & 15))
*dest++ = c
len -= 1
// now write big chunks at a time (processor-specific)...
// block size might not be 16, it's just pseudocode
while (len >= 16)
// some optimized vector code goes here
// glibc uses SSE2 when available
dest += 16
len -= 16
// the end is not aligned, so one byte at a time
while (len > 0)
*dest++ = c
len -= 1
Как видите, memset()
очень быстрый, и вы не получите ничего лучше для больших блоков памяти.
Тот факт, что memset()
обнуляет память, которая уже обнулена, означает, что память обнуляется дважды, но это объясняет только двукратную разницу в производительности. Разница в производительности здесь намного больше (я измерял более трех порядков в моей системе между malloc()+memset()
и calloc()
).
Вечеринка
Вместо 10-кратного цикла напишите программу, которая выделяет память до тех пор, пока malloc()
или calloc()
не возвратит NULL.
Что произойдет, если вы добавите memset()
?