Объяснение крошечных чтений (перекрывающихся, буферизованных), превосходящих по производительности большие непрерывные чтения?

(извините за несколько длинное вступление)

Во время разработки приложения, которое предварительно загружает весь большой файл (> 400 МБ) в буферный кеш для ускорения фактического запуска позже, я проверил, имеет ли чтение 4 МБ за раз какие-либо заметные преимущества по сравнению с чтением фрагментов только 1 МБ за раз. Удивительно, но меньшие запросы на самом деле оказались быстрее. Это казалось нелогичным, поэтому я провел более обширный тест.

Буферный кеш был очищен перед запуском тестов (просто для смеха, я тоже провел один запуск с файлом в буферах. Буферный кеш обеспечивает скорость более 2 ГБ/с независимо от размера запроса, хотя и с удивительным +/- 30% случайная дисперсия).
Все чтения использовали перекрывающийся ReadFile с одним и тем же целевым буфером (дескриптор был открыт с FILE_FLAG_OVERLAPPED и без FILE_FLAG_NO_BUFFERING). Используемый жесткий диск несколько устарел, но полностью функционален, NTFS имеет размер кластера 8 КБ. Диск был дефрагментирован после первоначального запуска (6 фрагментов против нефрагментированных, нулевая разница). Для лучших цифр я также использовал файл большего размера, ниже цифры указаны для чтения 1 ГБ.

Результаты действительно удивили:

4MB x 256    : 5ms per request,    completion 25.8s @ ~40 MB/s
1MB x 1024   : 11.7ms per request, completion 23.3s @ ~43 MB/s
32kB x 32768 : 12.6ms per request, completion 15.5s @ ~66 MB/s
16kB x 65536 : 12.8ms per request, completion 13.5s @ ~75 MB/s

Итак, это говорит о том, что отправка десяти тысяч запросов длиной в два кластера на самом деле лучше, чем отправка нескольких сотен больших непрерывных операций чтения. Время отправки (время до возврата ReadFile) существенно увеличивается по мере увеличения количества запросов, но время асинхронного выполнения уменьшается почти вдвое. на самом деле следует сказать 20-30%), в то время как асинхронное чтение завершается, что является удивительным объемом ЦП - очевидно, ОС также выполняет какое-то значительное количество занятых ожиданий. 30% загрузки процессора на 25 секунд на частоте 2,6 ГГц, это довольно много циклов для «ничего не делания».

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

Я вижу, как планировщик ввода-вывода сможет оптимизировать несколько запросов, сводя к минимуму поиски, особенно когда запросы являются произвольным доступом (а это не так!). Я также вижу, как жесткий диск сможет выполнить подобную оптимизацию при нескольких запросах в NCQ.
Однако мы говорим о смехотворном количестве смехотворно маленьких запросов, которые, тем не менее, превосходят то, что кажется разумным на коэффициент 2.

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

Многократное сопоставление/отмена отображения вида с разными смещениями занимает практически нулевое время. Использование представления размером 16 МБ и ошибка каждой страницы с помощью простого цикла for(), считывающего один байт на страницу, завершается за 9,2 секунды при ~ 111 МБ/с. Использование ЦП всегда ниже 3% (одно ядро). Тот же компьютер, тот же диск, все то же самое.

Также кажется, что Windows загружает 8 страниц в буферный кеш за раз, хотя на самом деле создается только одна страница. Сбои каждой 8-й страницы выполняются с одинаковой скоростью и загружают одинаковое количество данных с диска, но показывают более низкие показатели «физической памяти» и «системного кеша» и только 1/8 ошибок страниц. Последующие чтения доказывают, что страницы тем не менее окончательно находятся в буферном кеше (без задержки, без активности диска).

(Возможно, очень, очень отдаленно связано с файлом с отображением памяти быстрее при большом последовательном чтении?)

Чтобы сделать его более наглядным:
введите здесь описание изображения

Обновление:

Использование FILE_FLAG_SEQUENTIAL_SCAN, по-видимому, несколько «уравновешивает» чтение 128 КБ, повышая производительность на 100%. С другой стороны, это серьезно влияет на чтение 512 000 и 256 000 (вы должны задаться вопросом, почему?) и не оказывает никакого реального влияния на что-либо еще. График МБ/с для меньших размеров блоков, возможно, кажется немного более «ровным», но нет никакой разницы во времени выполнения.

введите здесь описание изображения

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

При учете фактического асинхронного чтения по сравнению с «немедленным» асинхронным чтением можно заметить, что более 256 КБ Windows выполняет каждый асинхронный запрос асинхронно. Чем меньше размер блока, тем больше запросов обслуживается "немедленно", даже если они недоступны немедленно (т. е. ReadFile просто запускается синхронно). Я не могу выделить четкую закономерность (например, «первые 100 запросов» или «более 1000 запросов»), но, похоже, существует обратная корреляция между размером запроса и синхронностью. При размере блока 8 КБ каждый асинхронный запрос обслуживается синхронно.
Буферизованная синхронная передача по какой-то причине в два раза быстрее асинхронной передачи (не знаю почему), следовательно, чем меньше размер запроса, тем быстрее общая передача, потому что больше передач выполняется синхронно.

Для предварительной ошибки с отображением памяти FILE_FLAG_SEQUENTIAL_SCAN приводит к немного другой форме графика производительности (есть «выемка», которая смещена немного назад), но общее затраченное время точно идентично (опять же, это удивительно, но я не могу Помоги).

Обновление 2:

Небуферизованный ввод-вывод делает графики производительности для тестовых случаев 1M, 4M и 512k запросов несколько выше и более «остроконечными» с максимумами в 90 ГБ/с, но также и с жесткими минимумами, общее время выполнения для 1 ГБ находится в пределах +/- 0,5. s буферизованного запуска (однако запросы с меньшим размером буфера выполняются значительно быстрее, потому что при более чем 2558 текущих запросах возвращается ERROR_WORKING_SET_QUOTA). Измеренное использование ЦП равно нулю во всех случаях без буферизации, что неудивительно, поскольку любой ввод-вывод выполняется через DMA.

Еще одно очень интересное наблюдение с FILE_FLAG_NO_BUFFERING заключается в том, что он значительно меняет поведение API. CancelIO больше не работает, по крайней мере, в смысле отмены ввода-вывода. С небуферизованными запросами в процессе выполнения CancelIO будет просто блокироваться до тех пор, пока все запросы не будут выполнены. Юрист, вероятно, возразит, что функция не может быть привлечена к ответственности за пренебрежение своими обязанностями, потому что, когда она возвращается, больше не остается запросов в процессе, поэтому в некотором роде она выполнила то, о чем просили, но мое понимание «отмены» несколько отличается.
С буферизованным, перекрывающимся вводом-выводом CancelIO просто перережет веревку, все текущие операции завершатся немедленно, как и следовало ожидать.

Еще одна забавная вещь заключается в том, что процесс неубиваем до тех пор, пока все запросы не завершатся или не завершатся ошибкой. Это имеет смысл, если ОС выполняет DMA в этом адресном пространстве, но, тем не менее, это потрясающая «функция».


person Damon    schedule 06.05.2011    source источник
comment
Ну, а что именно ты хотел бы увидеть? Важнейший фрагмент кода, по сути, не намного больше, чем for(...) ReadFile(...);. Или, в случае отображения памяти, файл for(...) sum += data[i];. Это плюс CloseHandle(CreateFile(... NO_BUFFERING)) в начале для очистки кеша.   -  person Damon    schedule 06.05.2011
comment
Мне было интересно, делаете ли вы что-нибудь «интересное», когда выдаете свои асинхронные чтения...   -  person Len Holgate    schedule 06.05.2011
comment
Буферный кеш очищался перед запуском тестов — как? (Только кеш записи можно сбросить напрямую, поэтому вы перезагружаетесь или читаете огромный файл между тестированием кэшированного файла?) Кроме того, какую версию Windows вы использовали? Pre-Vista может выполнять только 64 КБ операций ввода-вывода на вызов ядра (iirc), увеличено до (iirc) 4 мегабайта для Vista и 64 мегабайта для сервера 2008 года.   -  person snemarch    schedule 07.05.2011
comment
@snemarch: открытие файла с флагом NO_BUFFERING приведет к удалению всего содержимого кеша для этого файла. Не для этой ручки, а для всех, глобально (по крайней мере под XP, под Win 7 не тестил). Это абсолютно не соответствует документации, но так и происходит (неприятный сюрприз, если не ожидать). Моя система — Windows XP SP3 (профессиональная).   -  person Damon    schedule 08.05.2011
comment
вау, это самое длинное ТАКОЕ, что я когда-либо читал, здорово! ;)   -  person rogerdpack    schedule 15.10.2012


Ответы (1)


