Temporizador del sistema en Windows: gran cambio

El comportamiento del Programador de Windows cambió significativamente en Windows 10 2004 sin advertencias ni cambios en la documentación. Esto probablemente romperá varias aplicaciones. Esta no es la primera vez que sucede , pero este cambio es más serio.



En resumen, las llamadas a timeBeginPeriod desde un proceso ahora afectan a otros procesos menos que antes, aunque el efecto todavía está presente.



Creo que el nuevo comportamiento es esencialmente una mejora, pero es extraño y merece ser documentado. Honestamente, les advierto, solo tengo los resultados de mis propios experimentos, por lo que solo puedo adivinar los objetivos y algunos efectos secundarios de este cambio. Si alguno de mis hallazgos es incorrecto, hágamelo saber.



El temporizador interrumpe y su razón de ser



Primero, un poco de contexto sobre el diseño del sistema operativo. Es deseable que el programa se pueda dormir y luego se despierte. De hecho, esto no debe hacerse con mucha frecuencia (los hilos suelen esperar eventos, no temporizadores), pero a veces es necesario. Por lo tanto, Windows tiene una función de suspensión  : pase la duración de suspensión deseada en milisegundos y activará el proceso:



suspensión (1);



Vale la pena considerar cómo se implementa esto. Idealmente, cuando se llama a Sleep (1) , el procesador se irá a dormir. Pero, ¿cómo activa el sistema operativo el hilo si el procesador está inactivo? La respuesta son las interrupciones de hardware. El sistema operativo programa un chip, un temporizador de hardware, que luego activa una interrupción que activa el procesador, y el sistema operativo inicia su hilo.



Las funciones WaitForSingleObject y WaitForMultipleObjects también tienen valores de tiempo de espera, y estos tiempos de espera se implementan utilizando el mismo mecanismo.



Si hay muchos subprocesos esperando temporizadores, entonces el sistema operativo puede programar un temporizador de hardware para un tiempo individual para cada subproceso, pero esto generalmente lleva al hecho de que los subprocesos se despiertan en un momento aleatorio y el procesador no duerme normalmente. La eficiencia energética de la CPU depende en gran medida de su tiempo de suspensión ( el tiempo normal es de 8 ms ) y las activaciones aleatorias no contribuyen a esto. Si varios subprocesos sincronizan o agrupan sus expectativas de temporizador, el sistema se vuelve más eficiente energéticamente.



Hay muchas formas de combinar las activaciones, pero el mecanismo principal de Windows es interrumpir globalmente un temporizador que marca un ritmo constante. Cuando un hilo llama a Sleep (n) , el sistema operativo programará el hilo para que se inicie inmediatamente después de la primera interrupción del temporizador. Esto significa que el hilo puede terminar despertando un poco más tarde, pero Windows no es un sistema operativo en tiempo real, no garantiza un tiempo de activación específico en absoluto (en este momento, los núcleos del procesador pueden estar ocupados), por lo que es bastante normal que se despierte un poco más tarde.



El intervalo entre las interrupciones del temporizador depende de la versión de Windows y del hardware, pero en todas mis máquinas el valor predeterminado es 15,625 ms (1000 ms / 64). Esto significa que si llamas a Sleep (1)en algún momento aleatorio, el proceso se activará en algún lugar entre 1.0ms y 16.625ms en el futuro cuando se active la siguiente interrupción del temporizador global (o una vez más tarde si es demasiado pronto).



En resumen, la naturaleza de las demoras del temporizador es tal que (a menos que se use la espera activa para el procesador, y por favor no lo use ), el sistema operativo puede despertar subprocesos solo en ciertos momentos usando interrupciones del temporizador, y Windows usa interrupciones regulares.



Algunos programas no se adaptan a una gama de latencias tan amplia (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, muchos juegos, etc.). Afortunadamente, pueden solucionar el problema con la función timeBeginPeriodlo que permite que el programa solicite un intervalo más pequeño. También hay una función NtSetTimerResolution que permite que el intervalo se establezca en menos de un milisegundo, pero rara vez se usa y nunca se necesita, por lo que no lo mencionaré nuevamente.



Décadas de locura



