Вопрос

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

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

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

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

Это было полезно?

Решение

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

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

Однако,

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

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

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

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

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

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

Другие советы

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

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

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

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

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

(кстати, мне нравится mmap()/MapViewOfFile()).

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

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() со скользящим окном для 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 ^ (размер слова).На 32-битных машинах 4 ГБ недостаточно.Небезосновательно ожидать, что .pack файлы в Git становятся намного больше этого размера, поэтому мне нужно было читать файл по частям, не прибегая к обычному файловому вводу-выводу.Под обложками 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 встроенные функции, предварительная выборка, оптимизированные процедуры анализа в памяти, OpenMP и т. д.

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

Ну, может.

mmap на самом деле не является магией, потому что...

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

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

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

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

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

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

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

read() позволяет избежать этих ошибок

А read() Системный вызов, который обычно лежит в основе вызовов типа «блочное чтение», предлагаемых, например, в C, C++ и других языках, имеет один основной недостаток, о котором все хорошо знают:

  • Каждый read() вызов N байтов должен копировать N байтов из ядра в пространство пользователя.

С другой стороны, это позволяет избежать большей части вышеуказанных затрат — вам не нужно отображать 25 миллионов страниц 4K в пользовательском пространстве.Обычно вы можете malloc один небольшой буфер в пользовательском пространстве и повторно используйте его для всех ваших read звонки.Со стороны ядра почти нет проблем со страницами размером 4 КБ или промахами TLB, поскольку вся оперативная память обычно линейно отображается с использованием нескольких очень больших страниц (например, страниц по 1 ГБ на x86), поэтому покрываются базовые страницы в страничном кэше. очень эффективно в пространстве ядра.

Итак, по сути, у вас есть следующее сравнение, чтобы определить, что быстрее при однократном чтении большого файла:

Предполагается ли дополнительная работа на страницу mmap подход более затратный, чем побайтовое копирование содержимого файла из ядра в пространство пользователя, подразумеваемое использованием read()?

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

В частности, mmap подход становится относительно более быстрым, когда:

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

...в то время read() подход становится относительно более быстрым, когда:

  • А read() syscall имеет хорошую производительность копирования.например, хорошо copy_to_user производительность на стороне ядра.
  • Ядро имеет эффективный (относительно пользовательского пространства) способ распределения памяти, например, используя только несколько больших страниц с аппаратной поддержкой.
  • Ядро имеет быстрые системные вызовы и способ сохранять записи TLB ядра между системными вызовами.

Аппаратные факторы, указанные выше, различаются дико на разных платформах, даже в пределах одного семейства (например, в пределах поколений x86 и особенно в сегментах рынка) и, безусловно, в разных архитектурах (например, ARM, x86 или PPC).

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

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

Обновление после Spectre и Meltdown

Устранение уязвимостей 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 подход (по крайней мере, одна комбинация аппаратного обеспечения и ОС) лишь немного быстрее, чем его неиспользование, вероятно, потому, что ядро ​​использует обходной путь - таким образом, фактическое количество мелких неисправностей уменьшается примерно в 16 раз.

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

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

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

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

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

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

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

Я согласен, что ввод-вывод файлов с помощью mmap будет быстрее, но во время сравнительного анализа кода не следует ли привести встречный пример в некотором роде оптимизирован?

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

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 не соответствует размеру одной страницы виртуальной памяти на вашем компьютере...ИМХО, файловый ввод-вывод mmap'd по-прежнему выигрывает, но это должно сблизить ситуацию.

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

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

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

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

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

Я думаю, что самое замечательное в 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 (т.он не будет пытаться загрузить все содержимое перед возвратом из вызова, а сделает это асинхронно.с фидом_data).По крайней мере, этот флаг дает лучшие результаты, хотя в руководстве указано, что без MAP_PRIVATE ничего не происходит, начиная с версии 2.6.23.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top