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

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

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

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

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

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

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

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

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

Для большей наглядности:
enter image description here

Обновление:

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

enter image description here

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

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

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

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

18
задан Community 23 May 2017 в 10:27
поделиться