Aquí hay una locura: timeBeginPeriod puede ser llamado por cualquier programa y cambia el intervalo de interrupción del temporizador, y la interrupción del temporizador es un recurso global.



Imaginemos que el proceso A está en un bucle con una llamada a Sleep (1) . Esto es incorrecto, pero lo es, y por defecto se activa cada 15,625 ms, o 64 veces por segundo. Luego, el proceso B entra y llama a timeBeginPeriod (2) . Esto hace que el temporizador se dispare con más frecuencia y, de repente, el proceso A se activa 500 veces por segundo en lugar de 64 veces por segundo. ¡Esto es una locura! Pero así es como siempre ha funcionado Windows.



En este punto, si el proceso C apareció y se llamó timeBeginPeriod (4), eso no cambiaría nada: el proceso A seguiría despertando 500 veces por segundo. En tal situación, no es la última llamada la que establece las reglas, sino la llamada con el intervalo mínimo.



Por lo tanto, llamar a timeBeginPeriod desde cualquier programa en ejecución puede establecer el intervalo de interrupción del temporizador global. Si este programa sale o llama a timeEndPeriod , el nuevo mínimo entra en vigor. Si un programa llama a timeBeginPeriod (1) , este es ahora el intervalo de interrupción del temporizador de todo el sistema. Si un programa llama a timeBeginPeriod (1) y otro llama a timeBeginPeriod (4) , entonces el intervalo de interrupción del temporizador de un milisegundo se convierte en una ley universal.



Esto es importante porque una alta tasa de interrupción del temporizador, y su alta tasa de programación de subprocesos asociada, pueden desperdiciar una potencia de CPU significativa, como se explica aquí .



Una aplicación que necesita programación basada en temporizador es el navegador web. El estándar JavaScript tiene una función setTimeout que le pide al navegador que llame a una función JavaScript después de unos milisegundos. Chromium usa temporizadores para implementar esta y otras funciones (básicamente WaitForSingleObject con tiempos de espera, no Sleep). Esto a menudo requiere una mayor tasa de interrupción del temporizador. Para mantener baja la vida útil de la batería, Chromium se rediseñó recientemente para mantener la tasa de interrupción del temporizador por debajo de 125 Hz (intervalo de 8 ms) con la energía de la batería .



timeGetTime



La función timeGetTime (que no debe confundirse con GetTickCount) devuelve la hora actual, actualizada por una interrupción del temporizador. Históricamente, los procesadores no han sido muy buenos para mantener la hora precisa (sus relojes se oscilan deliberadamente para evitar servir como transmisores de FM y por otras razones), por lo que las CPU a menudo dependen de generadores de reloj separados para mantener la hora exacta. La lectura de estos chips es costosa, por lo que Windows mantiene actualizado un contador de milisegundos de 64 bits con una interrupción del temporizador. Este temporizador se almacena en la memoria compartida, por lo que cualquier proceso puede leer la hora actual de forma económica desde allí sin tener que ir al reloj. timeGetTime llama a ReadInterruptTick , que básicamente lee este contador de 64 bits. ¡Es así de simple!



Dado que el contador se actualiza mediante la interrupción del temporizador, podemos rastrearlo y encontrar la frecuencia de la interrupción del temporizador.



Nueva realidad indocumentada



Con el lanzamiento de Windows 10 2004 (abril de 2020), algunos de estos mecanismos han cambiado ligeramente, pero de una manera muy confusa. Primero, hubo mensajes de que timeBeginPeriod ya no estaba funcionando . De hecho, todo resultó mucho más complicado.



Los primeros experimentos dieron resultados mixtos. Cuando ejecuté el programa con una llamada a timeBeginPeriod (2) , clockres mostró un intervalo de temporizador de 2.0ms , pero un programa de prueba separado con un bucle Sleep (1) se despertó aproximadamente 64 veces por segundo en lugar de 500 veces como en versiones anteriores de Windows.



Experimento científico



Luego escribí un par de programas para estudiar el comportamiento del sistema. Un programa ( change_interval.cpp ) simplemente se sienta en un bucle, llamando timeBeginPeriod a intervalos de 1 a 15ms . Mantiene cada intervalo durante cuatro segundos y luego pasa al siguiente, y así sucesivamente en un círculo. Quince líneas de código. Fácil.



