С# I/O Parallelism увеличивает производительность с помощью SSD?

Я прочитал несколько ответов (для пример) здесь, в SO, где некоторые говорят, что parallelism не будет увеличивать производительность (возможно, при чтении IO).

Но я создал несколько тестов, которые показывают, что операции WRITE выполняются намного быстрее.

- READ TEST:

Я создал случайные 6000 файлов с фиктивными данными:

введите описание изображения здесь

Попробуйте прочитать их w/w/o parallelism:

var files =
    Directory.GetFiles("c:\\temp\\2\\", "*.*", SearchOption.TopDirectoryOnly).Take(1000).ToList();

    var sw = Stopwatch.StartNew();
    files.ForEach(f => ReadAllBytes(f).GetHashCode()); 
    sw.ElapsedMilliseconds.Dump("Run READ- Serial");
    sw.Stop(); 


    sw.Restart();
    files.AsParallel().ForAll(f => ReadAllBytes(f).GetHashCode()); 
    sw.ElapsedMilliseconds.Dump("Run READ- Parallel");
    sw.Stop();

Result1:

Запустить READ-Serial 595

Запустить READ-Parallel 193

Результат2:

Запустить READ-Serial 316

Запустить READ-Parallel 192

- ИСПЫТАНИЕ WRITE:

Переход на создание 1000 случайных файлов, где каждый файл 300K. (Я освободил каталог из теста prev)

введите описание изображения здесь

var bytes = new byte[300000];
Random r = new Random();
r.NextBytes(bytes);
var list = Enumerable.Range(1, 1000).ToList();

sw.Restart();
list.ForEach((f) => WriteAllBytes(@"c:\\temp\\2\\" + Path.GetRandomFileName(), bytes)); 
sw.ElapsedMilliseconds.Dump("Run WRITE serial");
sw.Stop();

sw.Restart();
list.AsParallel().ForAll((f) => WriteAllBytes(@"c:\\temp\\2\\" + 
Path.GetRandomFileName(), bytes)); 
sw.ElapsedMilliseconds.Dump("Run  WRITE Parallel");
sw.Stop();

Результат 1:

Запустить WRITE serial 2028

Запустить WRITE Parallel 368

Результат 2:

Запустить WRITE serial 784

Запустить WRITE Parallel 426

Вопрос:

Результаты меня удивили. Понятно, что против всех ожиданий (особенно с операциями WRITE) - производительность лучше с parallelism, но с операциями ввода-вывода.

Как/Почему результаты parallelism лучше? Похоже, что SSD может работать с потоками и что нет или меньше узких мест при запуске более одного задания за раз в устройстве ввода-вывода.

Nb Я не тестировал его с HDD (я буду рад, что тот, у которого есть HDD, будет запускать тесты.)

Ответы

Ответ 1

Бенчмаркинг - это сложное искусство, вы просто не измеряете то, что считаете себя. То, что на самом деле не накладные расходы ввода-вывода несколько очевидны из результатов теста, почему один многопоточный код быстрее во второй раз, когда вы его запускаете?

То, на что вы не рассчитываете, - это поведение кеша файловой системы. Он хранит копию содержимого диска в ОЗУ. Это особенно сильно влияет на многопоточное измерение кода, он вообще не использует никаких операций ввода-вывода. В двух словах:

  • Чтение происходит из ОЗУ, если кэш файловой системы имеет копию данных. Это работает на скоростях шины памяти, как правило, около 35 гигабайт в секунду. Если у него нет копии, считывание задерживается до тех пор, пока диск не поместит данные. Он не просто считывает запрошенный кластер, а весь объем данных на диске с диска.

  • Записывается прямо в ОЗУ, выполняется очень быстро. Эти данные записываются на диск лениво в фоновом режиме, в то время как программа продолжает выполнять, оптимизированная для минимизации движения записи в цилиндре. Только если больше нет ОЗУ, будет запись в режиме ожидания.

Фактический размер кеша зависит от установленного объема оперативной памяти и необходимости в ОЗУ, наложенном запущенными процессами. Очень грубым ориентиром является то, что вы можете рассчитывать на 1 ГБ на машине с 4 ГБ ОЗУ, 3 ГБ на машине с 8 ГБ ОЗУ. Он отображается на вкладке "Монитор ресурсов", "Память", отображается как "Кэшированное" значение. Имейте в виду, что он сильно изменен.

Достаточно, чтобы понять, что вы видите, тест Parallel значительно отличается от теста Serial, который уже прочитал все данные. Если вы написали тест, чтобы сначала выполнить тест Parallel, вы получили бы совсем другие результаты. Только если кэш холодный, вы можете увидеть потерю перфоманса из-за резьбы. Вам необходимо перезапустить машину, чтобы обеспечить это условие. Или сначала прочитайте еще один очень большой файл, достаточно большой, чтобы вырезать полезные данные из кеша.

Только если у вас есть априорное знание вашей программы, только когда-либо прочитанные данные, которые были только что написаны, вы можете безопасно использовать потоки, не рискуя первыми потерями. Эта гарантия, как правило, довольно трудно найти. Он действительно существует, хорошим примером является создание Visual Studio вашего проекта. Компилятор записывает результат сборки в каталог obj\Debug, затем MSBuild копирует его в bin\Debug. Выглядит очень расточительно, но это не так, что копия будет всегда выполняться очень быстро, так как файл горячий в кеше. Кэш также объясняет разницу между холодом и горячим началом .NET-программы и почему использование NGen не всегда лучше.

Ответ 2

Причиной такого поведения называется Файловое кэширование, которое является функцией Windows для повышения производительности файловых операций. Давайте рассмотрим краткое объяснение в Центр Windows Dev:

По умолчанию Windows кэширует данные файла, которые считываются с дисков и записанных на диски. Это означает, что операции чтения считывают данные файла из области в системной памяти, известной как кеш системного файла, а чем с физического диска.

Это означает, что жесткий диск (обычно) никогда не использовался во время тестов.

Мы можем избежать этого поведения, создав FileStream, используя флаг FILE_FLAG_NO_BUFFERING, задокументированный в MSDN. Давайте посмотрим на нашу новую функцию ReadUnBuffered, используя этот флаг:

private static object ReadUnbuffered(string f)
{
    //Unbuffered read and write operations can only
    //be performed with blocks having a multiple
    //size of the hard drive sector size
    byte[] buffer = new byte[4096 * 10];
    const ulong FILE_FLAG_NO_BUFFERING = 0x20000000;
    using (FileStream fs = new FileStream(
        f,
        FileMode.Open,
        FileAccess.Read,
        FileShare.None,
        8,
        (FileOptions)FILE_FLAG_NO_BUFFERING))
    {
        return fs.Read(buffer, 0, buffer.Length);
    }
}

Результат: Сеанс чтения намного быстрее. В моем случае даже почти в два раза быстрее.

Чтение файла с использованием стандартного кэша Windows позволяет выполнять операции ЦПУ и ОЗУ для управления кэшированием файлов, иметь дело с FileStream,... потому что файлы уже кэшированы. Несомненно, это не очень интенсивный процессор, но он не является незначительным. Поскольку файлы уже находятся в системном кеше, параллельный подход (без изменения кэша) показывает точно время этих служебных операций.

Это поведение также может быть перенесено на операции записи.

Ответ 3

Это очень интересная тема! Мне жаль, что я не могу объяснить технические подробности, но есть некоторые проблемы, которые необходимо уладить. Это немного длиннее, поэтому я не могу вставить их в комментарий. Пожалуйста, простите меня, чтобы отправить его в качестве "ответа".

Я думаю, вам нужно подумать о больших и малых файлах, также, тест должен выполняться несколько раз и получать среднее время, чтобы убедиться, что результат подлежит проверке. Общая рекомендация заключается в том, чтобы запустить его в 25 раз, как предполагает документ в эволюционных вычислениях.

Еще одна проблема связана с системным кэшированием. Вы создали только один буфер bytes и всегда записываете одно и то же, я не знаю, как система обрабатывает буфер, но чтобы свести к минимуму разницу, я предлагаю вам создать другой буфер для разных файлов.

(Обновление: возможно, GC также влияет на производительность, поэтому я снова пересмотрел, чтобы отложить GC в любом случае.)

У меня, к счастью, есть и SSD, и HDD на моем компьютере, и переработал тестовый код. Я выполнил его с различными конфигурациями и получил следующие результаты. Надеюсь, я могу вдохновить кого-то для лучшего объяснения.

