Last active
September 12, 2025 12:05
-
-
Save tcantenot/47e211e0e8dce95a33201411ead440e4 to your computer and use it in GitHub Desktop.
Precise sleep function on Windows written in C using high resolution timers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "precise_sleep.h" | |
| int main() | |
| { | |
| const int use_nt_set_timer_resolution = 1; | |
| InitPreciseSleepFunction(use_nt_set_timer_resolution); | |
| // Your app code that uses PreciseSleep(sleep_duration_s); | |
| DeinitPreciseSleepFunction(); | |
| return 0; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "precise_sleep.h" | |
| #include <Windows.h> | |
| #pragma comment(lib, "Winmm.lib") // timeGetDevCaps, timeBeginPeriod | |
| #ifndef K_ENABLE_PRECISE_SLEEP_LOGGING | |
| #define K_ENABLE_PRECISE_SLEEP_LOGGING 0 // Enable/Disable logging | |
| #endif | |
| #if K_ENABLE_PRECISE_SLEEP_LOGGING | |
| #include <stdio.h> | |
| #define K_PRECISE_SLEEP_LOG(...) printf(__VA_ARGS__) | |
| #define K_PRECISE_SLEEP_ERR(...) fprintf(stderr, __VA_ARGS__) | |
| #else | |
| #define K_PRECISE_SLEEP_LOG(...) (void)sizeof(0, __VA_ARGS__) | |
| #define K_PRECISE_SLEEP_ERR(...) (void)sizeof(0, __VA_ARGS__) | |
| #endif | |
| // Performance-counter frequency in "counts/second" | |
| static INT64 g_performance_counter_frequency; | |
| // Scheduler period in ms | |
| static double g_scheduler_period_ms; | |
| // Windows high resolution timer | |
| static HANDLE g_high_resolution_timer; | |
| // Max sleep time we perform to keep high precision (in 100ns) | |
| static INT64 g_high_resolution_timer_max_sleep_time_100ns; | |
| // Conversion factor between performance counter and '100ns' | |
| static double g_pc_to_100ns; | |
| // Tolerance to avoid overshooting due to timer setup overhead (in "counts/second") | |
| static INT64 g_high_resolution_timer_tolerance_pc; | |
| // Tolerance to avoid overshooting due to timer setup overhead (in 100ns) | |
| static INT64 g_high_resolution_timer_tolerance_100ns; | |
| // Tells whether we successfully set the scheduler period via NtSetTimerResolution | |
| static BOOLEAN g_is_using_nt_timer_resolution = FALSE; | |
| // Previous timer resolution returned by NtQueryTimerResolution | |
| static ULONG g_prev_timer_resolution_100ns = 0; | |
| // High precision sleep function using Windows high resolution timer | |
| void PreciseSleep_HighResolutionTimer(double seconds) | |
| { | |
| LARGE_INTEGER pc; | |
| QueryPerformanceCounter(&pc); | |
| const INT64 target_pc = (INT64)(pc.QuadPart + seconds * g_performance_counter_frequency); | |
| INT64 remaining_sleep_time_pc = target_pc - pc.QuadPart; | |
| while(remaining_sleep_time_pc > g_high_resolution_timer_tolerance_pc) | |
| { | |
| INT64 remaining_sleep_time_100ns = (INT64)(remaining_sleep_time_pc * g_pc_to_100ns - g_high_resolution_timer_tolerance_100ns); | |
| // Split the sleep time in intervals of time representing a fraction of the scheduler period to avoid oversleep | |
| if(remaining_sleep_time_100ns > g_high_resolution_timer_max_sleep_time_100ns) | |
| remaining_sleep_time_100ns = g_high_resolution_timer_max_sleep_time_100ns; | |
| // SetWaitableTimerEx expected a due_time_100ns time multiple of 100ns: | |
| // - positive values indicate absolute time. | |
| // - negative values indicate relative time. | |
| // https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer | |
| LARGE_INTEGER due_time_100ns; | |
| due_time_100ns.QuadPart = -remaining_sleep_time_100ns; | |
| SetWaitableTimerEx(g_high_resolution_timer, &due_time_100ns, 0, NULL, NULL, NULL, 0); | |
| WaitForSingleObject(g_high_resolution_timer, INFINITE); | |
| QueryPerformanceCounter(&pc); | |
| remaining_sleep_time_pc = target_pc - pc.QuadPart; | |
| } | |
| while(pc.QuadPart < target_pc) // Spin for any remaining time | |
| { | |
| YieldProcessor(); | |
| QueryPerformanceCounter(&pc); | |
| } | |
| } | |
| // High precision sleep function using the system Sleep | |
| void PreciseSleep_SystemSleep(double seconds) | |
| { | |
| LARGE_INTEGER pc; | |
| QueryPerformanceCounter(&pc); | |
| const INT64 target_pc = (INT64)(pc.QuadPart + seconds * g_performance_counter_frequency); | |
| const double k_sleep_tolerance_s = 0.000'02; | |
| const double sleep_duration_ms = (seconds - k_sleep_tolerance_s) * 1000.0 - g_scheduler_period_ms; // Sleep for 1 scheduler period less than requested. | |
| const int num_sleep_slices = (int)(sleep_duration_ms / g_scheduler_period_ms); | |
| if(num_sleep_slices > 0) | |
| Sleep((DWORD)(num_sleep_slices * g_scheduler_period_ms)); | |
| QueryPerformanceCounter(&pc); | |
| while(pc.QuadPart < target_pc) // Spin for any remaining time | |
| { | |
| YieldProcessor(); | |
| QueryPerformanceCounter(&pc); | |
| } | |
| } | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #endif | |
| // Let the linker import the functions | |
| NTSYSAPI NTSTATUS NTAPI NtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); | |
| NTSYSAPI NTSTATUS NTAPI NtQueryTimerResolution(PULONG MinimumResolution, PULONG MaximumResolution, PULONG CurrentResolution); | |
| #ifdef __cplusplus | |
| } | |
| #endif | |
| #pragma comment(lib, "ntdll.lib") | |
| // Precise sleep function | |
| void(*PreciseSleep)(double seconds) = NULL; | |
| void InitPreciseSleepFunction(int use_nt_set_timer_resolution) | |
| { | |
| g_is_using_nt_timer_resolution = FALSE; | |
| // First try to set a potentially more precise scheduler period via (the undocumented) NtSetTimerResolution | |
| // http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Time/NtQueryTimerResolution.html | |
| // http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Time/NtSetTimerResolution.html | |
| if(use_nt_set_timer_resolution) | |
| { | |
| ULONG min_timer_resolution_100ns = 0; | |
| ULONG max_timer_resolution_100ns = 0; | |
| ULONG curr_timer_resolution_100ns = 0; | |
| if(NtQueryTimerResolution(&min_timer_resolution_100ns, &max_timer_resolution_100ns, &curr_timer_resolution_100ns) == 0) | |
| { | |
| K_PRECISE_SLEEP_LOG("NtQueryTimerResolution: current resolution = %lu | range = [%lu, %lu]\n", curr_timer_resolution_100ns, max_timer_resolution_100ns, min_timer_resolution_100ns); | |
| ULONG actual_timer_resolution_100ns; | |
| if(NtSetTimerResolution(max_timer_resolution_100ns, TRUE, &actual_timer_resolution_100ns) == 0) | |
| { | |
| const double curr_timer_resolution_ms = actual_timer_resolution_100ns / 10'000.0; | |
| g_scheduler_period_ms = curr_timer_resolution_ms; | |
| g_is_using_nt_timer_resolution = TRUE; | |
| } | |
| else | |
| { | |
| K_PRECISE_SLEEP_ERR("NtSetTimerResolution failed.\n"); | |
| } | |
| } | |
| else | |
| { | |
| K_PRECISE_SLEEP_ERR("NtQueryTimerResolution failed.\n"); | |
| } | |
| } | |
| if(!g_is_using_nt_timer_resolution) | |
| { | |
| // https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timegetdevcaps | |
| TIMECAPS caps; | |
| MMRESULT r = timeGetDevCaps(&caps, sizeof caps); | |
| if(r != TIMERR_NOERROR) | |
| K_PRECISE_SLEEP_ERR("timeGetDevCaps failed with error %u\n", r); | |
| g_scheduler_period_ms = caps.wPeriodMin; | |
| K_PRECISE_SLEEP_LOG("InitPreciseSleepFunction: timeBeginPeriod(%f)\n", g_scheduler_period_ms); | |
| r = timeBeginPeriod((UINT)g_scheduler_period_ms); | |
| if(r != TIMERR_NOERROR) | |
| K_PRECISE_SLEEP_ERR("timeBeginPeriod(%f) failed with error %u\n", g_scheduler_period_ms, r); | |
| } | |
| K_PRECISE_SLEEP_LOG("Scheduler period = %fms\n", g_scheduler_period_ms); | |
| LARGE_INTEGER qpf; | |
| QueryPerformanceFrequency(&qpf); | |
| g_performance_counter_frequency = qpf.QuadPart; | |
| // 'Performance counter' -> '100ns' conversion factor | |
| g_pc_to_100ns = 10'000'000.0 / g_performance_counter_frequency; | |
| g_high_resolution_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); | |
| const double high_resolution_timer_tolerance_s = (g_scheduler_period_ms + 0.02) / 1000.0; | |
| g_high_resolution_timer_tolerance_100ns = (INT64)(high_resolution_timer_tolerance_s * 10'000'000); | |
| g_high_resolution_timer_tolerance_pc = (INT64)(high_resolution_timer_tolerance_s * g_performance_counter_frequency); | |
| // Split the sleep time in intervals of time representing 95% of the scheduler period | |
| // > "High resolution timer has a quirk that if you request a sleep period longer than | |
| // the system timer period, the precision of the timer plummets." | |
| // (https://blog.bearcats.nl/perfect-sleep-function/) | |
| g_high_resolution_timer_max_sleep_time_100ns = (INT64)g_scheduler_period_ms * 9'500; // 0.95 * ms_to_100ns = 0.95 * 10'000 | |
| if(g_high_resolution_timer) | |
| PreciseSleep = PreciseSleep_HighResolutionTimer; | |
| else | |
| PreciseSleep = PreciseSleep_SystemSleep; | |
| } | |
| void DeinitPreciseSleepFunction() | |
| { | |
| if(g_is_using_nt_timer_resolution) | |
| { | |
| ULONG actual_timer_resolution_100ns; | |
| if(NtSetTimerResolution(g_prev_timer_resolution_100ns, TRUE, &actual_timer_resolution_100ns) != 0) | |
| { | |
| K_PRECISE_SLEEP_ERR("NtSetTimerResolution failed.\n"); | |
| } | |
| } | |
| else | |
| { | |
| // https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod (in 'Remark' section) | |
| // Call this function immediately before using timer services, and call the timeEndPeriod function immediately after you are finished using the timer services. | |
| // You must match each call to timeBeginPeriod with a call to timeEndPeriod, specifying the same minimum resolution in both calls. | |
| K_PRECISE_SLEEP_LOG("DeinitPreciseSleepFunction: timeEndPeriod(%f)\n", g_scheduler_period_ms); | |
| const MMRESULT r = timeEndPeriod((UINT)g_scheduler_period_ms); | |
| if(r != TIMERR_NOERROR) | |
| K_PRECISE_SLEEP_ERR("timeEndPeriod(%f) failed with error %u\n", g_scheduler_period_ms, r); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #ifndef PRECISE_SLEEP_H | |
| #define PRECISE_SLEEP_H | |
| extern void(*PreciseSleep)(double seconds); | |
| void InitPreciseSleepFunction(int use_nt_set_timer_resolution); | |
| void DeinitPreciseSleepFunction(); | |
| #endif //PRECISE_SLEEP_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment