tag portable online mp3tag kid3 descargar windows winapi overlapped-io buffered

windows - portable - ¿Explicación para lecturas minúsculas(superpuestas, almacenadas en búfer) que superan a las lecturas contiguas grandes?



mp3tag portable (1)

(disculpas por la introducción algo larga)

Durante el desarrollo de una aplicación que prefigura un archivo grande completo (> 400 MB) en la memoria caché del búfer para acelerar la ejecución real más tarde, probé si leer 4MB a la vez aún tenía beneficios notables sobre la lectura de solo 1MB fragmentos a la vez. Sorprendentemente, las solicitudes más pequeñas en realidad resultaron ser más rápidas. Esto parecía contra-intuitivo, así que hice una prueba más extensa.

La memoria caché del búfer se eliminó antes de ejecutar las pruebas (solo para las risas, también hice una ejecución con el archivo en los búferes. La memoria caché del búfer ofrece más de 2GB / s independientemente del tamaño de la solicitud, aunque con un sorprendente +/- 30% varianza aleatoria).
Todas las lecturas utilizaron ReadFile superpuesto con el mismo búfer de destino (el identificador se abrió con FILE_FLAG_OVERLAPPED y sin FILE_FLAG_NO_BUFFERING ). El disco duro utilizado es algo antiguo pero completamente funcional, NTFS tiene un tamaño de clúster de 8kB. El disco se desfragmentó después de una ejecución inicial (6 fragmentos frente a no fragmentado, diferencia cero). Para obtener mejores cifras, también utilicé un archivo más grande, los números que figuran a continuación corresponden a la lectura de 1 GB.

Los resultados fueron realmente sorprendentes:

4MB x 256 : 5ms per request, completion 25.8s @ ~40 MB/s 1MB x 1024 : 11.7ms per request, completion 23.3s @ ~43 MB/s 32kB x 32768 : 12.6ms per request, completion 15.5s @ ~66 MB/s 16kB x 65536 : 12.8ms per request, completion 13.5s @ ~75 MB/s

Por lo tanto, esto sugiere que enviar diez mil solicitudes de dos grupos de longitud es realmente mejor que enviar unos cientos de lecturas contiguas y grandes. El tiempo de envío (tiempo antes de que regrese ReadFile) aumenta sustancialmente a medida que aumenta el número de solicitudes, pero el tiempo de finalización asíncrono casi se reduce a la mitad.
El tiempo de CPU del kernel es de alrededor de 5-6% en todos los casos (en un quadcore, por lo que uno debería decir 20-30%) mientras que las lecturas asíncronas se están completando, lo cual es una cantidad sorprendente de CPU. cantidad despreciable de espera ocupado, también. 30% de CPU por 25 segundos a 2.6 GHz, eso son bastantes ciclos para "no hacer nada".

¿Alguna idea de cómo se puede explicar esto? Tal vez alguien aquí tiene una visión más profunda del funcionamiento interno de Windows IO superpuesto? O, ¿hay algo sustancialmente erróneo en la idea de que puede usar ReadFile para leer un megabyte de datos?

Puedo ver cómo un programador de E / S podría optimizar múltiples solicitudes minimizando las búsquedas, especialmente cuando las solicitudes son de acceso aleatorio (¡y no lo son!). También puedo ver cómo un disco duro podría realizar una optimización similar dadas algunas solicitudes en el NCQ.
Sin embargo, estamos hablando de números ridículos de solicitudes ridículamente pequeñas, que sin embargo superan lo que parece razonable por un factor de 2.

Nota: El ganador claro es el mapeo de memoria. Casi me siento inclinado a agregar "como era de esperar" porque soy un gran fanático de la asignación de memoria, pero en este caso, realmente me sorprende , ya que las "solicitudes" son aún más pequeñas y el sistema operativo debería ser aún menos capaz de predecir y programar el IO. Al principio no probé la asignación de memoria porque parecía contrario a la intuición de que pudiera competir incluso de forma remota. Tanto para tu intuición, je.

Asignar / desasignar una vista repetidamente en diferentes desplazamientos lleva prácticamente el tiempo cero. El uso de una vista de 16 MB y el fallo de cada página con un simple bucle for () que lee un solo byte por página se completa en 9.2 segundos @ ~ 111 MB / s. El uso de la CPU es inferior al 3% (un núcleo) en todo momento. La misma computadora, el mismo disco, lo mismo todo.

También parece que Windows carga 8 páginas en el caché del búfer a la vez, aunque en realidad solo se crea una página. La falla de cada 8ª página se ejecuta a la misma velocidad y carga la misma cantidad de datos desde el disco, pero muestra métricas de "memoria física" y "caché del sistema" más bajas y solo 1/8 de las fallas de la página. Las lecturas subsiguientes demuestran que las páginas están definitivamente en la memoria caché del búfer (sin demora, sin actividad de disco).

(¿Es posible que un archivo muy mapeado en memoria esté muy, muy distante, más rápido en lectura secuencial enorme? )

Para hacerlo un poco más ilustrativo:

Actualizar:

El uso de FILE_FLAG_SEQUENTIAL_SCAN parece "equilibrar" un poco las lecturas de 128k, lo que mejora el rendimiento en un 100%. Por otro lado, tiene un impacto severo en las lecturas de 512k y 256k (tienes que preguntarte por qué) y no tiene ningún efecto real en nada más. El gráfico de MB / s de los tamaños de bloques más pequeños podría parecer un poco más "par", pero no hay diferencia en el tiempo de ejecución.

