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



    Привет, Хабр! Некоторое время назад меня заинтересовал вопрос: как эффективнее всего читать данные с диска (при условии, что у вас .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 доступ к файлам как минимум уменьшит вероятность падений и увеличит скорость.
    Технологический Центр Дойче Банка
    91,85
    Компания
    Поддержать автора
    Поделиться публикацией

    Похожие публикации

    Комментарии 18

      +7

      По-моему целесообразно убрать лидирующие нули в таблицах и отображать время в миллисекундах.

        +1

        Согласен, исправлю чуть позже

        +3

        Я все ждал, когда же в статье будет сравнение RandomAccess и SequentialScan, но его нет :(
        Кстати, размеры буферов для чтения тоже влияют на производительность.


        Сравнение количества потоков и используемого API (синхронное против асинхронного) лишь показывает, что мы в итоге упремся в ограничение роста производительности, т.е. закон Амдала. (кстати, тесты с 64/128/256 потоками наглядно это демонстрируют).

          0
          1. ScenarioAsync2 00:00:00.0670000 в SSD для Минимальный размер файла (байты): 1001 почему не жирное?
          2. Сравнение миллисекунд — неблагодарное дело. Было бы здорово, если бы тесты работали хотя бы секунду, а то и десяток.
          3. OOM не должно быть ни при каком раскладе. Вы складируете всё прочитанное в память? Тогда на время повлияет уборка мусора. Может быть, стоит складировать всё время в один и тот же byte[], если не ReadAllText?
          4. Для больших файлов, разбросанных по диску (видео, например), последовательное чтение должно быть существенно быстрее на HDD, в предположении о дефрагментированном харде.
          5. Использование существующих файлов на харде — плохая идея, имхо. Доступы к ним со стороны системы непредсказуемы. Лучше создать некоторую тестовую папку с таким набором файлов, который Вам интересен. Минус в том, что они все создаются примерно одновременно и вряд ли оказываются размазанными. Но, с другой стороны, в реальных условиях программа тоже устанавливается за ограниченное время.
            0
            1. Моя промашка, абсолютно согласен, спасибо!
            2. "Сравнение миллисекунд" — да, согласен. Результаты у меня стабильно воспроизводились, потому я оставил именно миллисекунды.
            3. И всё-таки, он получался. По моим предположениям (субъективным), всё это было от большого числа запущенных операций (и большого размера файлов), т.е. все объекты были действительно достижимы. Вы правы, для избежания подобного поведения стоило как минимум обрабатывать файлы блоками (например, используя Microsoft.IO.RecyclableMemoryStream. Здесь же моя идея была следующая: сравнить скорость, используя простую логику чтения (т.е. ту, которую чаще всего будет использовать разработчик при стандартных задачах чтения с диска). Я уверен, что при разработке IO-bound приложений используется немало оптимизаций, все они за рамками моих замеров
            4. Я запускал дефрагментацию заранее на HDD. Если есть силы и время — Вы можете запустить тест на своем компьютере, для этого достаточно сделать git pull и запустить проект TestHost. У меня теория не воспроизвелась.
            5. Возможно. Моё основное предположение — сделать замер скорости чтения случайного набора файлов. Т.е. когда они обновляются в разное время, а значит дефрагментация может постепенно разнести их по диску.
            +2
            В асинхронных тестах вы открываете файл с помощью вызова File.OpenRead(), а этот вызов создаёт FileStream в синхронном режиме. В итоге тестируется работа ThreadPool, а не асинхронного ввода-вывода. Попробуйте повторить асинхронные тесты, создав явно FileStream с параметром useAsync = true.
              0

              Интересный момент, окей, проверю чуть позже

                0
                а ответа нет? удалось проверить?
              +2

              Если вы не знакомы с BenchmarkDotNet — самое время познакомиться. Скорее всего, при общей простоте использования, он более грамотно замерит "миллисекунды". И — нет, я не Андрей Акиньшин. :)

                +1

                Да, я полностью согласен, однако я начал делать все замеры в марте 2016 года, когда BenchmarkDotNet от DreamWalker 'а была менее популярна.
                Сейчас я бы основывал свои проверки именно на ней.

                0
                У меня вот похуже задача сейчас есть… Надо не просто много файлов быстро прочитать, а перебрать содержимое папки со подпапками, причем внутри более миллиона файлов. Процесс занимает более получаса.
                Задача в том, чтобы найти новые файлы (которых еще нет в базе) и посчитать для них хеши по алгоритму. Но всё упирается именно в задачу перебора всех файлов в папке, чтобы найти именно новые
                  0
                  Можно сделать отдельный каталог для появления новых файлов, из которого скрипт будет переносить в каталоги с кучей файлов попутно отмечая в базе.
                    0
                    найти новые файлы
                    Чем плох FileSystemWatcher?
                      +1
                      Так мне не надо в реальном времени-то! Просто иногда запускаю процесс, который перебирает файлы и добавляет в базу новые, но далеко не каждый день это требуется делать.
                      Ну даже если использовать FileSystemWatcher — мне это не особо поможет, т.к. программа же все равно не будет знать о том, что между ее запусками какие-то файлы в папке поменяли/добавили. Т.е. мы опять возвращаемся к тому, что надо перебрать весь список файлов во всех подпапках папки, а этот процесс занимает более получаса — причем без чтения содержимого файлов! Фактически, мне нужно имя, путь и размер файла. При этом я уже и так отказался от использования FileInfo, оказалось, что для определения размера быстрее всего — открыть файл на чтение, как-то так:

                      using (var file = new FileStream(fn, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                      {
                          return file.Length;
                      }
                      


                      Судя по коду FileInfo, она запрашивает больше данных о файле и сильнее грузит жесткий диск в итоге, что при миллионе+ файлов уже в разы замедляет процесс скана.
                        +2
                        Программа Everything очень быстро ищет файлы, читая лог файловой системы. То есть прочитать лог быстрее, чем найти файлы в каталоге. Может вам стоит посмотреть в сторону работы с логом ($LogFile, $UsnJrnl of NTFS).
                          0
                          Ну, видимо это единственный вариант. Хотя это будет тот еще квест :)
                          Ну и тут может возникнуть другая проблема — размер MTF у меня 10,12 Гб…

                          P.S.: Я так и не нашел в .net других функций, которые бы не базировались бы в итоге на findfirst/findnext со всеми вытекающими из этого проблемами.
                            0

                            Кстати, если хочешь сделать совсем четко и реактивно, то использую Filter Driver. Вот ссылка на хабр.


                            И картинка оттуда

                            image

                              +2
                              Ну это опять больше для риалтайма нужно… Да и тут C# уже не поможет и надо будет на C++ делать, со всеми вытекающими проблемами

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое