Mmap() против блоков чтения

Я работаю над программой, которая будет обрабатывать файлы, размер которых потенциально может составлять 100 ГБ или более. Файлы содержат наборы записей переменной длины. У меня есть первая реализация и работает, и теперь я смотрю на улучшение производительности, особенно при эффективном выполнении ввода/вывода, поскольку файл ввода сканируется много раз.

Есть ли правило для использования mmap() по сравнению с чтением в блоках через библиотеку С++ fstream? То, что я хотел бы сделать, это прочитать большие блоки с диска в буфер, обработать полные записи из буфера, а затем прочитать больше.

Код mmap() может стать очень запутанным, поскольку блоки mmap 'd должны лежать на границах размера страницы (мое понимание), и записи могут потенциально понравиться на границах страниц. С fstream s я могу просто попытаться начать запись и начать читать снова, так как мы не ограничены чтением блоков, которые лежат на границах размера страницы.

Как я могу выбрать между этими двумя параметрами, не записав сначала полную реализацию? Любые эмпирические правила (например, mmap() в 2 раза быстрее) или простые тесты?

Ответ 1

Я пытался найти последнее слово о производительности mmap/read в Linux, и я наткнулся на хороший пост (ссылка) на Linux список рассылки ядра. Это с 2000 года, поэтому с тех пор было много улучшений ввода-вывода и виртуальной памяти в ядре, но это прекрасно объясняет причину, по которой mmap или read могут быть быстрее или медленнее.

  • Вызов mmap имеет больше накладных расходов, чем read (так же как epoll имеет больше накладных расходов, чем poll, у которых больше накладных расходов, чем read). Изменение сопоставлений виртуальной памяти является довольно дорогостоящей операцией на некоторых процессорах по тем же причинам, что и переключение между различными процессами дорого.
  • Система ввода-вывода уже может использовать кеш диска, поэтому, если вы прочитаете файл, вы попадете в кэш или пропустите его независимо от того, какой метод вы используете.

Однако

  • Карты памяти обычно быстрее для случайного доступа, особенно если ваши шаблоны доступа разрежены и непредсказуемы.
  • Карты памяти позволяют вам продолжать использовать страницы из кеша, пока вы не закончите. Это означает, что если вы используете файл в течение длительного периода времени, закройте его и снова откройте, страницы будут кэшироваться. С read ваш файл, возможно, был удален из кэша давным-давно. Это не применяется, если вы используете файл и немедленно отбрасываете его. (Если вы пытаетесь использовать mlock страницы, чтобы сохранить их в кеше, вы пытаетесь перехитрить дисковый кеш, и такой глупость редко помогает производительности системы).
  • Чтение файла напрямую очень просто и быстро.

Обсуждение mmap/read напоминает мне о двух других обсуждениях производительности:

  • Некоторые программисты Java были шокированы, обнаружив, что неблокирующий ввод-вывод часто медленнее блокировки ввода-вывода, что имеет смысл, если вы знаете, что для неблокирующего ввода-вывода требуется сделать больше системных вызовов.

  • Некоторые другие сетевые программисты были шокированы, узнав, что epoll часто медленнее, чем poll, что имеет смысл, если вы знаете, что для управления epoll требуется создание больших системных вызовов.

Вывод: Используйте карты памяти, если вы произвольно обращаетесь к данным, держите их в течение длительного времени или знаете, что можете поделиться им с другими процессами (MAP_SHARED не очень интересно, если нет фактического обмена). Обычно читайте файлы, если вы последовательно получаете доступ к данным или отбрасываете их после чтения. И если любой из методов делает вашу программу менее сложной, сделайте это. Для многих случаев в реальном мире нет уверенного способа показать одно быстрее, не тестируя ваше фактическое приложение, и НЕ является эталоном.

(Извините за некропольский вопрос, но я искал ответ, и этот вопрос продолжался в верхней части результатов Google.)

Ответ 2

mmap быстрее. Вы можете написать простой тест, чтобы доказать это самому себе:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

против

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

Ясно, что я оставляю детали (например, как определить, когда вы достигнете конца файла, если, например, ваш файл не является кратным page_size), но он действительно не должен будет намного сложнее, чем это.

