Тут вже є багато хороших відповідей, які охоплюють багато важливих моментів, тому я просто додам пару питань, яких я не бачив безпосередньо вгорі. Тобто ця відповідь не повинна вважатися вичерпною плюсами і мінусами, а скоріше доповненням до інших відповідей.
mmap здається магією
Прийняття випадку, коли файл уже повністю кешований 1 як базовий рівень 2 , mmap
може здатися схожим на магію :
mmap
потрібно лише 1 системний виклик для (потенційно) відображення всього файлу, після чого більше системних викликів не потрібно.
mmap
не вимагає копіювання даних файлу з ядра в простір користувача.
mmap
дозволяє отримати доступ до файлу "як пам'ять", включаючи обробку його будь-якими сучасними підказками, які ви можете зробити проти пам'яті, такими як автоматична векторизація компілятора, внутрішні символи SIMD , попереднє завантаження, оптимізовані процедури розбору пам'яті, OpenMP тощо.
У випадку, якщо файл уже знаходиться в кеші, перемогти його здається неможливо: ви просто отримуєте доступ до кешу сторінки ядра як пам'ять, і він не може отримати швидше, ніж це.
Ну, може.
mmap насправді не магія, тому що ...
mmap як і раніше працює на сторінці
Основна прихована вартість mmap
vs read(2)
(яка справді є порівнянною системою виклику на ОС для блоків читання ) полягає в тому, що mmap
вам потрібно буде виконати "деяку роботу" для кожної сторінки 4K в просторі користувача, навіть якщо вона може бути прихована механізм помилок сторінки.
Наприклад, типова реалізація, для якої лише mmap
весь файл потребує помилок, тому 100 ГБ / 4 К = 25 мільйонів помилок для зчитування файлу в 100 ГБ. Тепер це будуть незначні помилки , але 25 мільярдів помилок на сторінці все ще не будуть надшвидкими. Вартість незначної помилки, мабуть, у 100-х нано-кращому випадку.
mmap значною мірою покладається на продуктивність TLB
Тепер ви можете перейти MAP_POPULATE
до того, mmap
щоб сказати йому, щоб створити всі таблиці сторінок перед поверненням, тому під час доступу до нього не повинно бути помилок сторінки. Тепер у цьому є маленька проблема, що він також зчитує весь файл в оперативній пам’яті, який вибухне, якщо ви спробуєте зіставити файл 100 Гб, але давайте ігноруємо це поки що 3 . Ядро потрібно виконати роботу на одній сторінці, щоб створити ці таблиці сторінок (відображається як час ядра). Це в кінцевому підсумку є основною вартістю mmap
підходу, і вона пропорційна розміру файлу (тобто він не стає відносно менш важливим у міру збільшення розміру файлу) 4 .
Нарешті, навіть у користувальницькому просторі доступ до такого відображення не зовсім вільний (порівняно з великими буферами пам’яті, що не походять з файлових файлів mmap
) - навіть після створення таблиць сторінок кожен доступ до нової сторінки збирається, концептуально, нести пропусник TLB. Оскільки mmap
файл файлу означає використання кешу сторінки та його 4K-сторінок, ви знову зазнаєте цю вартість в 25 мільйонів разів за 100 Гб файл.
Тепер фактична вартість цих пропусків TLB сильно залежить, принаймні, від наступних аспектів вашого обладнання: (a) скільки у вас є 4K TLB enties та як працює решта кешування трансляції (b) наскільки добре працює попередній вибір програмного забезпечення за допомогою TLB - наприклад, чи може попередній вибір викликати прогулянку сторінки? (c) як швидко і наскільки паралельно апаратне обладнання для ходьби сторінок. У сучасних процесорах високого класу x86 Intel апаратне забезпечення ходіння сторінок в цілому дуже сильне: є щонайменше 2 паралельних ходунки сторінок, ходіння сторінок може відбуватися одночасно з продовженням виконання, а апаратне попереднє завантаження може викликати прогулянку сторінки. Таким чином, вплив TLB на завантаження зчитування потокового потоку є досить низьким - і таке завантаження часто буде виконуватись аналогічно незалежно від розміру сторінки. Інше обладнання, як правило, набагато гірше!
read () уникає цих підводних каменів
read()
Системний виклик, який є те , що зазвичай лежить в основі «блок читання» виклики типу пропонуються , наприклад, в C, C ++ і інші мови мають один основний недолік , що все добре знає:
- Кожен
read()
виклик N байтів повинен копіювати N байтів з ядра в користувальний простір.
З іншого боку, це дозволяє уникнути більшості вищезазначених витрат - не потрібно розміщувати 25 мільйонів 4K сторінок у просторі користувача. Зазвичай ви можете malloc
використовувати один невеликий буфер в просторі користувача та повторно використовувати його для всіх своїх read
дзвінків. З боку ядра майже не виникає проблем зі сторінками 4K або TLB не вистачає, оскільки вся ОЗУ зазвичай лінійно відображається за допомогою декількох дуже великих сторінок (наприклад, 1 Гб сторінок на x86), тому базові сторінки в кеші сторінок покриті дуже ефективно в просторі ядра.
Таким чином, ви маєте таке порівняння, щоб визначити, що швидше для одного читання великого файлу:
Хіба додаткова робота на сторінці, що має на увазі mmap
підхід, дорожче, ніж робота за байтом копіювання вмісту файлу з ядра в користувальний простір, мається на увазі за допомогою використання read()
?
У багатьох системах вони фактично приблизно збалансовані. Зауважте, що кожен масштабує з абсолютно різними атрибутами обладнання та стека ОС.
Зокрема, mmap
підхід стає відносно швидшим, коли:
- Операційна система має швидку обробку з незначними помилками і особливо оптимізацію об'ємних помилок, таких як неполадки.
- В ОС є хороша
MAP_POPULATE
реалізація, яка дозволяє ефективно обробляти великі карти у випадках, коли, наприклад, базові сторінки є суміжними у фізичній пам'яті.
- Обладнання має високу ефективність перекладу сторінок, такі як великі TLB, швидкі TLB другого рівня, швидкі та паралельні прогулянки сторінок, хороша взаємодія попереднього вибору з перекладом тощо.
... в той час як read()
підхід стає відносно швидшим, коли:
- Система
read()
виклику має хороші показники копіювання. Наприклад, хороші copy_to_user
показники на стороні ядра.
- Ядро має ефективний (по відношенню до країни користувача) спосіб відображення пам'яті, наприклад, використовуючи лише кілька великих сторінок з апаратною підтримкою.
- Ядро має швидкі системні виклики та спосіб зберігати записи TLB ядра навколо всіх системних дзвінків.
Вищевказані апаратні фактори різко різняться на різних платформах, навіть у межах одного сімейства (наприклад, в межах поколінь x86 і особливо на сегментах ринку) і, безумовно, в різних архітектурах (наприклад, ARM vs x86 проти PPC).
Фактори ОС також змінюються, тому що різні вдосконалення з обох сторін викликають великий стрибок відносної швидкості для одного або іншого підходу. Останній список включає:
- Додавання виправдання, описане вище, що справді допомагає
mmap
без випадків MAP_POPULATE
.
- Додавання
copy_to_user
методів швидкого шляху arch/x86/lib/copy_user_64.S
, наприклад, використання, REP MOVQ
коли він швидкий, що справді допомагає цьому read()
випадку.
Оновлення після Spectre та Meltdown
Пом'якшення вразливості Spectre та Meltdown значно підвищило вартість системного дзвінка. У системах, які я виміряв, вартість системного дзвінка "нічого не робити" (що є оцінкою чистого накладного витрат системного виклику, крім будь-якої фактичної роботи, виконаної викликом) склала приблизно від 100 нс на типовий сучасна система Linux приблизно до 700 нс. Крім того, залежно від вашої системи, виправлення ізоляції таблиці сторінок, спеціально для Meltdown, може мати додаткові ефекти нижче за потоком, окрім вартості прямого системного виклику через необхідність перезавантаження записів TLB.
Все це є відносним недоліком для read()
методів, що базуються на основі, порівняно з mmap
методами на основі, оскільки read()
методи повинні робити один системний виклик для кожного вартості даних "розміром буфера". Ви не можете довільно збільшити розмір буфера для амортизації цієї вартості, оскільки використання великих буферів зазвичай працює гірше, оскільки ви перевищуєте розмір L1 і, отже, постійно зазнаєте пропусків кешу.
З іншого боку, за допомогою mmap
, ви можете картографувати велику область пам'яті MAP_POPULATE
та ефективно отримувати доступ до неї за рахунок лише одного системного дзвінка.
1 Це більш-менш також включає випадок, коли файл не був повністю кешований для початку, але де заздалегідь читання ОС досить добре, щоб воно виглядало таким чином (тобто, сторінка зазвичай кешується до того часу, коли ви хочу це). Це тонка проблема, однак тому, що спосіб читання вперед читач часто сильно відрізняється між дзвінками mmap
та read
дзвінками, і його можна додатково коригувати за допомогою дзвінків "консультувати", як описано в 2 .
2 ... тому що, якщо файл не є кешованим, у вашій поведінці будуть повністю переважати проблеми IO, включаючи, наскільки симпатична ваша схема доступу до базового обладнання - і всі ваші зусилля повинні бути в тому, щоб такий доступ був настільки симпатичним, як можливо, наприклад, за допомогою використання madvise
або fadvise
дзвінків (і будь-які зміни рівня програми, які ви можете зробити для покращення моделей доступу).
3 Ви могли б обійти це, наприклад, послідовно mmap
вшиваючись у вікна меншого розміру, скажімо, 100 Мб.
4 Насправді, виявляється, що MAP_POPULATE
підхід (принаймні одна комбінація апаратних засобів / ОС) лише трохи швидший, ніж його не використовувати, можливо, тому, що ядро використовує помилку - тому фактична кількість незначних несправностей зменшується в 16 разів. або так.
mmap()
це в 2-6 разів швидше, ніж використання системних дзвінків, наприкладread()
.