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



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



так что в повседневной работе я считаю calloc как 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 что делать?

658   3  

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 Мб неиспользуемой оперативной памяти и передает ее вашему процессу, изменяя таблицу страниц.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

The 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 инициализирует память до некоторого обычно ненулевого значения перед ее возвратом, поэтому вторая версия вполне может инициализировать память дважды

Comments

    Ничего не найдено.