Skip to content

Commit 1c47049

Browse files
egonelbrealexbrainman
authored andcommitted
testing: use QueryPerformanceCounter on Windows
Windows time.Now granularity is around 0.5ms on modern systems, which introduces a significant noise into benchmark results. Instead of relying time.Now use QueryPerformanceCounter, which has significantly better granularity compared to time.Now. │ TimeNow-32 │ HighPrecisionTimeNow-32 │ │ sec/op │ sec/op vs base │ 4.812n ± 0% 30.580n ± 0% +535.43% (p=0.000 n=20) Fixes #31160 Change-Id: Ib2a574d638c9c6762a2524212def02265574e267 Reviewed-on: https://go-review.googlesource.com/c/go/+/557315 Reviewed-by: Quim Muntal <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alex Brainman <[email protected]> Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 9effeea commit 1c47049

File tree

9 files changed

+135
-23
lines changed

9 files changed

+135
-23
lines changed

src/internal/syscall/windows/syscall_windows.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,14 @@ func FinalPath(h syscall.Handle, flags uint32) (string, error) {
488488
}
489489
return syscall.UTF16ToString(buf), nil
490490
}
491+
492+
// QueryPerformanceCounter retrieves the current value of performance counter.
493+
//
494+
//go:linkname QueryPerformanceCounter
495+
func QueryPerformanceCounter() int64 // Implemented in runtime package.
496+
497+
// QueryPerformanceFrequency retrieves the frequency of the performance counter.
498+
// The returned value is represented as counts per second.
499+
//
500+
//go:linkname QueryPerformanceFrequency
501+
func QueryPerformanceFrequency() int64 // Implemented in runtime package.

