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


256

Известно, что 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],'\0',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сделать?

Ответы:


455

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

  1. Ваш процесс вызывает calloc()и просит 256 МБ.

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

  3. Ядро находит 256 МБ неиспользуемой оперативной памяти и передает ее вашему процессу, изменяя таблицу страниц.

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

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

Как это на самом деле работает

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

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

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

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

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

  1. Ваш процесс вызывает calloc()и просит 256 МБ.

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

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

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

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

Если вы используете 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()?


7
@Dietrich: объяснение виртуальной памяти Дитриха о том, что ОС выделяет одну и ту же страницу с нулевым заполнением много раз для calloc, легко проверить. Просто добавьте цикл, который записывает ненужные данные на каждой выделенной странице памяти (достаточно записи одного байта каждые 500 байтов). Общий результат должен стать намного ближе, так как система будет вынуждена действительно распределять разные страницы в обоих случаях.
Крис

1
@kriss: действительно, хотя на подавляющем большинстве систем достаточно одного байта каждые 4096
Дитрих Эпп

На самом деле, calloc()часто является частью mallocнабора реализации, и поэтому оптимизируется, чтобы не вызывать bzeroпри получении памяти из mmap.
Мирабилось

1
Спасибо за редактирование, это почти то, что я имел в виду. Ранее вы заявляли, что всегда используете calloc вместо malloc + memset. Пожалуйста, укажите 1. по умолчанию для malloc 2. если небольшую часть буфера нужно обнулить, memset эту часть 3. в противном случае используйте calloc. В частности, НЕ используйте malloc + memset для всего размера (используйте для этого calloc) и НЕ по умолчанию вызывайте все, так как это мешает таким вещам, как valgrind и статические анализаторы кода (вся память внезапно инициализируется). Кроме этого я думаю, что это хорошо.
работник месяца

5
Хотя скорость не связана, callocтакже менее подвержена ошибкам. То есть, где large_int * large_intможет произойти переполнение, calloc(large_int, large_int)возвращается NULL, но malloc(large_int * large_int)это неопределенное поведение, так как вы не знаете фактический размер возвращаемого блока памяти.
Дюны

12

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


2
Ты уверен? Какие системы это делают? Я думал, что большинство ОС просто выключают процессор, когда они простаивают, и обнуляют память по требованию для процессов, которые выделяются, как только они записывают в эту память (но не когда они выделяют ее).
Дитрих Эпп

@Dietrich - Не уверен. Я слышал это однажды, и это казалось разумным (и достаточно простым) способом сделать calloc()более эффективным.
Крис Латс

@Pierreten - я не могу найти никакой полезной информации о calloc()специфических оптимизациях, и мне не хочется интерпретировать исходный код libc для OP. Можете ли вы найти что-нибудь, чтобы показать, что эта оптимизация не существует / не работает?
Крис Латс

13
@Dietrich: Предполагается, что FreeBSD обнуляет страницы во время простоя: см. Настройку vm.idlezero_enable.
Зан Рысь

1
@DietrichEpp извините некро, но, например, Windows делает это.
Андреас Грапентин

1

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.