Как эффективнее читать данные с диска (при условии, что у вас .Net)

в 7:12, , рубрики: .net, async, asyncawait, await, comparison, hdd, io bound, io operation, microsoft .net, performance comparison, ssd, task, thread, threading, windows, Блог компании Технологический Центр Дойче Банка, Программирование, разработка под windows, сценарии, Тестирование IT-систем, чтение данных, чтение данных с диска

Как эффективнее читать данные с диска (при условии, что у вас .Net) - 1

Привет!

Некоторое время назад меня заинтересовал вопрос: как эффективнее всего читать данные с диска (при условии, что у вас .Net)? Задача чтения кучи файлов встречается во множестве программ, которые при самом старте начинают вычитывать конфигурации, некоторые самостоятельно подгружают модули и т.д.

В интернете я не нашел подобных сравнений (если не считать тюнинга под определенные конфигурации).

Результаты можно посмотреть на GithubSSDHDD.

Способы чтения и алгоритм тестирования

Есть несколько основных способов:

Тестировал я все на SSD и HDD (в первом случае был компьютер с Xeon 24 cores и 16 Гб памяти и Intel SSD, во втором — Mac Mini MGEM2LL/A с Core i5, 4 Гб RAM и HDD 5400-rpm). Системы такие, чтобы по результатам можно было бы понять, как лучше вести себя на относительно современных системах и на не очень новых.

Проект можно посмотреть здесь, он представляет собой один главный исполняемый файл TestsHost и кучу проектов с названиями Scenario*. Каждый тест это:

  1. Запуск exe-файла, который посчитает чистое время.
  2. Раз в секунду проверяется нагрузка на процессор, потребление оперативной памяти, нагрузка на диск и еще ряд производных параметров (с помощью Performance Counters).
  3. Результат запоминается, тест повторяется несколько раз. Итоговый результат работы — это среднее время, без учета самых больших и самых малых значений.

Подготовка к тесту более хитрая. Итак, перед запуском:

  1. Определяемся с размером файлов и с их числом (я выбрал такие, чтобы суммарный объем был больше, чем объем RAM, чтобы подавить влияние дискового кеша);
  2. Ищем на компьютере файлы заданного размера (а заодно игнорируем недоступные файлы и еще ряд спецпапок, про которые написано ниже);
  3. Запускаем один из тестов на наборе файлов, игнорируем результат. Все это нужно для того, чтобы сбросить кеш ОС, убрать влияние от предыдущих тестов и просто прогреть систему.

И не забываем про обработку ошибок:

  1. Программа выдаст код возврата 0 только в случае, если все файлы были прочитаны.
  2. Иногда весь тест падает, если вдруг система начинает активно читать файл. Вздыхаем и перезапускаем еще раз, добавляя файл (или папку) в игнорируемые. Так как я использовал каталоги Windows & Program Files как хороший источник файлов, наиболее реалистично размазанный по диску, некоторые файлы могли быть ненадолго заблокированы.
  3. Иногда один Performance Counter мог выдать ошибку, так как процесс, например, уже начал завершаться. В этом случае игнорируются все счетчики за эту секунду.
  4. На больших файлах некоторые тесты стабильно выдавали Out Of Memory исключения. Их я убрал из результатов.

И плюс стандартные моменты про нагрузочное тестирование:

  1. Компиляция — в режиме Release в MSVS. Запуск идет как отдельное приложение, без отладчика и пр. Нет какого-то тюнинга, ведь суть проверок именно в том — как в обыкновенном ПО читать файлы быстрее.
  2. Антивирус отключен, обновление системы остановлено, активные программы остановлены тоже. Больше никаких тюнингов не было, по той же причине.
  3. Каждый тест — это запуск отдельного процесса. Overhead получился в рамках погрешности (т.е. jit, траты на старт процесса и пр.), а потому я оставил именно такую изоляцию.
  4. Некоторые Performance Counters выдавали нулевой результат всегда для HDD/SSD. Так как набор счетчиков вшит в программу, я их оставил.
  5. Все программы запускались как x64, попытка сделать swap означала неэффективность по памяти и сразу же уходила вниз в статистике из-за большого времени работы.
  6. Thread Priority и пр. тюнинги не использовались, так как не было попыток выжать именно максимум (который будет сильно зависеть от намного большего числа факторов).
  7. Технологии: .Net 4.6, x64

Результаты

Как я уже написал в шапке, результаты есть на GithubSSDHDD.

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

Увеличив минимальный размер файла, я получил:

  1. В лидерах остался запуск программы с числом потоков, близким к числу ядер процессоров.
  2. В ряде тестов один из потоков постоянно ждал освобождение блокировки (см. Performance Counter «Concurrent Queue Length»).
  3. Синхронный способ чтение с диска все еще в аутсайдерах.

Минимальный размер файла (байты): 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 Кб, их число меньше, однако сами они больше. И как результат:

  1. Если не ограничивать число потоков, то время чтения становится ближе к синхронным операциям
  2. Ограничивать уже желательнее как (число ядер) * [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 доступ к файлам как минимум уменьшит вероятность падений и увеличит скорость.

Автор: imanushin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js