С# 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 (стратегия контроллера, количество внутренних чипов, свободное пространство и т.д.) преимущества параллелиализации, вероятно, будут сильно различаться.