(извините за несколько длинное вступление)
Во время разработки приложения, которое предварительно загружает весь большой файл (> 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 в этом адресном пространстве, но, тем не менее, это потрясающая «функция».
for(...) ReadFile(...);
. Или, в случае отображения памяти, файлfor(...) sum += data[i];
. Это плюсCloseHandle(CreateFile(... NO_BUFFERING))
в начале для очистки кеша. - person Damon   schedule 06.05.2011