Краткая версия: всегда используйте 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()
?