Почему malloc + memset медленнее, чем calloc?

известно, что calloc отличается от malloc в том, что он инициализирует память, выделенную. С calloc память обнуляется. С malloc память не очищается.

поэтому в повседневной работе я считаю calloc as malloc+memset. Кстати, для удовольствия я написал следующий код для бенчмарка.

результат сбивает с толку.

код 1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

вывод кода 1:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s  

код 2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'',BLOCK_SIZE);
                i++;
        }
}

вывод кода 2:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s  

замена memset С bzero(buf[i],BLOCK_SIZE) в коде 2 дает тот же результат.

мой вопрос: почему malloc+memset гораздо медленнее, чем calloc? Как calloc что делать?

3 ответов


короткая версия: всегда используйте calloc() вместо malloc()+memset(). В большинстве случаев они будут одинаковыми. В некоторых случаях calloc() будет делать меньше работы, потому что он может пропустить memset() полностью. В других случаях calloc() может даже обмануть и не выделять память! Однако,malloc()+memset() всегда будет делать полный объем работы.

понимание этого требует короткого тура по системе памяти.

быстрый тур памяти

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

распределители памяти, такие как malloc() и calloc() в основном там, чтобы взять небольшие выделения (от 1 байта до 100 КБ) и сгруппировать их в большие пулы памяти. Например, если выделить 16 байт malloc() сначала попытается получить 16 байт из одного из своих пулов, а затем попросит больше памяти у ядра, когда пул закончится. Однако, так как программа вы спрашивать о выделении для большого объема памяти сразу,malloc() и calloc() просто попросит эту память непосредственно из ядра. Порог для этого поведения зависит от вашей системы, но я видел 1 MiB, используемый в качестве порога.

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

таблица страниц сопоставляет адреса памяти с фактическим физическим ОЗУ. Адреса вашего процесса, 0x00000000-0xFFFFFFFF в 32-разрядной системе, не являются реальная память, но вместо этого адреса в виртуальная память. процессор разделяет эти адреса на 4 страницы KiB, и каждая страница может быть назначена другой части физической оперативной памяти путем изменения таблицы страниц. Изменять таблицу страниц разрешено только ядру.

как это не работает

вот как выделение 256 MiB делает не работы:

  1. ваш процесс называет calloc() и просит 256 база управляющей информации.

  2. стандартная библиотека вызывает mmap() и просит 256 MiB.

  3. ядро находит 256 MiB неиспользуемого ОЗУ и передает его вашему процессу, изменяя таблицу страниц.

  4. стандартная библиотека обнуляет ОЗУ с memset() и обратно из calloc().

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

как это работает

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

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

  • есть много программ, которые выделяют память, но не используют память сразу. Иногда память выделяется, но никогда не используется. Ядро знает это и лениво. При выделении новой памяти ядро не касается таблицы страниц вообще и не дает никакого ОЗУ для вашего процесса. Вместо этого он находит некоторое адресное пространство в вашем процессе, отмечает, что должно туда идти, и обещает, что он поместит ОЗУ, если ваша программа когда-либо его использует. Когда ваша программа пытается читать или писать с этих адресов, процессор запускает ошибка страницы и шаги ядра в назначают ОЗУ этим адресам и возобновляют вашу программу. Если вы никогда не используете память, ошибка страницы никогда не происходит, и ваша программа никогда не получает ОЗУ.

  • некоторые процессы выделяют память, а затем читают из нее без изменения. Это означает, что многие страницы в памяти разных процессов могут быть заполнены нетронутыми нулями, возвращенными из mmap(). Поскольку все эти страницы одинаковы, ядро делает все эти виртуальные адреса точкой одной общей страницы 4 Кб памяти, заполненной нулями. Если вы попытаетесь записать в эту память, процессор запускает еще одну ошибку страницы, и ядро делает шаг, чтобы дать вам новую страницу нулей, которая не совместно с другими программами.

окончательный процесс выглядит примерно так:

  1. ваш процесс называет calloc() и запрашивает 256 MiB.

  2. стандартная библиотека вызывает mmap() и просит 256 MiB.

  3. ядро находит 256 MiB неиспользуемого адрес космос, делает заметку о том, для чего теперь используется это адресное пространство, и возвращает.

  4. стандартная библиотека знает, что результат mmap() всегда заполняется нулями (или будет как только он фактически получает некоторую ОЗУ), поэтому он не касается памяти, поэтому нет ошибки страницы, и ОЗУ никогда не дается вашему процессу.

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

если вы используете memset() обнулить страницу,memset() вызовет ошибку страницы, вызовет выделение ОЗУ, а затем обнулит его, Хотя он уже заполнен нулями. Это огромный объем дополнительной работы, и объясняет, почему calloc() быстрее malloc() и memset(). Если в конечном итоге использовать память в любом случае,calloc() еще быстрее, чем malloc() и memset() но разница не совсем так нелепый.


это не всегда работает

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

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

рассеивание некоторых неправильных ответов

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

на 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() обнуление памяти, которая уже обнулена, означает, что память обнуляется дважды, но это объясняет только разницу в производительности 2x. Разница в производительности здесь намного больше (я измерил более трех порядков величины в моей системе между malloc()+memset() и calloc()).

фокус

вместо 10 циклов напишите программу, которая выделяет память до malloc() или calloc() возвращает NULL.

что произойдет, если добавить memset()?


потому что на многих системах, в свободное время обработки, ОС идет вокруг установки свободной памяти на ноль самостоятельно и маркировки его безопасным для calloc(), поэтому, когда вы называете calloc(), возможно, у него уже есть свободная, обнуленная память.


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