Я не эксперт по файловой системе, но я думаю, что здесь происходит несколько вещей. Прежде всего. w.r.t. ваш комментарий о картировании памяти стал победителем. В этом нет ничего удивительного, поскольку менеджер кэша NT основан на отображении памяти — выполняя отображение памяти самостоятельно, вы дублируете поведение менеджера кэша без дополнительных копий памяти.

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

Вы пытались добавить FILE_FLAG_SEQUENTIAL_SCAN к своим флагам CreateFile? Это указывает префетчеру быть еще более агрессивным.

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

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

person ReinstateMonica Larry Osterman    schedule 06.05.2011
comment
Я не пробовал FILE_FLAG_SEQUENTIAL_SCAN, потому что последовательная подсказка обычно имеет коннотацию агрессивной предварительной выборки, чтения один раз, выброса, и мое намерение было скорее не заботиться, не заботиться, хранить в кэше - но после прочтения MSDN это не похоже, это не так (по крайней мере, явно) для Windows. Я попробую, имеет ли это какое-то значение, позже сегодня вечером просто из любопытства (не думаю, что это действительно будет иметь большое значение). Что касается невыполненных запросов, то запросы подавались максимально быстро, один за другим. Итак, пример 4Mx256 будет иметь 255... - person Damon; 06.05.2011
comment
... невыполненные запросы до завершения первого. Версия 16x65536 (которая работает в два раза быстрее), конечно, будет иметь гораздо больше невыполненных запросов, но я полагаю, что нескольких сотен должно быть достаточно. Что касается FILE_FLAG_NO_BUFFERING, это полная противоположность тому, что я хочу. :-) Это правда, что FILE_FLAG_NO_BUFFERING напрямую обращается к прямому доступу к памяти с диска в ваше приложение, но меня совсем не интересуют данные. Кроме того, чтобы это работало, FILE_FLAG_NO_BUFFERING не только не использует буферы, но также отбрасывает любые данные этого файла, уже находящиеся в буферах (глобально). - person Damon; 06.05.2011
comment
В моем случае предназначено чтение данных с диска в буферы (он только случайно копирует в пространство приложения, как неизбежный побочный эффект). Это звучит странно, я знаю :-) Причина в том, что пользователь вводит некоторые данные в начале, что занимает 5-8 секунд, а затем приложение должно загрузить большой квазислучайный объем этого файла. Если файл находится в буферном кеше, загрузка происходит мгновенно, в противном случае это занимает около 15-20 секунд. Отсюда идея о том, что приложение может спекулятивно предугадывать буферы, пока пользователь печатает. - person Damon; 06.05.2011
comment
Меня просто сбивает с толку тот факт, что в основном сказать ОС, что мне нужны эти несколько огромных блоков смежных данных, медленнее, чем сказать ей, что мне нужны эти тысячи крошечных битов (которые могут быть разбросаны по всему диску), и это все еще медленнее, чем просто вызвать больше ну или менее предсказуемые сбои страниц... Что ж, посмотрим, что выдаст FILE_FLAG_SEQUENTIAL_SCAN дома, в любом случае интересно посмотреть :-) - person Damon; 06.05.2011
comment
Это не должно удивлять. Когда вы запрашиваете большие операции чтения (особенно без указания file_flag_sequential_scan), вы, по сути, боретесь с менеджером кеша — менеджер кеша оптимизирован для относительно небольших (64 КБ, 256 КБ) операций ввода-вывода, и когда вы выполняете большие операции ввода-вывода, могут возникнуть проблемы. сбивает с толку. Вот почему переход к небуферизованному вводу-выводу может быть такой огромной победой — диспетчер кеша не будет мешать. - person ReinstateMonica Larry Osterman; 07.05.2011
comment
Конечно, переход на небуферизованный ввод-вывод может быть проблемой — отчасти причина того, что производительность копирования файлов в Vista упала, заключалась в том, что они переключились на использование небуферизованного ввода-вывода, что снизило производительность копирования небольших файлов (хотя это очень помогло при больших размерах). файлы). - person ReinstateMonica Larry Osterman; 07.05.2011
comment
@Larry Osterman: обновлен Q для FILE_FLAG_SEQUENTIAL_SCAN (см. выше). - person Damon; 07.05.2011
comment
Интересный. Кстати, причина, по которой возвраты синхронны при кэшированном вводе-выводе, заключается в том, что запрос может быть полностью удовлетворен из кэша. Если запрос может быть удовлетворен из кеша, нет смысла делать запрос асинхронным (это будет медленнее) - person ReinstateMonica Larry Osterman; 08.05.2011
comment
@Larry Osterman: Да, так и задумано, это тоже имеет смысл, когда данные находятся в кеше. Как ни странно, он по-прежнему работает синхронно, как упоминалось в приведенном выше случае, когда данные явно не в кеше (это объясняет, почему отправка запросов тоже занимает несколько секунд). Но... это все же быстрее, чем асинхронный запуск. Это своего рода задом наперед объясняет, почему отображение памяти также быстрее (потому что это, очевидно, тоже синхронно, процесс блокируется при сбое страницы). Или, может быть, правильнее смотреть на это так, синхронные чтения - это не что иное, как замаскированные ошибки страниц. - person Damon; 08.05.2011
comment
Пробовали ли вы повторить этот эксперимент с перекрывающимся небуферизованным вводом-выводом? Как я уже сказал в предыдущем ответе, насколько я понимаю, вы получите наилучшую производительность при работе с большими файлами. - person ReinstateMonica Larry Osterman; 08.05.2011
comment
@Larry Osterman: Я сделал это из научного любопытства (хотя, как я уже сказал, небуферизованный ввод-вывод - это полная противоположность тому, что я хочу; он действительно не подходит для 99,9% всех людей). Небуферизованный ввод-вывод делает графики производительности для тестовых случаев 1M, 4M и 512k запросов несколько выше и более остроконечными с максимумами в 90 ГБ/с, но также и с жесткими минимумами, общее время выполнения для 1 ГБ находится в пределах +/- 0,5 с от буферизованный запуск (однако запросы с меньшим размером буфера выполняются значительно быстрее, потому что при более чем 2558 текущих запросах возвращается ERROR_WORKING_SET_QUOTA). - person Damon; 08.05.2011
comment
@Damon: Вы правы в том, что небуферизованный режим не подходит для определенных рабочих нагрузок. Но буферизованный ввод-вывод добавляет к миксу дополнительную копию, и эта дополнительная копия потенциально может снизить производительность. При небуферизованном вводе-выводе файловая система копирует файл непосредственно в буфер приложения без предварительного прохождения через кеш. С другой стороны, я считаю, что для определенных больших объемов чтения ввод-вывод фактически превращается в небуферизованный ввод-вывод, потому что накладные расходы на кэширование слишком велики. - person ReinstateMonica Larry Osterman; 08.05.2011
comment
Вы также измеряли использование ЦП во время этих операций? Я ожидаю, что для небольших чтений загрузка ЦП будет выше (поскольку вы копируете из кеша), а для больших чтений я ожидаю, что загрузка ЦП будет равна нулю (поскольку вы тратите все время на ожидание ввода/вывода). О завершить). - person ReinstateMonica Larry Osterman; 08.05.2011
comment
@Larry Osterman: измеренное использование ЦП равно нулю во всех случаях без буферизации, что неудивительно. Чтение больших блоков использует DMA, а чтение малых блоков не удается, поэтому они ничего не делают. Основная причина, по которой я считаю небуферизованный ввод-вывод непригодным, заключается в том, что он небуферизован. Buffered IO делает дополнительные копии, правда, но они незначительны. Кажется, есть некоторые странные фиксированные служебные эффекты (за которые я виню блокировку из-за отсутствия лучшего объяснения), но кроме этого, лучшее, что может случиться, это то, что ваши данные находятся в буферах. Единственным исключением является действительно редкий случай, когда вы хотите... - person Damon; 09.05.2011
comment
... поток с минимально возможной задержкой и с минимально возможной загрузкой процессора (возможно, с более негибкой среды), и вам действительно, действительно нужны данные только один раз и никогда больше. Воспроизведение фильма с DVD было бы одной из таких вещей. В любой другой ситуации буферы — это большая победа. Чтение из буферов не занимает много времени. Вот почему я вообще реализовал этот предварительный отказ. Утверждение, что данные находятся в буферах, — это своего рода тривиальная оптимизация, которая делает ваше приложение с большими объемами данных в 20-25 раз быстрее, не изменяя ни одной строки фактического кода. - person Damon; 09.05.2011
comment
@Larry Osterman: Скопировал некоторые из приведенных выше комментариев в Q, чтобы облегчить последующее чтение, также я нашел еще две интересные детали о небуферизованном вводе-выводе. - person Damon; 09.05.2011