Здесь уже есть много хороших ответов, которые охватывают многие существенные моменты, поэтому я просто добавлю пару вопросов, которые я не видел, рассмотренных непосредственно выше. Таким образом, этот ответ не следует считать исчерпывающим из плюсов и минусов, а скорее дополнением к другим ответам здесь.
ММАП кажется волшебством
Если взять в качестве базового 2 случай, когда файл уже полностью кэширован 1 , это может показаться очень похожим на магию :mmap
mmap
требуется только 1 системный вызов (потенциально) отобразить весь файл, после чего системные вызовы больше не нужны.
mmap
не требует копирования данных файла из ядра в пространство пользователя.
mmap
позволяет получить доступ к файлу «как к памяти», в том числе обрабатывать его любыми дополнительными приемами, которые вы можете сделать с памятью, такими как автоматическая векторизация компилятора, встроенные функции SIMD , предварительная выборка, оптимизированные процедуры синтаксического анализа в памяти, OpenMP и т. д.
В случае, если файл уже находится в кеше, кажется, что его невозможно превзойти: вы просто получаете прямой доступ к кешу страницы ядра как к памяти, и он не может работать быстрее, чем это.
Ну, это может.
Mmap на самом деле не волшебство, потому что ...
mmap по-прежнему работает на странице
Основная скрытая стоимость mmap
vs read(2)
(которая на самом деле сопоставима с системным вызовом на уровне ОС для блоков чтения ) заключается в том, что mmap
вам придется выполнять «некоторую работу» для каждой страницы 4K в пользовательском пространстве, даже если она может быть скрыта механизм сбоя страницы.
Например, для типичной реализации, mmap
в которой требуется всего один файл, необходимо выполнить отказ, поэтому 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 по системным вызовам.
Аппаратные факторы выше варьируются дико на различных платформах, даже в пределах одной и той же семье (например, в пределах х86 поколений и особенно рыночных сегментов) и , безусловно , через архитектур (например, 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
вызовами и может быть дополнительно отрегулирован вызовами «посоветовать», как описано в 2 .
2 ... потому что, если файл не кэшируется, ваше поведение будет полностью зависеть от проблем ввода-вывода, в том числе от того, насколько отзывчива ваша схема доступа к базовому оборудованию - и все ваши усилия должны быть направлены на то, чтобы такой доступ был таким же сочувственным, как возможно, например, с помощью madvise
или fadvise
вызовов (и любые изменения уровня приложения, которые вы можете сделать, чтобы улучшить шаблоны доступа).
3 Вы можете обойти это, например, последовательно mmap
вводя окна меньшего размера, скажем, 100 МБ.
4 На самом деле, оказывается, что MAP_POPULATE
подход (по крайней мере , один некоторые аппаратные / комбинация OS) лишь немного быстрее , чем он не используется, вероятно , потому , что ядро использует faultaround - так что фактическое число мелких дефектов уменьшается в 16 раз или так.
mmap()
это в 2-6 раз быстрее, чем при использовании системных вызовов, напримерread()
.