Temporizador do sistema no Windows: grande mudança

O comportamento do Agendador do Windows mudou significativamente no Windows 10 2004 sem quaisquer avisos ou alterações na documentação. Isso provavelmente interromperá vários aplicativos. Não é a primeira vez que isso acontece , mas essa mudança é mais séria.



Resumindo, as chamadas para timeBeginPeriod de um processo agora afetam menos outros processos do que antes, embora o efeito ainda esteja presente.



Acho que o novo comportamento é essencialmente uma melhoria, mas é estranho e merece ser documentado. Honestamente, estou avisando - só tenho os resultados dos meus próprios experimentos, então só posso supor sobre os objetivos e alguns efeitos colaterais dessa mudança. Se alguma das minhas descobertas estiver incorreta, por favor me avise.



Interrupções do cronômetro e sua razão de ser



Primeiro, um pouco de contexto sobre o design do sistema operacional. É desejável que o programa adormeça e depois acorde. Na verdade, isso não deve ser feito com muita frequência - os threads geralmente esperam por eventos, não por temporizadores - mas às vezes é necessário. Portanto, o Windows tem uma função Sleep  - passe a duração desejada do sleep em milissegundos e ele despertará o processo:



Sleep (1);



Vale a pena considerar como isso é implementado. Idealmente, quando Sleep (1) é chamado, o processador entra em hibernação. Mas como o sistema operacional ativa o thread se o processador está hibernando? A resposta é interrupções de hardware. O sistema operacional programa um chip - um cronômetro de hardware que dispara uma interrupção que ativa o processador e o sistema operacional inicia seu thread.



As funções WaitForSingleObject e WaitForMultipleObjects também têm valores de tempo limite e esses tempos limite são implementados usando o mesmo mecanismo.



Se houver muitos threads esperando por timers, o sistema operacional pode programar um timer de hardware para um tempo individual para cada thread, mas isso geralmente leva ao fato de que os threads acordam em um momento aleatório e o processador não dorme normalmente. A eficiência de energia da CPU é altamente dependente de seu tempo de hibernação ( o tempo normal é 8 ms ) e ativações aleatórias não contribuem para isso. Se vários threads sincronizarem ou agruparem suas expectativas de cronômetro, o sistema se tornará mais eficiente em termos de energia.



Há muitas maneiras de combinar ativações, mas o mecanismo principal do Windows é interromper globalmente o tique-taque do cronômetro a uma taxa constante. Quando um encadeamento chama Sleep (n) , o SO irá agendar o encadeamento para iniciar imediatamente após a primeira interrupção do cronômetro. Isso significa que o encadeamento pode acabar acordando um pouco mais tarde, mas o Windows não é um sistema operacional em tempo real, ele não garante um tempo específico de ativação (neste momento, os núcleos do processador podem estar ocupados), por isso é normal acordar um pouco mais tarde.



O intervalo entre as interrupções do cronômetro depende da versão do Windows e do hardware, mas em todas as minhas máquinas o padrão é 15.625ms (1000ms / 64). Isso significa que se você chamar Sleep (1)em algum momento aleatório, o processo será ativado em algum lugar entre 1,0 ms e 16,625 ms no futuro, quando a próxima interrupção do cronômetro global for disparada (ou uma vez mais tarde se for muito cedo).



Resumindo, a natureza dos atrasos do cronômetro é tal que (a menos que a espera ativa pelo processador seja usada e, por favor, não o use ), o sistema operacional pode ativar threads apenas em determinados momentos usando interrupções do cronômetro, e o Windows usa interrupções regulares.



Alguns programas não acomodam uma ampla gama de latências (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, muitos jogos, etc.). Felizmente, eles podem corrigir o problema com a função timeBeginPeriodo que permite ao programa solicitar um intervalo menor. Há também uma função NtSetTimerResolution que permite que o intervalo seja definido para menos de um milissegundo, mas raramente é usada e nunca é necessária, então não vou mencioná-la novamente.



Décadas de loucura



