ventajas solido precio para laptop juntos instalar duro discos disco c# multithreading parallel-processing

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:

  1. Archivos grandes (1GB) en serie / paralelo
  2. 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.