Es posible que también haya encontrado una explicación para que los bloques más pequeños se desempeñen mejor. Como sabe, las solicitudes asíncronas pueden ejecutarse de forma síncrona si el sistema operativo puede atender la solicitud de inmediato, es decir, desde los búferes (y para una variedad de limitaciones técnicas específicas de la versión).

Cuando se tienen en cuenta las lecturas asíncronas reales frente a las lecturas asíncronas "inmediatas", se observa que más de 256k, Windows ejecuta cada solicitud asíncrona de forma asíncrona. Cuanto más pequeño es el tamaño de bloque, más solicitudes se atienden "inmediatamente", incluso cuando no están disponibles inmediatamente (es decir, ReadFile simplemente se ejecuta de forma síncrona). No puedo distinguir un patrón claro (como "las primeras 100 solicitudes" o "más de 1000 solicitudes"), pero parece haber una correlación inversa entre el tamaño de la solicitud y la sincronicidad. En un tamaño de bloque de 8k, cada solicitud asíncrona se sirve de forma síncrona.
Las transferencias síncronas almacenadas en búfer son, por alguna razón, el doble de rápidas que las transferencias asíncronas (no tenemos idea de por qué), por lo tanto, cuanto más pequeños son los tamaños de solicitud, más rápida es la transferencia general, ya que se realizan más transferencias de forma sincrónica.

Para los valores predeterminados asignados en la memoria, FILE_FLAG_SEQUENTIAL_SCAN causa una forma ligeramente diferente del gráfico de rendimiento (hay una "muesca" que se mueve un poco hacia atrás), pero el tiempo total tomado es exactamente idéntico (de nuevo, esto es sorprendente, pero no puedo Ayúdalo).

Actualización 2:

Unbuffered IO hace que los gráficos de rendimiento para los testcases de solicitud de 1M, 4M y 512k sean algo más altos y más "puntiagudos" con máximos en los años 90 de GB / s, pero con mínimos severos también, el tiempo de ejecución general para 1GB está dentro de +/- 0.5 s de la ejecución en búfer (las solicitudes con tamaños de búfer más pequeños se completan significativamente más rápido, sin embargo, esto se debe a que con más de 2558 solicitudes en vuelo, se devuelve ERROR_WORKING_SET_QUOTA). El uso medido de la CPU es cero en todos los casos sin búfer, lo que no es sorprendente, ya que cualquier IO que ocurra se ejecuta a través de DMA.

Otra observación muy interesante con FILE_FLAG_NO_BUFFERING es que cambia significativamente el comportamiento de la API. CancelIO ya no funciona, al menos no en el sentido de cancelar IO . Con solicitudes en CancelIO no en CancelIO , CancelIO simplemente se bloqueará hasta que todas las solicitudes hayan finalizado. Un abogado probablemente diría que la función no puede ser considerada responsable por el incumplimiento de su deber, ya que no quedan más solicitudes en vuelo cuando regresa, por lo que de alguna manera ha hecho lo que se le pidió, pero mi entendimiento de "cancelar" es algo diferente
Con IO superpuesta y CancelIO , CancelIO simplemente cortará la cuerda, todas las operaciones en vuelo terminarán de inmediato, como cabría esperar.

Otra cosa graciosa es que el proceso no se puede destruir hasta que todas las solicitudes hayan finalizado o hayan fallado. Este tipo de sentido tiene sentido si el SO está haciendo DMA en ese espacio de direcciones, pero sin embargo es una "característica" sorprendente.


No soy un experto en sistemas de archivos, pero creo que hay un par de cosas que están sucediendo aquí. Antes que nada. Escribe tu comentario acerca de que el mapeo de memoria es el ganador. Esto no es del todo sorprendente, ya que el administrador de caché de NT se basa en la asignación de memoria: al hacer la asignación de memoria usted mismo, está duplicando el comportamiento del administrador de memoria caché sin las copias de memoria adicionales.

Cuando lees secuencialmente el archivo, el administrador de caché intenta pre-buscar los datos por ti, por lo que es probable que estés viendo el efecto de lectura anticipada en el administrador de caché. En algún punto, el administrador de caché detiene las lecturas de captación previa (o, más bien, en algún punto, los datos recopilados previamente no son suficientes para satisfacer sus lecturas y, por lo tanto, el administrador de caché tiene que detenerse). Eso puede explicar la desaceleración en las E / S más grandes que está viendo.

¿Has intentado agregar FILE_FLAG_SEQUENTIAL_SCAN a tus marcas de CreateFile? Eso le indica al recolector que sea aún más agresivo.

Esto puede ser contraintuitivo, pero tradicionalmente la forma más rápida de leer datos del disco es usar E / S asíncronas y FILE_FLAG_NO_BUFFERING. Cuando haga eso, la E / S pasa directamente del controlador de disco a sus búferes de E / S sin que haya un obstáculo (suponiendo que los segmentos del archivo sean contiguos; si no lo son, el sistema de archivos tendrá que emitir varias lecturas de disco para satisfacer la solicitud de lectura de la aplicación). Por supuesto, también significa que pierde la lógica de captación previa incorporada y tiene que rodar la suya. Pero con FILE_FLAG_NO_BUFFERING tienes el control completo de tu canal de E / S.

Otra cosa que debe recordar: cuando realice E / S asíncronas, es importante asegurarse de que siempre tenga una solicitud de E / S sobresaliente; de ​​lo contrario, perderá tiempo potencial entre la finalización de la última E / S y la próxima E / S. Está empezado.