Здесь уже есть много хороших ответов, которые охватывают многие существенные моменты, поэтому я просто добавлю пару вопросов, которые я не видел, рассмотренных непосредственно выше. Таким образом, этот ответ не следует считать исчерпывающим из плюсов и минусов, а скорее дополнением к другим ответам здесь.
Mmap кажется волшебством
В случае, когда файл уже полностью кэширован 1 в качестве базовой линии 2 , mmap
может выглядеть как magic :
mmap
требуется только 1 системный вызов для (потенциально) отображения всего файла, после чего системные вызовы больше не нужны.
mmap
не требует копирования данных файла из ядра в пространство пользователя.
mmap
позволяет получить доступ к файлу «как к памяти», включая обработку его с помощью любых дополнительных хитростей, которые вы можете сделать с памятью, таких как автоматическая векторизация компилятора, SIMD встроенные функции, предварительная выборка, оптимизированная в процедуры разборки памяти, OpenMP и т. д.
В случае, если файл уже находится в кеше, кажется, что его невозможно превзойти: вы просто получаете прямой доступ к кешу страницы ядра как к памяти, и он не может получить скорость быстрее этого.
Ну, может.
mmap на самом деле не волшебство, потому что ...
mmap по-прежнему работает на странице
Основная скрытая стоимость mmap
против read(2)
(которая на самом деле сопоставима с системным вызовом на уровне ОС для блоков чтения ) заключается в том, что с mmap
вы будете необходимо выполнить «некоторую работу» для каждой страницы 4K в пользовательском пространстве, даже если она может быть скрыта механизмом сбоя страницы.
В качестве примера типичной реализации, в которой всего mmap
s должен быть выполнен сбой всего файла, поэтому 100 ГБ / 4 КБ = 25 миллионов ошибок для чтения файла размером 100 ГБ. Теперь это будут незначительные ошибки , но 25 миллиардов страниц все еще не будут слишком быстрыми. В лучшем случае стоимость незначительной ошибки, вероятно, исчисляется сотнями нанос.
mmap сильно зависит от производительности TLB
Теперь вы можете передать MAP_POPULATE
в mmap
, чтобы сообщить ему настроить все таблицы страниц перед возвратом, чтобы при обращении к нему не было ошибок страницы. Теперь в этом есть небольшая проблема, заключающаяся в том, что он также считывает весь файл в ОЗУ, который взорвется, если вы попытаетесь отобразить файл размером 100 ГБ, но давайте пока проигнорируем это 3 . Ядро должно выполнить на страницу работы , чтобы настроить эти таблицы страниц (отображается как время ядра). Это приводит к большим затратам в подходе mmap
и пропорционально размеру файла (то есть он не становится относительно менее важным при увеличении размера файла) 4 .
Наконец, даже при доступе к пользовательскому пространству такое отображение не является совершенно бесплатным (по сравнению с большими буферами памяти, не происходящими из файлового mmap
) - даже после настройки таблиц страниц каждый доступ к концептуально новая страница будет иметь место для пропуска TLB. Поскольку mmap
использование файла означает использование кэша страниц и его страниц размером 4 КБ, вы снова несете эту стоимость в 25 миллионов раз за файл объемом 100 ГБ.
Теперь фактическая стоимость этих пропусков TLB сильно зависит как минимум от следующих аспектов вашего оборудования: (a) сколько у вас 4K TLB и как работает остальная часть кэширования перевода (b) насколько хорошо оборудование Предварительная выборка имеет дело с TLB - например, может ли предварительная выборка вызвать просмотр страницы? (c) насколько быстро и параллельно работает оборудование для перемещения по страницам. На современных высокопроизводительных процессорах Intel 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 против x86 против PPC).
Коэффициенты ОС также меняются, с различными улучшениями с обеих сторон, вызывающими большой скачок в относительной скорости для одного подхода или другого. Недавний список включает в себя:
- Добавление описанного выше обходного отказа, которое действительно помогает делу
mmap
без MAP_POPULATE
.
- Добавление быстрых путей
copy_to_user
в arch/x86/lib/copy_user_64.S
, например, использование REP MOVQ
, когда это быстро, что действительно помогает в случае read()
.
Обновление после Призрака и Обрушения
Снижение уязвимостей Spectre и Meltdown значительно увеличило стоимость системного вызова. В системах, которые я измерил, стоимость системного вызова «ничего не делать» (который является оценкой чистой служебной нагрузки системного вызова, помимо любой фактической работы, выполняемой вызовом) выросла с примерно 100 нс на типичном современная система Linux примерно до 700 нс. Кроме того, в зависимости от вашей системы исправление изоляции таблицы страниц , специально предназначенное для Meltdown, может иметь дополнительные нисходящие эффекты помимо затрат на прямые системные вызовы из-за необходимости перезагрузки записей TLB.
Все это является относительным недостатком методов, основанных на read()
, по сравнению с методами, основанными на mmap
, поскольку методы read()
должны выполнять один системный вызов для каждого значения "размера буфера" данных. Вы не можете произвольно увеличить размер буфера, чтобы амортизировать эту стоимость, поскольку использование больших буферов обычно работает хуже, так как вы превышаете размер L1 и, следовательно, постоянно испытываете потери в кеше.
С другой стороны, с помощью mmap
вы можете отобразить большую область памяти с помощью MAP_POPULATE
и получить к ней эффективный доступ только за один системный вызов.
1 Это более или менее также включает в себя случай, когда файл не был полностью кэширован для начала, но когда упреждающее чтение ОС достаточно, чтобы оно выглядело так (т.е. страница обычно кэшируется к тому времени, когда вы этого хотите). Это небольшая проблема, потому что способ чтения с опережением часто весьма различен между вызовами mmap
и read
и может быть дополнительно отрегулирован вызовами "advise", как описано в 2 .
2 ... потому что, если файл не кэширован, ваше поведение будет полностью зависеть от проблем ввода-вывода, в том числе от того, насколько отзывчив ваш шаблон доступа к базовому оборудованию - и все ваши усилия должны быть направлены на то, чтобы такой доступ был как можно более отзывчивым, например, с помощью madvise
или fadvise
вызовов (и любых изменений уровня приложения, которые вы можете сделать, чтобы улучшить шаблоны доступа).
3 Вы можете обойти это, например, последовательно mmap
в окнах меньшего размера, скажем, 100 МБ.
4 На самом деле, оказывается, что подход MAP_POPULATE
(по крайней мере, одна комбинация аппаратного обеспечения / ОС) лишь немного быстрее, чем его отсутствие, возможно, потому что ядро использует обход ошибки - поэтому фактическое количество мелких неисправностей уменьшается примерно в 16 раз.