Если вы можете, вы можете попытаться разбить свои данные на несколько файлов, которые могут быть mmap() - ed в целом, а не частично (намного проще).

Пару месяцев назад у меня была полупеченная реализация класса mmap() для раздвижных окон для boost_iostreams, но никто не заботился, и я занялся другими вещами. К сожалению, несколько недель назад я удалил архив старых незавершенных проектов, и это было одной из жертв: - (

Обновить. Я также должен добавить оговорку, что этот тест будет выглядеть совсем по-другому в Windows, потому что Microsoft внедрила отличный кеш файл, который делает большинство из того, что вы сделали бы с mmap в первую очередь. Т.е. для часто встречающихся файлов вы можете просто создать std:: ifstream.read(), и это будет так же быстро, как mmap, потому что кэш файлов уже сделал бы карту памяти для вас, и он прозрачен.

Окончательное обновление. Посмотрите, люди: во множестве различных комбинаций плат ОС и стандартных библиотек и дисков и иерархий памяти я не могу точно сказать, что системный вызов mmap, рассматриваемый как черный ящик, всегда всегда будет значительно быстрее, чем read. Это было не совсем мое намерение, даже если бы мои слова могли быть истолкованы именно так. В конечном счете, моя точка зрения заключалась в том, что ввод-вывод с отображением памяти, как правило, быстрее, чем байтов на основе байтов; это все еще верно. Если вы обнаружите экспериментально, что нет никакой разницы между ними, то единственное объяснение, которое кажется мне разумным, заключается в том, что ваша платформа реализует сопоставление памяти под обложками таким образом, чтобы выгодно выполнять вызовы read. Единственный способ быть абсолютно уверенным в том, что вы используете переносимый памятью ввода-вывода портативным способом, - это использовать mmap. Если вы не заботитесь о переносимости, и можете полагаться на конкретные характеристики ваших целевых платформ, то использование read может быть подходящим, не жертвуя заметно любой производительностью.

Изменить для очистки списка ответов: @jbl:

скользящее окно mmap звучит интересно. Можете ли вы сказать немного больше об этом?

Конечно, я писал С++-библиотеку для Git (libgit ++, если хотите), и у меня возникла аналогичная проблема: мне нужно было открыть большие (очень большие) файлы и не иметь производительность будет полной собакой (как это было бы с std::fstream).

Boost::Iostreams уже имеет источник mapped_file, но проблема заключалась в том, что он был mmap ping целыми файлами, что ограничивает вас 2 ^ (wordize). На 32-битных машинах 4 ГБ недостаточно велик. Неразумно ожидать, что файлы .pack в Git станут намного больше, поэтому мне нужно было прочитать файл в кусках, не прибегая к регулярному вводу/выводу файла. Под обложками Boost::Iostreams я реализовал Source, который представляет собой более или менее другой вид взаимодействия между std::streambuf и std::istream. Вы также можете попробовать подобный подход, просто наследуя std::filebuf в mapped_filebuf и аналогичным образом наследуя std::fstream в a mapped_fstream. Это взаимодействие между двумя, что трудно получить право. Boost::Iostreams выполняет некоторые из выполненных для вас работ, а также предоставляет перехваты для фильтров и цепей, поэтому я подумал, что было бы более полезно реализовать его таким образом.

Ответ 3

Основная стоимость исполнения - это дисковый ввод-вывод. "mmap()", конечно, быстрее, чем istream, но разница может быть не заметна, потому что дисковый ввод/вывод будет доминировать в ваших run-times.

Я попробовал фрагмент кода Бена Коллинза (см. выше/ниже), чтобы проверить его утверждение о том, что "mmap() работает быстрее" и не обнаружил заметной разницы. См. Мои комментарии к его ответу.

Я бы, конечно, не рекомендовал отдельно копировать каждую запись по очереди, если ваши "записи" огромны - это было бы ужасно медленно, для чего требовалось 2 системных вызова для каждой записи и, возможно, потерять страницу из кеша дисковой памяти.....

В вашем случае я думаю, что mmap(), istream и низкоуровневые вызовы open()/read() будут примерно одинаковыми. Я бы рекомендовал mmap() в этих случаях:

  • Внутри файла есть произвольный доступ (не последовательный), AND
  • все это удобно помещается в память или имеется локальная ссылка в файле, чтобы можно было отображать определенные страницы и отображать другие страницы. Таким образом, операционная система использует доступную оперативную память для максимальной выгоды.
  • ИЛИ если несколько процессов читают/работают в одном файле, то mmap() является фантастическим, потому что все процессы используют одни и те же физические страницы.

(btw - Мне нравится mmap()/MapViewOfFile()).

Ответ 4

Здесь уже есть много хороших ответов, которые охватывают многие существенные моменты, поэтому я просто добавлю пару вопросов, которые я не видел, рассмотренных непосредственно выше. Таким образом, этот ответ не следует считать исчерпывающим из плюсов и минусов, а скорее дополнением к другим ответам здесь.

ММАП кажется волшебством

В случае, когда файл уже полностью кэширован 1 как базовый уровень 2mmap может показаться очень похожим на магию:

  1. mmap требует только 1 системного вызова (потенциально) для сопоставления всего файла, после чего системные вызовы больше не нужны.
  2. mmap не требует копирования данных файла из ядра в пространство пользователя.
  3. mmap позволяет получить доступ к файлу "как к памяти", в том числе обрабатывать его любыми дополнительными приемами, которые вы можете сделать с памятью, такими как автоматическая векторизация компилятора, встроенные функции SIMD, предварительная выборка, оптимизированные процедуры анализа в памяти, OpenMP и т.д.

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

Ну, это может.

Mmap на самом деле не волшебство, потому что...

mmap по-прежнему работает на странице

Основная скрытая стоимость mmap сравнению с read(2) (которая на самом деле является сопоставимым системным вызовом на уровне ОС для блоков чтения) заключается в том, что с mmap вам придется выполнять "некоторую работу" для каждой страницы 4K в пользовательском пространстве, даже если это может быть скрыто механизмом ошибки страницы.

Например, для типичной реализации, которая просто mmap весь файл, необходимо выполнить аварийный сбой, поэтому 100 ГБ /4 КБ = 25 миллионов ошибок для чтения файла размером 100 ГБ. Теперь, это будут незначительные ошибки, но 25 миллиардов страниц все еще не будут слишком быстрыми. В лучшем случае стоимость незначительной ошибки, вероятно, исчисляется сотнями нано.

mmap сильно зависит от производительности TLB

Теперь вы можете передать MAP_POPULATE в mmap чтобы он MAP_POPULATE настроить все таблицы страниц перед возвратом, поэтому при обращении к нему не должно быть ошибок страницы. Теперь у него есть небольшая проблема, заключающаяся в том, что он также считывает весь файл в ОЗУ, который взорвется, если вы попытаетесь отобразить файл размером 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 MB.

4 На самом деле, оказывается, что MAP_POPULATE подход (по крайней мере один некоторые аппаратные/комбинация OS) лишь немного быстрее, чем он не используется, вероятно, потому, что ядро использует faultaround - так что фактическое число мелких дефектов уменьшается с коэффициентом 16 или около того.

Ответ 5

Извините, что Бен Коллинз потерял свой скопированный исходный код mmap. Это было бы неплохо иметь в Boost.

Да, отображение файла происходит намного быстрее. Фактически вы используете подсистему виртуальной памяти ОС для привязки памяти к диску и наоборот. Подумайте об этом так: если бы разработчики ОС OS могли сделать это быстрее, они бы это сделали. Потому что это делает примерно все быстрее: базы данных, время загрузки, время загрузки программы и т.д.

Подход с раздвижным окном действительно не так сложно, поскольку сразу несколько отображаемых страниц могут быть отображены. Таким образом, размер записи не имеет значения до тех пор, пока самая большая из любой отдельной записи будет вписываться в память. Важное значение имеет управление ведением бухгалтерского учета.

Если запись не начинается с границы getpagesize(), ваше сопоставление должно начинаться на предыдущей странице. Длина отображаемого региона продолжается от первого байта записи (округляется, если необходимо, до ближайшего кратного getpagesize()) до последнего байта записи (округляется до ближайшего кратного getpagesize()). Когда вы закончите обработку записи, вы можете ее отменить() и перейти к следующему.

Все это прекрасно работает под Windows, также используя CreateFileMapping() и MapViewOfFile() (и GetSystemInfo() для получения SYSTEM_INFO.dwAllocationGranularity --- not SYSTEM_INFO.dwPageSize).

Ответ 6

mmap должен быть быстрее, но я не знаю, сколько. Это очень зависит от вашего кода. Если вы используете mmap, лучше всего всего разбить весь файл, что значительно облегчит вам жизнь. Одна из потенциальных проблем заключается в том, что если ваш файл превышает 4 ГБ (или на практике это ограничение ниже, часто 2 ГБ), вам понадобится 64-битная архитектура. Поэтому, если вы используете среду 32, вы, вероятно, не хотите ее использовать.

Сказав это, может быть лучший путь к повышению производительности. Вы сказали, что входной файл сканируется много раз, если вы можете прочитать его за один проход, а затем сделать с ним, что потенциально может быть намного быстрее.

Ответ 7

Я согласен с тем, что ввод/вывод файлов mmap'd будет быстрее, но пока ваш бенчмаркинг кода не должен быть несколько оптимизирован для примера счетчика?

Бен Коллинз писал (а):

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

Я бы посоветовал также попробовать:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

Кроме того, вы также можете попытаться сделать размер буфера таким же размером, как одна страница виртуальной памяти, в случае, если 0x1000 не является размером одной страницы виртуальной памяти на вашем компьютере... IMHO mmap'd file I/O все еще выигрывает, но это должно приблизить ситуацию.

Ответ 8

Возможно, вам следует предварительно обработать файлы, поэтому каждая запись находится в отдельном файле (или, по крайней мере, каждый файл имеет размер, соответствующий mmap).

Также вы могли бы выполнить все шаги обработки для каждой записи, прежде чем переходить на следующую? Может быть, это позволит избежать некоторых издержек ввода-вывода?

Ответ 9

На мой взгляд, использование mmap() "просто" освобождает разработчика от необходимости писать собственный код кеширования. В простом случае "прочитанный файл как раз один раз" это не будет сложно (хотя, как указывает mlbrock, вы все равно сохраняете копию памяти в пространстве операций), но если вы собираетесь туда и обратно в файл или пропуская биты и т.д., я считаю, что разработчики ядра, вероятно, сделали лучшую работу по реализации кэширования, чем я могу...

Ответ 10

Я помню, как много лет назад отображался огромный файл, содержащий древовидную структуру. Я был поражен скоростью по сравнению с обычной де-сериализацией, которая включает в себя много работы в памяти, например, распределение узлов дерева и указателей установки. Так что на самом деле я сравнивал один вызов с mmap (или его аналог в Windows) против многих (MANY) вызовов для вызова оператора new и конструктора. Для такой задачи mmap является непревзойденным по сравнению с де-сериализацией. Разумеется, для этого нужно искать повышающий перемещаемый указатель.

Ответ 11

Это звучит как хороший прецедент для многопоточности... Я бы подумал, что вы можете легко настроить один поток для чтения данных, а другие его обрабатывают. Это может быть способ значительно увеличить воспринимаемую производительность. Просто мысль.

Ответ 12

Я думаю, что самое лучшее в mmap - это потенциал для асинхронного чтения с:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

Проблема в том, что я не могу найти правильный MAP_FLAGS, чтобы дать понять, что эта память должна быть синхронизирована с файлом как можно скорее. Я надеюсь, что MAP_POPULATE дает правильную подсказку для mmap (т.е. Он не будет пытаться загрузить все содержимое перед возвратом из вызова, но будет делать это в async с помощью feed_data). По крайней мере, это дает лучшие результаты с этим флагом даже в том, что в руководстве указано, что он ничего не делает без MAP_PRIVATE с 2.6.23.