Aqui está uma coisa maluca: timeBeginPeriod pode ser chamado por qualquer programa e muda o intervalo de interrupção do cronômetro, e a interrupção do cronômetro é um recurso global.



Vamos imaginar que o processo A está em um loop com uma chamada para Sleep (1) . Isso está errado, mas está, e por padrão ele desperta a cada 15,625 ms, ou 64 vezes por segundo. Em seguida, o processo B chega e chama timeBeginPeriod (2) . Isso faz com que o cronômetro seja acionado com mais freqüência e, de repente, o processo A desperta 500 vezes por segundo em vez de 64 vezes por segundo. Isso é loucura! Mas é assim que o Windows sempre funcionou.



Neste ponto, se o processo C apareceu e chamou timeBeginPeriod (4), isso não mudaria nada - o processo A continuaria a acordar 500 vezes por segundo. Nessa situação, não é a última chamada que define as regras, mas a chamada com o intervalo mínimo.



Assim, uma chamada para timeBeginPeriod de qualquer programa em execução pode definir o intervalo de interrupção do cronômetro global. Se este programa sair ou chamar timeEndPeriod , o novo mínimo terá efeito. Se um programa chama timeBeginPeriod (1) , este é agora o intervalo de interrupção do cronômetro de todo o sistema. Se um programa chama timeBeginPeriod (1) e outro chama timeBeginPeriod (4) , então o intervalo de interrupção do cronômetro de um milissegundo se torna uma lei universal.



Isso é importante porque uma alta taxa de interrupção do temporizador - e sua alta taxa de escalonamento de thread associada - pode desperdiçar energia significativa da CPU, conforme discutido aqui .



Um aplicativo que precisa de programação baseada em cronômetro é o navegador da web. O padrão JavaScript tem uma função setTimeout que pede ao navegador para chamar uma função JavaScript após alguns milissegundos. O Chromium usa temporizadores para implementar este e outros recursos (basicamente WaitForSingleObject com tempo limite, não Sleep) Isso geralmente requer um aumento na taxa de interrupção do temporizador. Para manter a vida útil da bateria baixa, o Chromium foi recentemente redesenhado para manter a taxa de interrupção do cronômetro abaixo de 125 Hz (intervalo de 8 ms) com a energia da bateria .



timeGetTime



A função timeGetTime (não deve ser confundida com GetTickCount) retorna a hora atual, atualizada por uma interrupção do cronômetro. Os processadores não têm sido, historicamente, muito bons em manter a hora precisa (seus relógios são deliberadamente flutuados para evitar servir como transmissores FM e por outros motivos), portanto, as CPUs geralmente dependem de geradores de relógio separados para manter a hora precisa. Ler esses chips é caro, por isso o Windows mantém um contador de milissegundos de 64 bits atualizado com uma interrupção do temporizador. Esse cronômetro é armazenado na memória compartilhada, de modo que qualquer processo pode ler a hora atual de maneira barata sem ter que ir para o relógio. timeGetTime chama ReadInterruptTick , que essencialmente apenas lê este contador de 64 bits. É simples assim!



Como o contador é atualizado pela interrupção do temporizador, podemos rastreá-lo e encontrar a frequência de interrupção do temporizador.



Nova realidade não documentada



Com o lançamento do Windows 10 2004 (abril de 2020), alguns desses mecanismos mudaram um pouco, mas de uma forma muito confusa. Primeiro, havia mensagens de que timeBeginPeriod não estava mais funcionando . Na verdade, tudo acabou sendo muito mais complicado.



Os primeiros experimentos deram resultados mistos. Quando executei o programa com uma chamada para timeBeginPeriod (2) , clockres mostrou um intervalo de cronômetro de 2,0 ms , mas um programa de teste separado com um loop Sleep (1) acordou cerca de 64 vezes por segundo em vez de 500 vezes como nas versões anteriores do Windows.



Experimento científico



Então, escrevi alguns programas para estudar o comportamento do sistema. Um programa ( change_interval.cpp ) simplesmente fica em um loop, chamando timeBeginPeriod em intervalos de 1 a 15ms . Ela mantém cada intervalo por quatro segundos e então passa para o próximo e assim por diante em um círculo. Quinze linhas de código. Fácil.



