Как эффективнее читать данные с диска (при условии, что у вас .Net) +17
Программирование, Разработка под Windows, .NET, Тестирование IT-систем, Блог компании Технологический Центр Дойче Банка
Рекомендация: подборка платных и бесплатных курсов Java - https://katalog-kursov.ru/
Привет, Хабр! Некоторое время назад меня заинтересовал вопрос: как эффективнее всего читать данные с диска (при условии, что у вас .Net)? Задача чтения кучи файлов встречается во множестве программ, которые при самом старте начинают вычитывать конфигурации, некоторые самостоятельно подгружают модули и т.д.
В интернете я не нашел подобных сравнений (если не считать тюнинга под определенные конфигурации).
Результаты можно посмотреть на Github: SSD, HDD.
Способы чтения и алгоритм тестирования
Есть несколько основных способов:
Тестировал я все на SSD и HDD (в первом случае был компьютер с Xeon 24 cores и 16 Гб памяти и
Intel SSD, во втором — Mac Mini MGEM2LL/A с Core i5, 4 Гб RAM и HDD 5400-rpm). Системы такие, чтобы по результатам можно было бы понять, как лучше вести себя на относительно современных системах и на не очень новых.
Проект можно посмотреть
здесь, он представляет собой один главный исполняемый файл
TestsHost и кучу проектов с названиями Scenario*. Каждый тест это:
- Запуск exe-файла, который посчитает чистое время.
- Раз в секунду проверяется нагрузка на процессор, потребление оперативной памяти, нагрузка на диск и еще ряд производных параметров (с помощью Performance Counters).
- Результат запоминается, тест повторяется несколько раз. Итоговый результат работы — это среднее время, без учета самых больших и самых малых значений.
Подготовка к тесту более хитрая. Итак, перед запуском:
- Определяемся с размером файлов и с их числом (я выбрал такие, чтобы суммарный объем был больше, чем объем RAM, чтобы подавить влияние дискового кеша);
- Ищем на компьютере файлы заданного размера (а заодно игнорируем недоступные файлы и еще ряд спецпапок, про которые написано ниже);
- Запускаем один из тестов на наборе файлов, игнорируем результат. Все это нужно для того, чтобы сбросить кеш ОС, убрать влияние от предыдущих тестов и просто прогреть систему.
И не забываем про обработку ошибок:
- Программа выдаст код возврата 0 только в случае, если все файлы были прочитаны.
- Иногда весь тест падает, если вдруг система начинает активно читать файл. Вздыхаем и перезапускаем еще раз, добавляя файл (или папку) в игнорируемые. Так как я использовал каталоги Windows & Program Files как хороший источник файлов, наиболее реалистично размазанный по диску, некоторые файлы могли быть ненадолго заблокированы.
- Иногда один Performance Counter мог выдать ошибку, так как процесс, например, уже начал завершаться. В этом случае игнорируются все счетчики за эту секунду.
- На больших файлах некоторые тесты стабильно выдавали Out Of Memory исключения. Их я убрал из результатов.
И плюс стандартные моменты про нагрузочное тестирование:
- Компиляция — в режиме Release в MSVS. Запуск идет как отдельное приложение, без отладчика и пр. Нет какого-то тюнинга, ведь суть проверок именно в том — как в обыкновенном ПО читать файлы быстрее.
- Антивирус отключен, обновление системы остановлено, активные программы остановлены тоже. Больше никаких тюнингов не было, по той же причине.
- Каждый тест — это запуск отдельного процесса. Overhead получился в рамках погрешности (т.е. jit, траты на старт процесса и пр.), а потому я оставил именно такую изоляцию.
- Некоторые Performance Counters выдавали нулевой результат всегда для HDD/SSD. Так как набор счетчиков вшит в программу, я их оставил.
- Все программы запускались как x64, попытка сделать swap означала неэффективность по памяти и сразу же уходила вниз в статистике из-за большого времени работы.
- Thread Priority и пр. тюнинги не использовались, так как не было попыток выжать именно максимум (который будет сильно зависеть от намного большего числа факторов).
- Технологии: .Net 4.6, x64
Результаты
Как я уже написал в шапке, результаты есть на
Github:
SSD,
HDD.
SSD диск
Минимальный размер файла (байты): 2, максимальный размер (байты): 25720320, средний размер (байты): 40953.1175
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.2260000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.5080000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.1120000
|
ScenarioAsyncWithMaxParallelCount24
|
00:00:00.1540000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.2510000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.5240000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.5970000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.7610000
|
ScenarioSyncAsParallel
|
00:00:00.9340000
|
ScenarioReadAllAsParallel
|
00:00:00.3360000
|
ScenarioAsync
|
00:00:00.8150000
|
ScenarioAsync2
|
00:00:00.0710000
|
ScenarioNewThread
|
00:00:00.6320000
|
Итак, при чтении множества мелких файлов два победителя — асинхронные операции. На деле в обоих случаях .Net использовал 31 поток.
По сути обе программы различались наличием или отсутствием ActionBlock для ScenarioAsyncWithMaxParallelCount32 (с ограничением), в итоге получилось, что чтение лучше не ограничивать, тогда будет использоваться больше памяти (в моем случае в 1,5 раза), а ограничение будет просто на уровне стандартных настроек (т.к. Thread Pool зависит от числа ядер и т.д.)
Минимальный размер файла (байты): 1001, максимальный размер (байты): 25720320, средний размер (байты): 42907.8608
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.4070000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.2210000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.1240000
|
ScenarioAsyncWithMaxParallelCount24
|
00:00:00.2430000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.3180000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.5100000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.7270000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.8190000
|
ScenarioSyncAsParallel
|
00:00:00.7590000
|
ScenarioReadAllAsParallel
|
00:00:00.3120000
|
ScenarioAsync
|
00:00:00.5080000
|
ScenarioAsync2
|
00:00:00.0670000
|
ScenarioNewThread
|
00:00:00.6090000
|
Увеличив минимальный размер файла, я получил:
- В лидерах остался запуск программы с числом потоков, близким к числу ядер процессоров.
- В ряде тестов один из потоков постоянно ждал освобождение блокировки (см. Performance Counter «Concurrent Queue Length»).
- Синхронный способ чтение с диска все еще в аутсайдерах.
Минимальный размер файла (байты): 10007, максимальный размер (байты): 62 444 171, средний размер (байты): 205102.2773
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.6830000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.5440000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.6620000
|
ScenarioAsyncWithMaxParallelCount24
|
00:00:00.8690000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.5630000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.2050000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.1600000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.4890000
|
ScenarioSyncAsParallel
|
00:00:00.7090000
|
ScenarioReadAllAsParallel
|
00:00:00.9320000
|
ScenarioAsync
|
00:00:00.7160000
|
ScenarioAsync2
|
00:00:00.6530000
|
ScenarioNewThread
|
00:00:00.4290000
|
И последний тест для SSD: файлы от 10 Кб, их число меньше, однако сами они больше. И как результат:
- Если не ограничивать число потоков, то время чтения становится ближе к синхронным операциям
- Ограничивать уже желательнее как (число ядер) * [2.5 — 5.5]
HDD диск
Если с SSD все было более-менее хорошо, здесь у меня участились падения, так что часть результатов с упавшими программами я исключил.
Минимальный размер файла (байты): 1001, максимальный размер (байты): 54989002, средний размер (байты): 210818,0652
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.3410000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.3050000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.2470000
|
ScenarioAsyncWithMaxParallelCount24
|
00:00:00.1290000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.1810000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.1940000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.4010000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.5170000
|
ScenarioSyncAsParallel
|
00:00:00.3120000
|
ScenarioReadAllAsParallel
|
00:00:00.5190000
|
ScenarioAsync
|
00:00:00.4370000
|
ScenarioAsync2
|
00:00:00.5990000
|
ScenarioNewThread
|
00:00:00.5300000
|
Для мелких файлов в лидерах опять асинхронное чтение. Однако и синхронная работа тоже показала неплохой результат. Ответ кроется в нагрузке на диск, а именно — в ограничении параллельных чтений. При попытке принудительно начать читать во много потоков система упирается в большую очередь на чтение. В итоге вместо параллельной работы время тратится на попытки параллельно обслужить много запросов.
Минимальный размер файла (байты): 1001, максимальный размер (байты): 54989002, средний размер (байты): 208913,2665
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.6880000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.2160000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.5870000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.5700000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.5070000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.4060000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.4800000
|
ScenarioSyncAsParallel
|
00:00:00.4680000
|
ScenarioReadAllAsParallel
|
00:00:00.4680000
|
ScenarioAsync
|
00:00:00.3780000
|
ScenarioAsync2
|
00:00:00.5390000
|
ScenarioNewThread
|
00:00:00.6730000
|
Для среднего размера файлов асинхронное чтение продолжало показывать лучший результат, разве что число потоков желательно ограничивать еще меньшим значением.
Минимальный размер файла (байты): 10008, максимальный размер (байты): 138634176, средний размер (байты): 429888,6019
Сценарий
|
Время
|
ScenarioAsyncWithMaxParallelCount4
|
00:00:00.5230000
|
ScenarioAsyncWithMaxParallelCount8
|
00:00:00.4110000
|
ScenarioAsyncWithMaxParallelCount16
|
00:00:00.4790000
|
ScenarioAsyncWithMaxParallelCount24
|
00:00:00.3870000
|
ScenarioAsyncWithMaxParallelCount32
|
00:00:00.4530000
|
ScenarioAsyncWithMaxParallelCount64
|
00:00:00.5060000
|
ScenarioAsyncWithMaxParallelCount128
|
00:00:00.5810000
|
ScenarioAsyncWithMaxParallelCount256
|
00:00:00.5540000
|
ScenarioReadAllAsParallel
|
00:00:00.5850000
|
ScenarioAsync
|
00:00:00.5530000
|
ScenarioAsync2
|
00:00:00.4440000
|
Опять в лидерах асинхронное чтение с ограничением на число параллельных операций. Причем, рекомендуемое число потоков стало еще меньше. А параллельное синхронное чтение стабильно стало показывать Out Of Memory.
При большем увеличении размера файла сценарии без ограничения на число параллельных чтений чаще падали с Out Of Memory. Так как результат не был стабильным от запуска к запуску, подобное тестирование я уже счел нецелесообразным.
Итог
Какой же результат можно почерпнуть из этих тестов?
- Почти во всех случаях асинхронное чтение, по сравнению с синхронным, давало лучший результат по скорости.
- При росте размера файла целесообразно ограничивать число потоков, так как иначе чтение будет медленным, плюс повысится риск OOM.
- Во всех случаях не было радикально большого прироста в производительности, максимум — в 2-3 раза. А потому возможно, что переписывать старое legacy приложение на асинхронное чтение не стоит.
- Однако для новых программ async доступ к файлам как минимум уменьшит вероятность падений и увеличит скорость.
-->
К сожалению, не доступен сервер mySQL