Otro programa ( measure_interval.cpp ) ejecuta varias pruebas para comprobar cómo cambia su comportamiento cuando cambia change_interval.cpp. El programa monitorea tres parámetros.



  1. Ella le pregunta al sistema operativo cuál es la resolución actual del temporizador global usando NtQueryTimerResolution .

  2. timeGetTime, ,  — , .

  3. Sleep(1), . .


@FelixPetriconi ejecutó pruebas para mí en Windows 10 1909 y yo ejecuté pruebas en Windows 10 2004. Aquí están los resultados sin fluctuaciones :







Esto significa que timeBeginPeriod aún establece el intervalo de temporizador global en todas las versiones de Windows. A partir de los resultados de timeGetTime () , podemos decir que la interrupción se activa a esta velocidad en al menos un núcleo de procesador, y la hora se actualiza. Tenga en cuenta también que 2.0 en la primera línea para 1909 también fue 2.0 en Windows XP , luego 1.0 en Windows 7/8, y luego parece haber vuelto a 2.0 nuevamente.



Sin embargo, el comportamiento de la programación cambia drásticamente en Windows 10 2004. Anteriormente, el retraso para la suspensión (1)en cualquier proceso es igual al intervalo de interrupción del temporizador, excepto timeBeginPeriod (1), dando un gráfico como este:







En Windows 10 2004, la relación entre timeBeginPeriod y la latencia de suspensión en otro proceso (que no llamó timeBeginPeriod ) parece extraño:







la forma exacta del lado izquierdo del gráfico no está clara, pero ¡definitivamente va en la dirección opuesta a la anterior!



¿Por qué?



Efectos



Como se señaló en la discusión sobre reddit y hacker-news, es probable que la mitad izquierda del gráfico sea un intento de imitar la latencia "normal" lo más fielmente posible, dada la precisión disponible de la interrupción del temporizador global. Es decir, con un intervalo de interrupción de 6 milisegundos, el retardo se produce en unos 12 ms (dos ciclos), y con un intervalo de interrupción de 7 milisegundos, se retrasará unos 14 ms (dos ciclos). Sin embargo, medir los retrasos reales muestra que la realidad es aún más confusa. Con una interrupción del temporizador establecida en 7 ms, una latencia de reposo (1) de 14 ms ni siquiera es el resultado más común:







algunos lectores pueden culpar al sistema por ruido aleatorio, pero cuando la tasa de interrupción del temporizador es de 9 ms o superior, el ruido es cero, por lo que no podría ser una explicación.Intente ejecutar el código actualizado usted mismo . Los intervalos de interrupción del temporizador de 4 ms a 8 ms parecen ser particularmente controvertidos. Las mediciones de intervalo probablemente deberían realizarse utilizando QueryPerformanceCounter porque el código actual se ve afectado aleatoriamente por cambios en las reglas de programación y cambios en la precisión del temporizador.



Todo esto es muy extraño y no entiendo la lógica o la implementación. Esto puede ser un error, pero lo dudo. Creo que detrás de esto hay una compleja lógica de compatibilidad con versiones anteriores. Pero la forma más efectiva de evitar problemas de compatibilidad es documentar los cambios, preferiblemente con anticipación, y aquí las ediciones se realizan sin previo aviso.



Esto no afectará a la mayoría de los programas. Si un proceso desea una interrupción del temporizador más rápida, debe llamar al propio timeBeginPeriod.... Sin embargo, pueden ocurrir los siguientes problemas:



  • El programa podría asumir accidentalmente que Sleep (1) y timeGetTime tienen la misma resolución, lo que ya no es el caso. Aunque, tal suposición parece poco probable.

  • , .  — Windows System Timer Tool TimerResolution 1.2. «» , . , . , , .

  • , , . , . . , , timeBeginPeriod , , .




El programa de prueba change_interval.cpp solo funciona si nadie solicita una tasa de interrupción del temporizador más alta. Dado que tanto Chrome como Visual Studio tienen la costumbre de hacer esto, tuve que hacer la mayor parte de mi experimentación sin acceso a Internet y sin codificación en el bloc de notas . Alguien sugirió Emacs, pero involucrarme en esta discusión está fuera de mi alcance.



All Articles