In questo articolo andremo a esplorare la possibilità di modifica della risoluzione dei timer di Windows.
L’applicazione delle nozioni e tecniche esposte è per il sistema operativo Windows, anche se tutti gli altri sistemi operativi (Linux ad esempio) soffrono delle stesso peculiarità. Lo scrivente non ha la conoscenza sufficiente per fornire tecniche di programmazione equivalenti in altri sistemi operativi.
A tutti sarà accaduto di avere la necessità di un evento di timing abbastanza preciso come qualche millisecondo oppure anche diverse centinaia di millisecondi ma con una precisione elevata.
Facendo qualche prova banalmente con uno sleep si ottiene invece che il tempo di attesa non è proprio così preciso. E ciò si apprezzare se ci sono cicli multipli nel programma. Provate a fare girare un programma con:
procedure TForm1.Button1Click(Sender: TObject);
begin
sleep(5);
end;
e vedrete che lo sleep non dura 5 millisecondi. Varia tra tempi inferiori ai 5 ms. a tempi di circa 17 ms. Come accennavo tale comportamento è comune a tutti i sistemi operativi. Nel seguente codice vi posto quanto serve per verificare quanto detto (dovete creare una nuova applicazione e aggiungere un pulsate con l’evento OnClick, poi copiate nella sezione implementation quanto sotto):
implementation
{$R *.lfm}
Uses MMSystem;
//per Cicli: costante di cicli al millisecondo nelle CPU di nuova generazione (dalle serie Intel 2 in poi, da verificare)
const CiclialMs = 2000000; //Mezzo nanosecondo a ciclo
//per Cicli: costante di cicli al microsecondo nelle CPU di nuova generazione (dalle serie Intel 2 in poi, da verificare)
const Ciclialus = 2000; //Mezzo nanosecondo a ciclo
var t1, t2: UInt64;
function Cicli: UInt64; register;
begin
asm
{$IFDEF WIN64}
//Per i processori INTEL, il conteggio ritornato da rdtsc è invariante (no clock / no core), dipende dalla CPU ed è di circa mezzo nanosecondo
rdtsc; //(RDX:RAX in 64 bit)
lfence;
shl RDX, 32
or RAX, RDX //RAX per i valori di ritorno come intero sino a 64 bit (in WIN64)
{$ELSE}
//Per i processori INTEL, il conteggio ritornato da rdtsc è invariante (no clock / no core), dipende dalla CPU ed è di circa mezzo nanosecondo
rdtsc; //(EDX:EAX in 32 bit)
lfence;
//EDX:EAX per i valori di ritorno come intero sino a 64 bit (in WIN32)
{$ENDIF}
end;
end;
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin
//In t1 vengono riportati i cicli attuali del processore
t1 := Cicli;
sleep(5);
t2 := Cicli;
//Microsecondi trascorsi
ShowMessage('diff = ' + IntToStr((t2-t1) div Ciclialus));
end;
Perché accade ciò, due motivazioni principali che interessano anche in questo caso la maggioranza dei sistemi operativi:
- La prima motivazione è che Windows (così come Linux, Mac, Android, etc …) non è un sistema realtime, e quindi non è possibile usare in modo deterministico una qualsiasi funzione. Durante l’esecuzione di una qualsiasi funzione il sistema operativo esegue una miriade di attività come aggiornamento della grafica, input tastiera e mouse, gestione memoria, esecuzione elaborazioni dei driver, etc …;
- La seconda è che quasi tutte le funzionalità di timing si basano sul timer di sistema che ha una risoluzione tipica di circa 1 millisecondo e una gestione però di 15 millisecondi circa. Ciò significa che tutto ciò che riguarda i timing vengono eseguiti con “passi” di 15 millisecondi con tempi assolutamente variabili (come accenavo tra i 4 ms. e i 17 ms. in condizioni normali).
Lo sleep in realtà ha due comportamenti particolari, uno è quello noto a tutti di sospendere l’attività del THREAD corrente (o del MAIN THREAD come nel caso in esempio dato che viene usato in un evento di un componente) ed è quello che si ottiene indicando come argomento un valore maggiore di zero, l’altro invece è quello di sospendere a tempo indeterminato il THREAD quando il valore dell’argomento è zero.
In entrambi i casi il kernel del sistema operativo durante lo sleep esegue una commutazione di contesto ed esegue un altro thread . La differenza è che mentre con un valore di intervallo maggiore di zero il kernel continua l’esecuzione del thread non appena ci sono lo condizioni entro il valore dell’intervallo (infatti lo sleep può durare anche meno dell’intervallo indicato) nel caso di argomento a zero il kernel “azzera” il tempo a disposizione per il thread e lo richiama con il prossimo scheduling.
Ci sarebbe anche un altro caso, sleep con l’argomento INFINITE … ma non consiglio di provarlo.
Riferimento base: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep
Come facciamo a migliorare le prestazioni di uno sleep (ovviamente se serve) ? Molti si chiederanno perché dovrebbe servire, che cosa cambia tra 4 o 17 ms. ?
Bhè, diciamo che in alcuni casi potrebbe essere utile avere un tempo abbastanza preciso ad esempio quando si ha a che fare con una sequenza di azioni legate ad eventi asincroni.
Per poter avere una migliore risposta con lo sleep, si può usare le funzionalità delle “Estensioni Multimediali” tramite il wrapper MMSystem (WinApi.MMSystem in Delphi) che consentono di modificare la risposta del sistema operativo in particolare proprio quella legata ai timing.
L’uso di “timebeginperiod” e “timeendperiod” consente di modificare la risposta sino al millisecondo. L’uso di tali funzioni ha riflessi a livello globale di sistema operativo sino a Windows 10 versione pre 2004, mentre con le release successive l’impatto di tale uso è solo a livello di applicativo. In Windows 11, può essere che se l’applicativo non è in primo piano ignori le modifiche e usi il timing predefinito (15 millisecondi circa).
Di seguito un esempio (è l’esempio precedente con inserite le due funzioni), FATE ATTENZIONE CHE PER OGNI timebegin… ci deve essere un timeend…. ed è anche utile che l’uso dei timing così ristretti non sia esteso a tutta l’applicazione in quanto vengono richieste più risorse per questo uso.
procedure TForm1.Button1Click(Sender: TObject);
begin
//timebegin.. e timeend... devono essere chiamati con lo stesso argomento
timeBeginPeriod(1);
//In t1 vengono riportati i cicli attuali del processore
t1 := Cicli;
sleep(5);
t2 := Cicli;
//timebegin.. e timeend... devono essere chiamati con lo stesso argomento
timeEndPeriod(1);
//Microsecondi trascorsi
ShowMessage('diff = ' + IntToStr((t2-t1) div Ciclialus));
end;
Vedrete che lo sleep risulta più stabile, con un valore rilevato intorno ai 7 ms.
Se volete sapere quali limiti ha il timer di sistema e come è settato, usate l’utility ClockRes rilasciata da SysInternals a questo link: https://learn.microsoft.com/it-it/sysinternals/downloads/clockres
Se avete suggerimenti o volete fare qualche domanda riguardo questo articolo, potete usufruire del forum italiano su Lazarus e Free Pascal: