c# - solido - ssd vs hdd
¿El paralelismo de E/S de C#aumenta el rendimiento con SSD? (4)
¡Es un tema muy interesante! Lamento no poder explicar los detalles técnicos, pero hay algunas preocupaciones que deben plantearse. Es un poco largo, así que no puedo incluirlos en el comentario. Por favor, perdóname para publicarlo como una "respuesta".
Creo que debe pensar tanto en archivos grandes como pequeños, además, la prueba debe ejecutarse varias veces y obtener el tiempo promedio para asegurarse de que el resultado sea verificable. Una guía general es ejecutarlo 25 veces como sugiere un artículo en computación evolutiva.
Otra preocupación es sobre el almacenamiento en caché del sistema. Solo creó un búfer de bytes
y siempre escribe lo mismo, no sé cómo el sistema maneja el búfer, pero para minimizar la diferencia, le sugiero que cree un búfer diferente para diferentes archivos.
(Actualización: tal vez GC también afecte el rendimiento, así que revisé nuevamente para dejar de lado GC tanto como pude).
Afortunadamente, tengo tanto SSD como HDD en mi computadora y revisé el código de prueba. Lo ejecuté con diferentes configuraciones y obtuve los siguientes resultados. Espero poder inspirar a alguien para una mejor explicación.
1KB, 256 Archivos
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
512KB, 256 Archivos
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
Mi máquina es: i7-6820HQ / 32G / Windows 7 Enterprise x64 / VS2017 Professional / Target .NET 4.6 / Se ejecuta en modo de depuración.
Los dos discos duros son:
Unidad C: IDE / Crucial_CT275MX300SSD4 ___________________ M0CR021
Unidad D: IDE / ST2000LM003_HN-M201RAD __________________ 2BE10001
El código revisado es el siguiente:
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()}");
Bueno, no he probado completamente el código, por lo que puede tener errores. Me di cuenta de que a veces se detiene en la lectura paralela. Supongo que se debió a que la eliminación de los archivos de la lectura secuencial se completó DESPUÉS de leer la lista de archivos existentes en el siguiente paso, por lo que se queja de que el archivo no encontró un error.
Otro problema es que usé los archivos recién creados para la prueba de lectura. Teóricamente, es mejor no hacerlo (incluso reiniciar la computadora / completar el espacio vacío en el SSD para evitar el almacenamiento en caché), pero no me molesté porque la comparación prevista es entre el rendimiento secuencial y el paralelo.
Actualizar:
No sé cómo explicar el motivo, pero creo que puede deberse a que el recurso de IO está bastante inactivo. Voy a intentar dos cosas la siguiente:
- Archivos grandes (1GB) en serie / paralelo
- Cuando otras actividades de fondo utilizan el disco IO.
Actualización 2:
Algunos resultados de archivos grandes (512M, 32 archivos):
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
Lamento no tener tiempo para completar las 25 ejecuciones, pero el resultado muestra en archivos grandes que el R / W secuencial podría ser más rápido que en paralelo si el uso del disco está completo. Creo que puede ser la razón de otras discusiones sobre SO.
He leído algunas respuestas (por example ) aquí en SO donde algunos dicen que el paralelismo no va a aumentar el rendimiento (tal vez en lectura IO).
Pero he creado algunas pruebas que muestran que también las operaciones de ESCRITURA son mucho más rápidas.
- LEER PRUEBA:
He creado archivos 6000 aleatorios con datos ficticios:
Intentemos leerlos sin paralelismo:
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();
Resultado 1:
Ejecutar LEER- Serial 595
Ejecutar LEER- Paralelo 193
Resultado2:
Ejecutar LEER- Serial 316
Ejecutar leer-paralelo 192
- PRUEBA DE ESCRITURA:
Vamos a crear 1000 archivos aleatorios donde cada archivo es de 300K. (He vaciado el directorio de la prueba anterior)
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();
Resultado 1:
Ejecutar WRITE serie 2028
Ejecutar WRITE Parallel 368
Resultado 2:
Ejecutar WRITE serie 784
Ejecutar WRITE Paralelo 426
Pregunta:
Los resultados me han sorprendido. Está claro que, en contra de todas las expectativas (especialmente con las operaciones WRITE), el rendimiento es mejor con el paralelismo, pero con las operaciones IO.
¿Cómo / por qué vienen los resultados del paralelismo mejor? Parece que el SSD puede funcionar con subprocesos y que no hay / hay menos cuello de botella cuando se ejecuta más de un trabajo a la vez en el dispositivo IO.
Nb No lo probé con HDD (me alegrará que uno que tenga HDD realice las pruebas).
La evaluación comparativa es un arte difícil, simplemente no estás midiendo lo que crees que eres. El hecho de que no sea realmente una sobrecarga de E / S es algo obvio en los resultados de la prueba, ¿por qué el código de un solo hilo es más rápido la segunda vez que lo ejecuta?
Lo que no está contando es el comportamiento de la caché del sistema de archivos . Mantiene una copia del contenido del disco en la memoria RAM. Esto tiene un impacto particularmente grande en la medición de código de subprocesos múltiples, ya que no utiliza ninguna E / S en absoluto . En una palabra:
Las lecturas provienen de la memoria RAM si la memoria caché del sistema de archivos tiene una copia de los datos. Esto funciona a velocidades de bus de memoria, normalmente alrededor de 35 gigabytes / segundo. Si no tiene una copia, la lectura se retrasa hasta que el disco suministre los datos. No se limita a leer el clúster solicitado, sino una cantidad completa de datos del disco.
Las escrituras van directamente a la memoria RAM, se completa muy rápidamente. Los datos se escriben perezosamente en el disco en segundo plano mientras el programa se sigue ejecutando, optimizado para minimizar el movimiento de la cabeza de escritura en el orden de los cilindros. Solo si no hay más RAM disponible, una escritura se detendrá alguna vez.
El tamaño real de la memoria caché depende de la cantidad de RAM instalada y de la necesidad de RAM impuesta por los procesos en ejecución. Una guía muy aproximada es que puede contar con 1 GB en una máquina con 4 GB de RAM, 3 GB en una máquina con 8 GB de RAM. Es visible en el Monitor de recursos, en la pestaña Memoria, se muestra como el valor "En caché". Tenga en cuenta que es altamente variable.
Lo suficiente para dar sentido a lo que ve, los beneficios de la prueba paralela en gran medida de la prueba en serie ya han leído todos los datos. Si hubiera escrito la prueba para que la prueba paralela se ejecutara primero, entonces habría obtenido resultados muy diferentes. Solo si el caché está frío podría ver la pérdida de rendimiento debido a los subprocesos. Tendrías que reiniciar tu máquina para asegurar esa condición. O lea otro archivo muy grande primero, lo suficientemente grande como para expulsar datos útiles de la memoria caché.
Solo si tiene un conocimiento a priori de su programa que solo haya leído datos recién escritos, podrá usar los hilos de forma segura sin riesgo de pérdida de rendimiento. Esa garantía es normalmente bastante difícil de conseguir. Sí existe, un buen ejemplo es que Visual Studio está construyendo tu proyecto. El compilador escribe el resultado de la compilación en el directorio obj / Debug, luego MSBuild lo copia en bin / Debug. Parece muy inútil, pero no lo es, esa copia siempre se completará muy rápidamente ya que el archivo está caliente en la caché. La memoria caché también explica la diferencia entre un inicio en frío y un inicio en caliente de un programa .NET y por qué no siempre es mejor usar NGen.
La razón de este comportamiento se llama File Caching, que es una característica de Windows para mejorar el rendimiento de las operaciones de archivos. Echemos un vistazo a una breve explicación en el Centro de desarrollo de Windows :
De forma predeterminada, Windows almacena en caché los datos de archivos que se leen de los discos y se escriben en ellos. Esto implica que las operaciones de lectura leen datos de archivos de un área en la memoria del sistema conocida como el caché de archivos del sistema, en lugar de hacerlo desde el disco físico.
Esto significa que el disco duro (normalmente) nunca se utiliza durante sus pruebas.
Podemos evitar este comportamiento creando un FileStream
usando el indicador FILE_FLAG_NO_BUFFERING
, documentado en el MSDN . Echemos un vistazo a nuestra nueva función ReadUnBuffered
usando este indicador:
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);
}
}
El resultado: la lectura en serie es mucho más rápida. En mi caso, incluso casi el doble de rápido.
La lectura de un archivo utilizando el caché estándar de Windows solo tiene que realizar operaciones de CPU y RAM para administrar el chaching de archivos, lidiar con FileStream
, ... porque los archivos ya están almacenados en caché. Claro, no requiere mucha CPU pero no es despreciable. Como los archivos ya están en la memoria caché del sistema, el enfoque paralelo (sin modificación de la memoria caché) muestra exactamente el tiempo de estas operaciones generales .
Este comportamiento también se puede transferir a operaciones de escritura.
Primero, la prueba debe excluir cualquier operación de CPU / RAM (GetHashCode) ya que el código de serie puede estar esperando a la CPU antes de realizar la siguiente operación de disco.
Internamente, un SSD siempre está tratando de paralizar las operaciones entre sus diferentes chips internos. Su capacidad para hacerlo depende del modelo, de la cantidad de espacio libre (TRIMmed) que tiene, etc. Hasta hace un tiempo, esto debería comportarse igual en paralelo y en serie, porque la cola entre el SO y el SSD es en serie de todos modos ... A menos que el SSD sea compatible con NCQ (cola de comandos nativos), que permite al SSD seleccionar qué operación de la cola realizar a continuación, para maximizar el uso de todos sus chips. Entonces, lo que está viendo podría ser los beneficios de NCQ. (Tenga en cuenta que NCQ también funciona para unidades de disco duro).
Debido a las diferencias entre los SSD (estrategia del controlador, número de chips internos, espacio libre, etc.) los beneficios de la paralelización probablemente varíen mucho.