1KB, 256 файлов

Avg Write Parallel SSD: 46.88
Avg Write Serial   SSD: 94.32
Avg Read  Parallel SSD: 4.28
Avg Read  Serial   SSD: 15.48
Avg Write Parallel HDD: 35.4
Avg Write Serial   HDD: 71.52
Avg Read  Parallel HDD: 4.52
Avg Read  Serial   HDD: 14.68

512 КБ, 256 файлов

Avg Write Parallel SSD: 86.84
Avg Write Serial   SSD: 210.84
Avg Read  Parallel SSD: 65.64
Avg Read  Serial   SSD: 80.84
Avg Write Parallel HDD: 85.52
Avg Write Serial   HDD: 186.76
Avg Read  Parallel HDD: 63.24
Avg Read  Serial   HDD: 82.12
// Note: GC seems still kicked in the parallel reads on this test

Моя машина: i7-6820HQ/32G/Windows 7 Enterprise x64/VS2017 Professional/Target.NET 4.6/Работа в режиме отладки.

Два жестких диска:

C диск: IDE\Crucial_CT275MX300SSD4 ___________________ M0CR021

D: IDE\ST2000LM003_HN-M201RAD __________________ 2BE10001

Пересмотренный код выглядит следующим образом:

Stopwatch sw = new Stopwatch();
string path;
int fileSize = 1024 * 1024 * 1024;
int numFiles = 2;

byte[] bytes = new byte[fileSize];
Random r = new Random(DateTimeOffset.UtcNow.Millisecond);
List<int> list = Enumerable.Range(0, numFiles).ToList();
List<List<byte>> allBytes = new List<List<byte>>(numFiles);

List<string> files;

int numTests = 1;

List<long> wss = new List<long>(numTests);
List<long> wps = new List<long>(numTests);
List<long> rss = new List<long>(numTests);
List<long> rps = new List<long>(numTests);

List<long> wsh = new List<long>(numTests);
List<long> wph = new List<long>(numTests);
List<long> rsh = new List<long>(numTests);
List<long> rph = new List<long>(numTests);

Enumerable.Range(1, numTests).ToList().ForEach((i) => {
    path = @"C:\SeqParTest\";

    allBytes.Clear();
    GC.Collect();
    GC.WaitForFullGCComplete();
    list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List<byte>(bytes)); });
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    list.AsParallel().ForAll((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray()));
    wps.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    Debug.Print($"Write parallel SSD #{i}: {wps[i - 1]}");

    allBytes.Clear();
    GC.Collect();
    GC.WaitForFullGCComplete();
    list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List<byte>(bytes)); });
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    list.ForEach((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray()));
    wss.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    Debug.Print($"Write serial   SSD #{i}: {wss[i - 1]}");

    files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList();
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    files.AsParallel().ForAll(f => File.ReadAllBytes(f).GetHashCode());
    rps.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    files.ForEach(f => File.Delete(f));
    Debug.Print($"Read  parallel SSD #{i}: {rps[i - 1]}");
    GC.Collect();
    GC.WaitForFullGCComplete();

    files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList();
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    files.ForEach(f => File.ReadAllBytes(f).GetHashCode());
    rss.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    files.ForEach(f => File.Delete(f));
    Debug.Print($"Read  serial   SSD #{i}: {rss[i - 1]}");
    GC.Collect();
    GC.WaitForFullGCComplete();

    path = @"D:\SeqParTest\";

    allBytes.Clear();
    GC.Collect();
    GC.WaitForFullGCComplete();
    list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List<byte>(bytes)); });
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    list.AsParallel().ForAll((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray()));
    wph.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    Debug.Print($"Write parallel HDD #{i}: {wph[i - 1]}");

    allBytes.Clear();
    GC.Collect();
    GC.WaitForFullGCComplete();
    list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List<byte>(bytes)); });
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    list.ForEach((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray()));
    wsh.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    Debug.Print($"Write serial   HDD #{i}: {wsh[i - 1]}");

    files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList();
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    files.AsParallel().ForAll(f => File.ReadAllBytes(f).GetHashCode());
    rph.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    files.ForEach(f => File.Delete(f));
    Debug.Print($"Read  parallel HDD #{i}: {rph[i - 1]}");
    GC.Collect();
    GC.WaitForFullGCComplete();

    files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList();
    try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { }
    sw.Restart();
    files.ForEach(f => File.ReadAllBytes(f).GetHashCode());
    rsh.Add(sw.ElapsedMilliseconds);
    sw.Stop();
    try { GC.EndNoGCRegion(); } catch (Exception) { }
    files.ForEach(f => File.Delete(f));
    Debug.Print($"Read  serial   HDD #{i}: {rsh[i - 1]}");
    GC.Collect();
    GC.WaitForFullGCComplete();
});

Debug.Print($"Avg Write Parallel SSD: {wps.Average()}");
Debug.Print($"Avg Write Serial   SSD: {wss.Average()}");
Debug.Print($"Avg Read  Parallel SSD: {rps.Average()}");
Debug.Print($"Avg Read  Serial   SSD: {rss.Average()}");

Debug.Print($"Avg Write Parallel HDD: {wph.Average()}");
Debug.Print($"Avg Write Serial   HDD: {wsh.Average()}");
Debug.Print($"Avg Read  Parallel HDD: {rph.Average()}");
Debug.Print($"Avg Read  Serial   HDD: {rsh.Average()}");

Ну, я не полностью протестировал код, так что он может глючить. Я понял, что иногда останавливается на параллельном чтении, я предполагаю, что это произошло из-за удаления файлов из последовательного чтения. ПОСЛЕ чистки списка существующих файлов на следующем шаге, поэтому он жалуется на ошибку файла, не найденную.

Другая проблема заключается в том, что я использовал вновь созданные файлы для теста чтения. Теоретически лучше не делать этого (даже перезагружать компьютер/заполнять пустое пространство на SSD, чтобы избежать кэширования), но я не беспокоился, потому что предполагаемое сравнение происходит между последовательной и параллельной производительностью.

Update:

Я не знаю, как объяснить причину, но я думаю, это может быть, потому что ресурс IO довольно бездействует? Я попробую следующее:

  • Большие файлы (1 ГБ) в последовательном/параллельном
  • Когда другие фоновые действия используют диск IO.

Обновление 2:

Некоторые результаты из больших файлов (512M, 32 файла):

Write parallel SSD #1: 140935
Write serial   SSD #1: 133656
Read  parallel SSD #1: 62150
Read  serial   SSD #1: 43355
Write parallel HDD #1: 172448
Write serial   HDD #1: 138381
Read  parallel HDD #1: 173436
Read  serial   HDD #1: 142248

Write parallel SSD #2: 122286
Write serial   SSD #2: 119564
Read  parallel SSD #2: 53227
Read  serial   SSD #2: 43022
Write parallel HDD #2: 175922
Write serial   HDD #2: 137572
Read  parallel HDD #2: 204972
Read  serial   HDD #2: 142174

Write parallel SSD #3: 121700
Write serial   SSD #3: 117730
Read  parallel SSD #3: 107546
Read  serial   SSD #3: 42872
Write parallel HDD #3: 171914
Write serial   HDD #3: 145923
Read  parallel HDD #3: 193097
Read  serial   HDD #3: 142211

Write parallel SSD #4: 125805
Write serial   SSD #4: 118252
Read  parallel SSD #4: 113385
Read  serial   SSD #4: 42951
Write parallel HDD #4: 176920
Write serial   HDD #4: 137520
Read  parallel HDD #4: 208123
Read  serial   HDD #4: 142273

Write parallel SSD #5: 116394
Write serial   SSD #5: 116592
Read  parallel SSD #5: 61273
Read  serial   SSD #5: 43315
Write parallel HDD #5: 172259
Write serial   HDD #5: 138554
Read  parallel HDD #5: 275791
Read  serial   HDD #5: 142311

Write parallel SSD #6: 107839
Write serial   SSD #6: 135071
Read  parallel SSD #6: 79846
Read  serial   SSD #6: 43328
Write parallel HDD #6: 176034
Write serial   HDD #6: 138671
Read  parallel HDD #6: 218533
Read  serial   HDD #6: 142481

Write parallel SSD #7: 120438
Write serial   SSD #7: 118032
Read  parallel SSD #7: 45375
Read  serial   SSD #7: 42978
Write parallel HDD #7: 173151
Write serial   HDD #7: 140579
Read  parallel HDD #7: 176492
Read  serial   HDD #7: 142153

