{"id":290,"date":"2024-10-01T06:10:25","date_gmt":"2024-10-01T13:10:25","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/go\/?p=290"},"modified":"2024-10-01T08:24:13","modified_gmt":"2024-10-01T15:24:13","slug":"high-resolution-timers-windows","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/go\/high-resolution-timers-windows\/","title":{"rendered":"High-Resolution Timers on Windows"},"content":{"rendered":"<p>The Go Windows port added support for high-resolution timers in Go 1.23, boosting resolution from ~15.6ms to ~0.5ms.\nThis enhancement affects <a href=\"https:\/\/pkg.go.dev\/time#Timer\">time.Timer<\/a>, <a href=\"https:\/\/pkg.go.dev\/time#Ticker\">time.Ticker<\/a>, and functions that put the goroutine to sleep, such as <a href=\"https:\/\/pkg.go.dev\/time#Sleep\">time.Sleep<\/a>.<\/p>\n<p><img decoding=\"async\" src=\".\/go-sleep.png\" alt=\"Sleeping gopher\" \/><\/p>\n<blockquote>\n<p>Gopher image by <a href=\"https:\/\/creativemarket.com\/Maria_Letta\">Maria Letta<\/a>, licensed under <a href=\"https:\/\/creativecommons.org\/public-domain\/cc0\/\">CC0 1.0<\/a>.<\/p>\n<\/blockquote>\n<p>Using high resolution timers on Windows has been a long-standing request, tracked in <a href=\"https:\/\/github.com\/golang\/go\/issues\/44343\">golang\/go#44343<\/a> and implemented by us (the <a href=\"https:\/\/devblogs.microsoft.com\/go\/welcome-to-the-microsoft-for-go-developers-blog\/\">Microsoft Go team<\/a>) in <a href=\"https:\/\/go-review.googlesource.com\/c\/go\/+\/488675\">CL 488675<\/a>. It is important to note that Go used high-resolution timers on Windows before Go 1.16, but they were removed in <a href=\"https:\/\/go-review.googlesource.com\/c\/go\/+\/232298\">CL 232298<\/a> because their implementation unfortunately conflicted with the Go scheduler, leading to latency issues. There wasn&#8217;t a good solution to this problem until recently.<\/p>\n<p>Let&#8217;s explore how high-resolution timers made their way back to Go&#8217;s standard library and why it&#8217;s important.<\/p>\n<h3>The Naive Approach<\/h3>\n<p>A basic <code>time.Sleep<\/code> implementation (let&#8217;s call it the naive <code>Sleep<\/code> from now on) that uses the Win32 high-resolution timer API might look like this:<\/p>\n<pre><code class=\"language-go\">func Sleep(d time.Duration) {\n    \/\/ Pseudo-code\n    timer, _ := CreateWaitableTimerExW(0, 0, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)\n    SetWaitableTimer(timer, d, 0, 0, 0, 0)\n    WaitForSingleObject(timer, windows.INFINITE)\n}<\/code><\/pre>\n<p>However, this approach blocks the OS thread, which doesn&#8217;t mesh well with Go&#8217;s concurrency model.\nGo&#8217;s goroutines are multiplexed onto a small number of threads.\nBlocking a thread directly can severely limit Go&#8217;s concurrency capabilities.<\/p>\n<p>Consider this code using the naive <code>Sleep<\/code> implementation:<\/p>\n<pre><code class=\"language-go\">func count() {\n    for i := 0; i &lt; 10; i++ {\n        Sleep(1 * time.Second)\n        fmt.Print(i)\n    }\n}\n\nfunc main() {\n    runtime.GOMAXPROCS(1) \/\/ Limit the number of threads to 1\n    go count()\n    Sleep(12 * time.Second)\n    fmt.Println(\"Done\")\n}<\/code><\/pre>\n<p>You might expect it to print <code>0123456789Done<\/code>, but it actually prints <code>Done<\/code>.\nWhy? Because the single thread is blocked by the naive <code>Sleep<\/code> in the main goroutine, preventing the count goroutine from running.<\/p>\n<p>Note that if the naive <code>Sleep<\/code> were implemented using the <code>syscall<\/code> package, this would work.\nThe Go runtime always schedules syscalls on a separate thread that doesn&#8217;t count against the <code>GOMAXPROCS<\/code> limit.\nUnfortunately, potentially creating a new thread for each timer is inefficient and could reduce the timer&#8217;s effective resolution, so this trick isn&#8217;t used in the Go standard library.<\/p>\n<h3>The Go Scheduler Solution<\/h3>\n<p>The <a href=\"https:\/\/pkg.go.dev\/time\">time<\/a> package integrates tightly with the Go scheduler, which is responsible for deciding which goroutine should be executed next and scheduling the goroutines to the kernel threads.\nWhen <code>time.Sleep<\/code> is called, the scheduler adds a virtual timer to a list of active timers.\nThe scheduler checks this list to wake up the associated goroutine when a timer expires.\nIf no work is pending and no timers have expired, the scheduler puts the current thread to sleep until the next timer&#8217;s expiration time.<\/p>\n<p>You might think the naive <code>Sleep<\/code> could work here, but new work might arrive before the timer expires.\nThe Go scheduler needs to wake up the sleeping thread to handle this new work, which isn&#8217;t possible with the naive implementation.<\/p>\n<p>On Windows, the Go scheduler solves the concurrent I\/O operations and timers issue by using <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/fileio\/i-o-completion-ports\">I\/O Completion Ports<\/a> (IOCP).\nIt calls <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/fileio\/getqueuedcompletionstatusex-func\">GetQueuedCompletionStatusEx<\/a> to wait for I\/O work up to a specified timeout, which is the next timer&#8217;s expiration time.\nSimplifying, the <code>time.Sleep<\/code> implementation before Go 1.23 was this:<\/p>\n<pre><code class=\"language-go\">func Sleep(d time.Duration) {\n    var entries [64]OVERLAPPED_ENTRY\n    var n int\n    err := GetQueuedCompletionStatusEx(iocph, &amp;entries[0], len(entries), &amp;n, d, 0)\n    if err == WAIT_TIMEOUT {\n        \/\/ A timer expired.\n    }\n    \/\/ There is new I\/O work to do.\n}<\/code><\/pre>\n<p>The problem is that this function&#8217;s resolution is ~15.6ms, which isn&#8217;t sufficient for the <code>time<\/code> package.<\/p>\n<h3>Enter High-Resolution Timers<\/h3>\n<p>As of Go 1.23, the Go scheduler associates a high-resolution timer with the IOCP port using the Windows API <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/devnotes\/ntassociatewaitcompletionpacket\">NtAssociateWaitCompletionPacket<\/a>.\nWhen the timer expires, the Windows kernel wakes up the sleeping thread &#8211;if it hasn&#8217;t already been woken up by new I\/O work arriving.\nThen, the Go scheduler uses the thread to run the sleeping goroutine.<\/p>\n<p>The solution is elegant and simple, and because it only uses APIs available since Windows 10, it works on all versions of Windows supported by the Go Windows port.\nIt would have been nice to implement this solution rather than dropping high-resolution timers in Go 1.16.\nIt was technically possible: the Windows APIs did already exist.\nHowever, <code>NtAssociateWaitCompletionPacket<\/code> was only documented in 2023, and relying on <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/w8cookbook\/undocumented-apis\">undocumented APIs<\/a> is not a good idea.<\/p>\n<p>Back to the simplified code, the final implementation looks like this:<\/p>\n<pre><code class=\"language-go\">func Sleep(d time.Duration) {\n    \/\/ Create high-resolution timer.\n    timer := CreateWaitableTimerExW(0, 0, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 0)\n    SetWaitableTimer(timer, d, 0, 0, 0, 0)\n\n    \/\/ Associate the timer with the IOCP port.\n    NtAssociateWaitCompletionPacket(waitiocphandle, iocph, timer, highResKey, 0, 0, 0)\n\n    \/\/ Wait for work or the timer to expire.\n    var entries [64]OVERLAPPED_ENTRY\n    var n int\n    err := GetQueuedCompletionStatusEx(iocph, &amp;entries[0], len(entries), &amp;n, 0, 0)\n    for _, entry := range entries[:n] {\n        if entry.Key == highResKey {\n            \/\/ A high-resolution timer expired.\n        }\n        \/\/ There is new I\/O work to do.\n    }\n}<\/code><\/pre>\n<h3>Why Is This Important?<\/h3>\n<p>High-resolution timers are crucial for applications that require precise timing.<\/p>\n<p>One example recently reported to the Go issue tracker is <a href=\"https:\/\/github.com\/golang\/go\/issues\/61665\">golang\/go#61665<\/a>, where the default Go CPU profiler sampling rate, 100 Hertz (one sample every 10 ms), was too high for the normal Windows timer resolution, leading to inaccurate profiling data.<\/p>\n<p>Another example reported to the Go issue tracker is <a href=\"https:\/\/github.com\/golang\/go\/issues\/44343#issuecomment-819032865\">golang\/go#44343<\/a>, in which <a href=\"https:\/\/pkg.dev.go\/golang.org\/x\/time\/rate\">golang.org\/x\/time\/rate<\/a> was limiting too aggressively on Windows when the rate limit was too high, leading to a lower throughput than expected.<\/p>\n<p>There are other situations where high-resolution timers aren&#8217;t necessary for correct behavior, but not having them can lead to unexpected slowdowns.\nIt is common for Go developers to call <code>time.Sleep(time.Millisecond)<\/code> when they want to wait for a short period, there are <a href=\"https:\/\/github.com\/search?q=language%3AGo+time.Sleep%28time.Millisecond%29&amp;type=code&amp;p=1\">at least 24,200 occurrences<\/a> on GitHub alone!\nThe worse cases are the ones that involve a loop, as even a harmless <code>for range 60 { time.Sleep(time.Millisecond) }<\/code> can take 1 second to complete instead of the intended 0.06 seconds.<\/p>\n<p>This enhancement is available as of Go 1.23, so make sure to update your Go installation to take advantage of it.<\/p>\n<p>Stay tuned for more updates, and happy coding! \ud83d\ude80<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Go Windows port added support for high-resolution timers in Go 1.23, boosting the resolution of time.Sleep from ~15.6ms to ~0.5ms.<\/p>\n","protected":false},"author":145999,"featured_media":307,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2,1],"tags":[4,7],"class_list":["post-290","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-community","category-go","tag-go","tag-windows"],"acf":[],"blog_post_summary":"<p>The Go Windows port added support for high-resolution timers in Go 1.23, boosting the resolution of time.Sleep from ~15.6ms to ~0.5ms.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/posts\/290","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/users\/145999"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/comments?post=290"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/posts\/290\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/media\/307"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/media?parent=290"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/categories?post=290"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/go\/wp-json\/wp\/v2\/tags?post=290"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}