Outro programa ( measure_interval.cpp ) executa vários testes para verificar como seu comportamento muda quando change_interval.cpp muda. O programa monitora três parâmetros.



  1. Ela pergunta ao sistema operacional qual é a resolução atual do cronômetro global usando NtQueryTimerResolution .

  2. timeGetTime, ,  — , .

  3. Sleep(1), . .


@FelixPetriconi executou testes para mim no Windows 10 1909 e eu executei testes no Windows 10 2004. Aqui estão os resultados sem jitter :







Isso significa que timeBeginPeriod ainda define o intervalo do cronômetro global em todas as versões do Windows. A partir dos resultados de timeGetTime () , podemos dizer que a interrupção é disparada nessa taxa em pelo menos um núcleo do processador e a hora é atualizada. Observe também que 2.0 na primeira linha para 1909 também era 2.0 no Windows XP , depois 1.0 no Windows 7/8 e, em seguida, parece ter retornado ao 2.0 novamente?



No entanto, o comportamento de agendamento muda drasticamente no Windows 10 2004. Anteriormente, o atraso para Suspensão (1)em qualquer processo é igual ao intervalo de interrupção do cronômetro, exceto para timeBeginPeriod (1), dando um gráfico como este:







No Windows 10 2004, a relação entre timeBeginPeriod e latência de sono em outro processo (que não chamou timeBeginPeriod ) parece estranha:







A forma exata do lado esquerdo do gráfico não é clara, mas ele definitivamente vai na direção oposta da anterior!



Por quê?



Efeitos



Conforme apontado na discussão sobre reddit e hacker-news, é provável que a metade esquerda do gráfico seja uma tentativa de imitar a latência "normal" o mais próximo possível, dada a precisão disponível da interrupção do cronômetro global. Ou seja, com um intervalo de interrupção de 6 milissegundos, o retardo ocorre em cerca de 12 ms (dois ciclos), e com um intervalo de interrupção de 7 milissegundos, ele será retardado em cerca de 14 ms (dois ciclos). No entanto, medir os atrasos reais mostra que a realidade é ainda mais confusa. Com uma interrupção do temporizador definida para 7 ms, uma latência Sleep (1) de 14 ms não é nem mesmo o resultado mais comum:







alguns leitores podem culpar o ruído aleatório no sistema, mas quando a taxa de interrupção do temporizador é de 9 ms ou mais, o ruído é zero, então não poderia ser uma explicação.Tente executar o código atualizado você mesmo . Os intervalos de interrupção do cronômetro de 4ms a 8ms parecem ser particularmente controversos. As medições de intervalo provavelmente devem ser feitas usando QueryPerformanceCounter porque o código atual é afetado aleatoriamente por mudanças nas regras de agendamento e mudanças na precisão do cronômetro.



Isso tudo é muito estranho e não entendo a lógica ou a implementação. Pode ser um erro, mas duvido. Acho que há uma lógica complexa de compatibilidade com versões anteriores por trás disso. Mas a maneira mais eficaz de evitar problemas de compatibilidade é documentar as alterações, de preferência com antecedência, e aqui as edições são feitas sem aviso prévio.



Isso não afetará a maioria dos programas. Se um processo deseja uma interrupção mais rápida do cronômetro, ele mesmo deve chamar timeBeginPeriod... No entanto, os seguintes problemas podem ocorrer:



  • O programa pode assumir acidentalmente que Sleep (1) e timeGetTime têm a mesma resolução, o que não é mais o caso. Embora, tal suposição pareça improvável.

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

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




O programa de teste change_interval.cpp só funciona se ninguém solicitar uma taxa de interrupção do cronômetro mais alta. Como o Chrome e o Visual Studio têm o hábito de fazer isso, tive que fazer a maior parte da minha experimentação sem acesso à Internet e codificação no bloco de notas . Alguém sugeriu o Emacs, mas me envolver nessa discussão está além do meu poder.



All Articles