Write parallel SSD #8: 108862
Write serial   SSD #8: 123556
Read  parallel SSD #8: 120162
Read  serial   SSD #8: 42983
Write parallel HDD #8: 174699
Write serial   HDD #8: 137619
Read  parallel HDD #8: 204069
Read  serial   HDD #8: 142480

Write parallel SSD #9: 111618
Write serial   SSD #9: 117854
Read  parallel SSD #9: 51224
Read  serial   SSD #9: 42970
Write parallel HDD #9: 173069
Write serial   HDD #9: 136936
Read  parallel HDD #9: 159978
Read  serial   HDD #9: 143401

Write parallel SSD #10: 115381
Write serial   SSD #10: 118545
Read  parallel SSD #10: 79509
Read  serial   SSD #10: 43818
Write parallel HDD #10: 179545
Write serial   HDD #10: 138556
Read  parallel HDD #10: 167978
Read  serial   HDD #10: 143033

Write parallel SSD #11: 113105
Write serial   SSD #11: 116849
Read  parallel SSD #11: 84309
Read  serial   SSD #11: 42620
Write parallel HDD #11: 179432
Write serial   HDD #11: 139014
Read  parallel HDD #11: 219161
Read  serial   HDD #11: 142515

Write parallel SSD #12: 124901
Write serial   SSD #12: 121769
Read  parallel SSD #12: 137192
Read  serial   SSD #12: 43144
Write parallel HDD #12: 176091
Write serial   HDD #12: 139042
Read  parallel HDD #12: 214205
Read  serial   HDD #12: 142576

Write parallel SSD #13: 110896
Write serial   SSD #13: 123152
Read  parallel SSD #13: 56633
Read  serial   SSD #13: 42665
Write parallel HDD #13: 173123
Write serial   HDD #13: 138514
Read  parallel HDD #13: 210003
Read  serial   HDD #13: 142215

Write parallel SSD #14: 117762
Write serial   SSD #14: 126865
Read  parallel SSD #14: 90005
Read  serial   SSD #14: 44089
Write parallel HDD #14: 172958
Write serial   HDD #14: 139908
Read  parallel HDD #14: 217826
Read  serial   HDD #14: 142216

Write parallel SSD #15: 109912
Write serial   SSD #15: 121276
Read  parallel SSD #15: 72285
Read  serial   SSD #15: 42827
Write parallel HDD #15: 176255
Write serial   HDD #15: 139084
Read  parallel HDD #15: 183926
Read  serial   HDD #15: 142111

Write parallel SSD #16: 122476
Write serial   SSD #16: 126283
Read  parallel SSD #16: 47875
Read  serial   SSD #16: 43799
Write parallel HDD #16: 173436
Write serial   HDD #16: 137203
Read  parallel HDD #16: 294374
Read  serial   HDD #16: 142387

Write parallel SSD #17: 112168
Write serial   SSD #17: 121079
Read  parallel SSD #17: 79001
Read  serial   SSD #17: 43207

Я сожалею, что у меня нет времени для завершения всех 25 запусков, но результат показывает, что на больших файлах последовательный R/W может быть быстрее, чем параллельный, если использование диска заполнено. Я думаю, что это может послужить причиной других дискуссий о SO.

Ответ 4

Во-первых, тест должен исключать любые операции ЦП/ОЗУ (GetHashCode), поскольку серийный код может ожидать CPU перед выполнением следующей операции с диском.

Внутри SSD всегда пытается провести параллелизацию операций между его различными внутренними чипами. Его способность делать это зависит от модели, сколько (TRIMmed) свободного места у нее есть и т.д. До того, как некоторое время назад это должно вести себя одинаково в параллельном и последовательном порядке, потому что очередь между ОС и SSD все равно является последовательной.... Если SSD не поддерживает NCQ (Native Command Queue), который позволяет SSD выбирать, какую операцию из очереди делать дальше, чтобы максимизировать использование всех своих чипов. Таким образом, вы можете увидеть преимущества NCQ. (Обратите внимание, что NCQ также работает для жестких дисков).

Из-за различий между SSD (стратегия контроллера, количество внутренних чипов, свободное пространство и т.д.) преимущества параллелиализации, вероятно, будут сильно различаться.