Коротка версія: Завжди використовувати 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 сторінки KiB, і кожна сторінка може бути призначена іншому фрагменту фізичної оперативної пам’яті шляхом зміни таблиці сторінок. Лише ядро може змінювати таблицю сторінок.
Як це не працює
Ось як не працює розподіл 256 Мб :
Ваш процес дзвонить calloc()
і просить 256 Мб.
Стандартна бібліотека дзвонить mmap()
і просить 256 Мб.
Ядро знаходить 256 Мб невикористаної оперативної пам’яті і передає його вашому процесу шляхом зміни таблиці сторінок.
Стандартна бібліотека обнулює RAM з memset()
і повертається з calloc()
.
Зрештою, ваш процес закінчується, і ядро відновлює оперативну пам'ять, щоб його можна було використовувати в іншому процесі.
Як це насправді працює
Вищеописаний процес спрацював би, але це просто не відбувається таким чином. Є три основні відмінності.
Коли ваш процес отримує нову пам'ять з ядра, ця пам’ять, ймовірно, використовувалася іншим процесом раніше. Це ризик для безпеки. Що робити, якщо в цій пам'яті є паролі, ключі шифрування або секретні рецепти сальси? Щоб запобігти витоку чутливих даних, ядро завжди очищає пам’ять, перш ніж передавати їх процесу. Ми можемо також очистити пам'ять, обнуляючи її, і якщо нова пам'ять буде нульовою, ми можемо також зробити її гарантією, тому mmap()
гарантує, що нова пам'ять, яку вона повертає, завжди нульова.
Є багато програм, які виділяють пам'ять, але не використовують пам'ять відразу. Інколи пам'ять виділяється, але ніколи не використовується. Ядро це знає і лінивий. Коли ви виділяєте нову пам'ять, ядро взагалі не торкається таблиці сторінок і не дає ніякої оперативної пам’яті вашому процесу. Натомість він знаходить деякий адресний простір у вашому процесі, записує те, що потрібно туди відправляти, і обіцяє, що він поставить туди оперативну пам’ять, якщо програма коли-небудь насправді використовує її. Коли ваша програма намагається прочитати або записати з цих адрес, процесор запускає помилку на сторінці і виконує дії ядра при призначенні оперативної пам’яті цим адресам і відновить вашу програму. Якщо ви ніколи не використовуєте пам'ять, помилка сторінки ніколи не відбувається, і ваша програма ніколи фактично не отримує оперативну пам'ять.
Деякі процеси виділяють пам'ять, а потім читають з неї, не змінюючи її. Це означає, що багато сторінок пам'яті в різних процесах можуть бути заповнені незайманими нулями, поверненими звідти mmap()
. Оскільки ці сторінки однакові, ядро змушує всі ці віртуальні адреси вказувати єдину спільну пам'ять на 4 KiB, заповнену нулями. Якщо ви спробуєте записати в цю пам'ять, процесор запускає іншу помилку сторінки, і ядро вводить, щоб отримати свіжу сторінку нулів, яка не поділяється з будь-якими іншими програмами.
Заключний процес виглядає приблизно так:
Ваш процес дзвонить calloc()
і просить 256 Мб.
Стандартна бібліотека дзвонить mmap()
і просить 256 Мб.
Ядро знаходить 256 Мб невикористаного адресного простору, робить примітку про те, для чого використовується цей адресний простір, і повертає.
Стандартна бібліотека знає , що результат mmap()
завжди заповнений нулями (або буде , коли він фактично отримує оперативну пам'ять), так що це не стосується пам'яті, так що немає ніякої помилки сторінки, і RAM ніколи не дається до процесу .
Зрештою, ваш процес закінчується, і ядро не потребує повернення оперативної пам’яті, оскільки його ніколи не було виділено.
Якщо ви використаєте 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()
нульова пам'ять, яка вже нульова, означає, що пам'ять отримує нуль двічі, але це лише пояснює різницю продуктивності в 2 рази. Різниця в продуктивності тут набагато більша (я вимірював більше трьох порядків у своїй системі між malloc()+memset()
і calloc()
).
Партія витівки
Замість циклу 10 разів напишіть програму, яка виділяє пам'ять до malloc()
або calloc()
поверне NULL.
Що станеться, якщо додати memset()
?