src/runtime/os_windows.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
//go:cgo_import_dynamic runtime._LoadLibraryW LoadLibraryW%1 "kernel32.dll"
4444
//go:cgo_import_dynamic runtime._PostQueuedCompletionStatus PostQueuedCompletionStatus%4 "kernel32.dll"
4545
//go:cgo_import_dynamic runtime._QueryPerformanceCounter QueryPerformanceCounter%1 "kernel32.dll"
46+
//go:cgo_import_dynamic runtime._QueryPerformanceFrequency QueryPerformanceFrequency%1 "kernel32.dll"
4647
//go:cgo_import_dynamic runtime._RaiseFailFastException RaiseFailFastException%3 "kernel32.dll"
4748
//go:cgo_import_dynamic runtime._ResumeThread ResumeThread%1 "kernel32.dll"
4849
//go:cgo_import_dynamic runtime._RtlLookupFunctionEntry RtlLookupFunctionEntry%3 "kernel32.dll"
@@ -100,6 +101,7 @@ var (
100101
_LoadLibraryW,
101102
_PostQueuedCompletionStatus,
102103
_QueryPerformanceCounter,
104+
_QueryPerformanceFrequency,
103105
_RaiseFailFastException,
104106
_ResumeThread,
105107
_RtlLookupFunctionEntry,
@@ -246,6 +248,20 @@ func windowsLoadSystemLib(name []uint16) uintptr {
246248
return stdcall3(_LoadLibraryExW, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32)
247249
}
248250

251+
//go:linkname windows_QueryPerformanceCounter internal/syscall/windows.QueryPerformanceCounter
252+
func windows_QueryPerformanceCounter() int64 {
253+
var counter int64
254+
stdcall1(_QueryPerformanceCounter, uintptr(unsafe.Pointer(&counter)))
255+
return counter
256+
}
257+
258+
//go:linkname windows_QueryPerformanceFrequency internal/syscall/windows.QueryPerformanceFrequency
259+
func windows_QueryPerformanceFrequency() int64 {
260+
var frequency int64
261+
stdcall1(_QueryPerformanceFrequency, uintptr(unsafe.Pointer(&frequency)))
262+
return frequency
263+
}
264+
249265
func loadOptionalSyscalls() {
250266
bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:])
251267
if bcryptPrimitives == 0 {

src/testing/benchmark.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (b *B) StartTimer() {
123123
runtime.ReadMemStats(&memStats)
124124
b.startAllocs = memStats.Mallocs
125125
b.startBytes = memStats.TotalAlloc
126-
b.start = time.Now()
126+
b.start = highPrecisionTimeNow()
127127
b.timerOn = true
128128
}
129129
}
@@ -133,7 +133,7 @@ func (b *B) StartTimer() {
133133
// want to measure.
134134
func (b *B) StopTimer() {
135135
if b.timerOn {
136-
b.duration += time.Since(b.start)
136+
b.duration += highPrecisionTimeSince(b.start)
137137
runtime.ReadMemStats(&memStats)
138138
b.netAllocs += memStats.Mallocs - b.startAllocs
139139
b.netBytes += memStats.TotalAlloc - b.startBytes
@@ -156,7 +156,7 @@ func (b *B) ResetTimer() {
156156
runtime.ReadMemStats(&memStats)
157157
b.startAllocs = memStats.Mallocs
158158
b.startBytes = memStats.TotalAlloc
159-
b.start = time.Now()
159+
b.start = highPrecisionTimeNow()
160160
}
161161
b.duration = 0
162162
b.netAllocs = 0
@@ -325,7 +325,7 @@ func (b *B) launch() {
325325
func (b *B) Elapsed() time.Duration {
326326
d := b.duration
327327
if b.timerOn {
328-
d += time.Since(b.start)
328+
d += highPrecisionTimeSince(b.start)
329329
}
330330
return d
331331
}

src/testing/export_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
package testing
66

77
var PrettyPrint = prettyPrint
8+
9+
type HighPrecisionTime = highPrecisionTime
10+
11+
var HighPrecisionTimeNow = highPrecisionTimeNow

src/testing/fuzz.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ func fRunner(f *F, fn func(*F)) {
674674
}
675675
for root := &f.common; root.parent != nil; root = root.parent {
676676
root.mu.Lock()
677-
root.duration += time.Since(root.start)
677+
root.duration += highPrecisionTimeSince(root.start)
678678
d := root.duration
679679
root.mu.Unlock()
680680
root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
@@ -687,7 +687,7 @@ func fRunner(f *F, fn func(*F)) {
687687
}
688688

689689
// No panic or inappropriate Goexit.
690-
f.duration += time.Since(f.start)
690+
f.duration += highPrecisionTimeSince(f.start)
691691

692692
if len(f.sub) > 0 {
693693
// Unblock inputs that called T.Parallel while running the seed corpus.
@@ -700,9 +700,9 @@ func fRunner(f *F, fn func(*F)) {
700700
for _, sub := range f.sub {
701701
<-sub.signal
702702
}
703-
cleanupStart := time.Now()
703+
cleanupStart := highPrecisionTimeNow()
704704
err := f.runCleanup(recoverAndReturnPanic)
705-
f.duration += time.Since(cleanupStart)
705+
f.duration += highPrecisionTimeSince(cleanupStart)
706706
if err != nil {
707707
doPanic(err)
708708
}
@@ -719,7 +719,7 @@ func fRunner(f *F, fn func(*F)) {
719719
}
720720
}()
721721

722-
f.start = time.Now()
722+
f.start = highPrecisionTimeNow()
723723
f.resetRaces()
724724
fn(f)
725725

src/testing/testing.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -615,10 +615,10 @@ type common struct {
615615
isParallel bool // Whether the test is parallel.
616616

617617
parent *common
618-
level int // Nesting depth of test or benchmark.
619-
creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run.
620-
name string // Name of test or benchmark.
621-
start time.Time // Time test or benchmark started
618+
level int // Nesting depth of test or benchmark.
619+
creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run.
620+
name string // Name of test or benchmark.
621+
start highPrecisionTime // Time test or benchmark started
622622
duration time.Duration
623623
barrier chan bool // To signal parallel subtests they may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing).
624624
signal chan bool // To signal a test is done.
@@ -1457,7 +1457,7 @@ func (t *T) Parallel() {
14571457
// We don't want to include the time we spend waiting for serial tests
14581458
// in the test duration. Record the elapsed time thus far and reset the
14591459
// timer afterwards.
1460-
t.duration += time.Since(t.start)
1460+
t.duration += highPrecisionTimeSince(t.start)
14611461

14621462
// Add to the list of tests to be released by the parent.
14631463
t.parent.sub = append(t.parent.sub, t)
@@ -1486,8 +1486,8 @@ func (t *T) Parallel() {
14861486
if t.chatty != nil {
14871487
t.chatty.Updatef(t.name, "=== CONT %s\n", t.name)
14881488
}
1489-
running.Store(t.name, time.Now())
1490-
t.start = time.Now()
1489+
running.Store(t.name, highPrecisionTimeNow())
1490+
t.start = highPrecisionTimeNow()
14911491

14921492
// Reset the local race counter to ignore any races that happened while this
14931493
// goroutine was blocked, such as in the parent test or in other parallel
@@ -1619,7 +1619,7 @@ func tRunner(t *T, fn func(t *T)) {
16191619
// Flush the output log up to the root before dying.
16201620
for root := &t.common; root.parent != nil; root = root.parent {
16211621
root.mu.Lock()
1622-
root.duration += time.Since(root.start)
1622+
root.duration += highPrecisionTimeSince(root.start)
16231623
d := root.duration
16241624
root.mu.Unlock()
16251625
root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
@@ -1634,7 +1634,7 @@ func tRunner(t *T, fn func(t *T)) {
16341634
doPanic(err)
16351635
}
16361636

1637-
t.duration += time.Since(t.start)
1637+
t.duration += highPrecisionTimeSince(t.start)
16381638

16391639
if len(t.sub) > 0 {
16401640
// Run parallel subtests.
@@ -1652,10 +1652,10 @@ func tRunner(t *T, fn func(t *T)) {
16521652

16531653
// Run any cleanup callbacks, marking the test as running
16541654
// in case the cleanup hangs.
1655-
cleanupStart := time.Now()
1655+
cleanupStart := highPrecisionTimeNow()
16561656
running.Store(t.name, cleanupStart)
16571657
err := t.runCleanup(recoverAndReturnPanic)
1658-
t.duration += time.Since(cleanupStart)
1658+
t.duration += highPrecisionTimeSince(cleanupStart)
16591659
if err != nil {
16601660
doPanic(err)
16611661
}
@@ -1684,7 +1684,7 @@ func tRunner(t *T, fn func(t *T)) {
16841684
}
16851685
}()
16861686

1687-
t.start = time.Now()
1687+
t.start = highPrecisionTimeNow()
16881688
t.resetRaces()
16891689
fn(t)
16901690

@@ -1732,7 +1732,7 @@ func (t *T) Run(name string, f func(t *T)) bool {
17321732
if t.chatty != nil {
17331733
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
17341734
}
1735-
running.Store(t.name, time.Now())
1735+
running.Store(t.name, highPrecisionTimeNow())
17361736

17371737
// Instead of reducing the running count of this test before calling the
17381738
// tRunner and increasing it afterwards, we rely on tRunner keeping the
@@ -2372,7 +2372,7 @@ func (m *M) startAlarm() time.Time {
23722372
func runningList() []string {
23732373
var list []string
23742374
running.Range(func(k, v any) bool {
2375-
list = append(list, fmt.Sprintf("%s (%v)", k.(string), time.Since(v.(time.Time)).Round(time.Second)))
2375+
list = append(list, fmt.Sprintf("%s (%v)", k.(string), highPrecisionTimeSince(v.(highPrecisionTime)).Round(time.Second)))
23762376
return true
23772377
})
23782378
sort.Strings(list)

src/testing/testing_other.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,26 @@
66

77
package testing
88

9+
import "time"
10+
911
// isWindowsRetryable reports whether err is a Windows error code
1012
// that may be fixed by retrying a failed filesystem operation.
1113
func isWindowsRetryable(err error) bool {
1214
return false
1315
}
16+
17+
// highPrecisionTime represents a single point in time.
18+
// On all systems except Windows, using time.Time is fine.
19+
type highPrecisionTime struct {
20+
now time.Time
21+
}
22+
23+
// highPrecisionTimeNow returns high precision time for benchmarking.
24+
func highPrecisionTimeNow() highPrecisionTime {
25+
return highPrecisionTime{now: time.Now()}
26+
}
27+
28+
// highPrecisionTimeSince returns duration since b.
29+
func highPrecisionTimeSince(b highPrecisionTime) time.Duration {
30+
return time.Now().Sub(b.now)
31+
}

src/testing/testing_windows.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ package testing
99
import (
1010
"errors"
1111
"internal/syscall/windows"
12+
"math/bits"
1213
"syscall"
14+
"time"
1315
)
1416

1517
// isWindowsRetryable reports whether err is a Windows error code
@@ -30,3 +32,39 @@ func isWindowsRetryable(err error) bool {
3032
}
3133
return false
3234
}
35+
36+
// highPrecisionTime represents a single point in time with query performance counter.
37+
// time.Time on Windows has low system granularity, which is not suitable for
38+
// measuring short time intervals.
39+
//
40+
// TODO: If Windows runtime implements high resolution timing then highPrecisionTime
41+
// can be removed.
42+
type highPrecisionTime struct {
43+
now int64
44+
}
45+
46+
// highPrecisionTimeNow returns high precision time for benchmarking.
47+
func highPrecisionTimeNow() highPrecisionTime {
48+
var t highPrecisionTime
49+
// This should always succeed for Windows XP and above.
50+
t.now = windows.QueryPerformanceCounter()
51+
return t
52+
}
53+
54+
func (a highPrecisionTime) sub(b highPrecisionTime) time.Duration {
55+
delta := a.now - b.now
56+
57+
if queryPerformanceFrequency == 0 {
58+
queryPerformanceFrequency = windows.QueryPerformanceFrequency()
59+
}
60+
hi, lo := bits.Mul64(uint64(delta), uint64(time.Second)/uint64(time.Nanosecond))
61+
quo, _ := bits.Div64(hi, lo, uint64(queryPerformanceFrequency))
62+
return time.Duration(quo)
63+
}
64+
65+
var queryPerformanceFrequency int64
66+
67+
// highPrecisionTimeSince returns duration since a.
68+
func highPrecisionTimeSince(a highPrecisionTime) time.Duration {
69+
return highPrecisionTimeNow().sub(a)
70+
}

src/testing/testing_windows_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package testing_test
6+
7+
import (
8+
"testing"
9+
"time"
10+
)
11+
12+
var sink time.Time
13+
var sinkHPT testing.HighPrecisionTime
14+
15+
func BenchmarkTimeNow(b *testing.B) {
16+
for i := 0; i < b.N; i++ {
17+
sink = time.Now()
18+
}
19+
}
20+
21+
func BenchmarkHighPrecisionTimeNow(b *testing.B) {
22+
for i := 0; i < b.N; i++ {
23+
sinkHPT = testing.HighPrecisionTimeNow()
24+
}
25+
}

0 commit comments

Comments
 (0)