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

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

159 голосов
/ 05 сентября 2008

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

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

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

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

Ответы [ 12 ]

177 голосов
/ 17 июня 2011

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

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

Тем не менее,

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

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

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

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

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

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

42 голосов
/ 30 сентября 2008

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

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

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

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

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

(кстати - я люблю mmap () / MapViewOfFile ()).

41 голосов
/ 05 сентября 2008

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

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

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

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

Изменить, чтобы очистить список ответов: @jbl:

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

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

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

28 голосов
/ 02 января 2017

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

Mmap кажется волшебством

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

  1. mmap требуется только 1 системный вызов для (потенциально) отображения всего файла, после чего системные вызовы больше не нужны.
  2. mmap не требует копирования данных файла из ядра в пространство пользователя.
  3. 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 раз.

7 голосов
/ 16 сентября 2008

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

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

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

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

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

4 голосов
/ 05 сентября 2008

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

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

3 голосов
/ 30 сентября 2008

Я согласен с тем, что ввод / вывод 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 файл I / O все еще побеждает, но это должно сближать.

2 голосов
/ 27 марта 2014

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

2 голосов
/ 29 марта 2009

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

2 голосов
/ 05 сентября 2008

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

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

...