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

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

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

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

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

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

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

    Однако,

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

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

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

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

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

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

    Главной производительностью будет дисковый ввод / вывод. «mmap ()», конечно, быстрее, чем istream, но разница может быть не заметна, потому что дисковый ввод / вывод будет доминировать в ваших исполнениях.

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

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

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

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

    (Кстати, я люблю mmap () / MapViewOfFile ()).

    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 в целом, а не частично (намного проще).

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

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

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

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

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

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

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

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

    mmap кажется волшебным

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

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

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

    Ну, может.

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

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

    Первичная скрытая стоимость mmap vs read(2) (что действительно сопоставимо с системным уровнем OS-уровня для блоков чтения ) заключается в том, что с mmap вам нужно будет выполнить «некоторую работу» для каждой страницы 4K в пользовательском пространстве, хотя он может быть скрыт механизмом ошибки страницы.

    Для примера типичная реализация, которая просто mmap s весь файл будет нуждаться в сбое, чтобы 100 ГБ / 4K = 25 миллионов ошибок, чтобы прочитать 100-Гбайт файл. Теперь это будут незначительные ошибки , но 25 миллиардов ошибок страниц по-прежнему не будут очень быстрыми. Стоимость незначительной ошибки, вероятно, находится в 100 с наном в лучшем случае.

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

    Теперь вы можете передать MAP_POPULATE в mmap чтобы сообщить ему, чтобы все таблицы страниц были возвращены, поэтому при доступе к ним не должно быть ошибок страницы. Теперь у этого есть небольшая проблема, что он также считывает весь файл в ОЗУ, который взорвется, если вы попытаетесь отобразить 100-Гбайт файл, но давайте проигнорируем это на данный момент 3 . Ядро должно выполнять работу на странице для настройки этих таблиц страниц (отображается как время ядра). Это в конечном итоге является основной стоимостью подхода mmap , и оно пропорционально размеру файла (т. Е. Он не становится относительно менее важным по мере увеличения размера файла). 4 .

    Наконец, даже в пользовательском пространстве доступ к такому сопоставлению не является абсолютно бесплатным (по сравнению с большими буферами памяти, которые не происходят из файлового mmap ) – даже если таблицы страниц настроены, каждый доступ к новой странице будет , концептуально, несут пропуски TLB. Поскольку mmap файл означает использование кеша страницы и его страниц 4K, вы снова берете эту стоимость 25 миллионов раз за 100-Гбайт-файл.

    Теперь фактическая стоимость этих пропусков TLB в значительной степени зависит, по крайней мере, от следующих аспектов вашего оборудования: (a) сколько 4K TLB у вас есть и как работает остальная работа по кешированию перевода (б), насколько хорошо выполняется предварительная выборка оборудования с TLB – например, может ли prefetch инициировать прохождение страницы? (c) насколько быстро и насколько параллельны оборудование для ходьбы страницы. На современных высокопроизводительных процессорах Intel x86 процессоры для прокрутки страниц в целом очень прочные: есть, по меньшей мере, два параллельных сторожевых ходока, одновременная перемотка страниц может продолжаться, а предварительная выборка аппаратного обеспечения может инициировать прохождение страницы. Таким образом, влияние TLB на нагрузку чтения с streamом довольно низкое – и такая загрузка часто будет выполняться одинаково независимо от размера страницы. Однако другое оборудование, как правило, намного хуже!

    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 vs PPC).

    Факторы ОС также меняются, причем различные улучшения с обеих сторон вызывают большой скачок относительной скорости для одного подхода или другого. Недавний список включает:

    • Добавление описанной выше неисправности, которая действительно помогает в случае mmap без MAP_POPULATE .
    • Добавление методов copy_to_user быстрого пути в arch/x86/lib/copy_user_64.S , например, с использованием REP MOVQ когда оно выполняется быстро, что действительно помогает в случае read() .

    1 Это более или менее также включает в себя случай, когда файл не был полностью кэширован для начала, но где скорость чтения ОС достаточно хороша, чтобы заставить ее выглядеть так (например, страница обычно кэшируется к тому времени, когда вы хочу это). Это тонкий вопрос, хотя из-за того, что способ чтения вперед часто отличается между сообщениями mmap и read и может быть дополнительно скорректирован с помощью «советовать» вызовам, как описано в 2 .

    2 … потому что, если файл не кэшируется, ваше поведение будет полностью зависеть от проблем с IO, в том числе от того, насколько симпатизирующий вашему шаблону доступа к базовому оборудованию – и все ваши усилия должны заключаться в обеспечении такого доступа, как симпатизирующего возможно, например, посредством использования madvise или fadvise вызовов (и любых изменений уровня приложения, которые вы можете внести для улучшения шаблонов доступа).

    3 Вы можете обойти это, например, путем последовательного mmap в windows меньшего размера, скажем, 100 МБ.

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

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

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

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

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

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

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

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

    Я согласен с тем, что ввод / вывод файлов 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 still побеждает, но это должно приблизить ситуацию.

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

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

    Я помню, как много лет назад отображался огромный файл, содержащий древовидную структуру. Я был поражен скоростью по сравнению с обычной де-сериализацией, которая включает в себя много работы в памяти, например, выделение узлов дерева и указателей установки. Поэтому на самом деле я сравнивал один вызов с MMAP (или его коллегой в Windows) против многих вызовов (МНОГО) для вызовов оператора new и конструктора. Для такой задачи mmap является непревзойденным по сравнению с де-сериализацией. Конечно, нужно заглянуть в ускоритель для перемещаемого указателя.

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

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

    Я думаю, что самое лучшее в 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.

    Давайте будем гением компьютера.