time: optimize time.Time.Sub and time.Since
dsnet opened this issue · 12 comments
According to a profiling data from a large number of servers at Google, time.Time.Sub
is within the top 150 functions by CPU time. The current implementation of Sub
is not inlineable. The logic for Sub
is not particularly complicated and I believe it can be made to be inlined with some love.
time.Since
is also a hot function. It uses Sub
internally. Unfortunately, it can't be inlined since it makes a call to runtime.now
.
Oddly, looking at the output of -gcflags="-m -m"
, I don't see Time.Sub
listed at all, either as inlineable or non-inlineable.
CL https://golang.org/cl/32971 mentions this issue.
Time.Sub
currently has an inlining cost of 116, well above the current inlining threshold of 80. Some minor fiddling around (below) brings it down to 111. I don't see this happening without some compiler fixes; that is #17566. (Note that Time.Sub currently compiles into 227 bytes of code, about the same as two appends.)
// Add returns the time t+d.
func (t Time) Add(d Duration) Time {
t.sec += int64(d / 1e9)
t.nsec += int32(d % 1e9)
if t.nsec >= 1e9 {
t.sec++
t.nsec -= 1e9
} else if t.nsec < 0 {
t.sec--
t.nsec += 1e9
}
return t
}
// Sub returns the duration t-u. If the result exceeds the maximum (or minimum)
// value that can be stored in a Duration, the maximum (or minimum) duration
// will be returned.
// To compute t-d for a duration d, use t.Add(-d).
func (t Time) Sub(u Time) Duration {
d := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)
// Check for overflow or underflow.
switch {
case u.Add(d).Equal(t):
return d // d is correct
case t.Before(u):
return minDuration // t - u is negative out of range
}
return maxDuration // t - u is positive out of range
}
I looked at this earlier today, and I am suspicious of the current overflow checking logic. Sub
does overflow checking by checking that u.Add(d).Equal(t)
, which initially seems legit, but it's kinda strange since the Add
method itself does not do overflow checking. Thus, I find it strange that overflow checking is depending on a function that is not overflow safe.
This is a version of Sub
I came up with that is inlineable:
func (t Time) SubC(u Time) Duration {
sec := t.sec - u.sec
nsec := t.nsec - u.nsec // Cannot overflow; in range of [-999999999, 999999999]
d1 := Duration(sec) * Second
d2 := d1 + Duration(nsec) // Overflows only when nsec < 0
overflow := (sec < u.sec) != (t.sec > 0) || // Subtraction overflow?
(d1/Second) != Duration(sec) || // Multiplication overflow?
(d2 < d1) != (nsec < 0) // Addition overflow?
if !overflow {
return d2
}
// Overflow can only occur if number of seconds apart is large enough.
// Thus, we do not need to compare the nanoseconds.
if t.sec < u.sec {
return minDuration
} else {
return maxDuration
}
}
On my machine, this version runs 3.2x faster.
Sadly bumping this to Go 1.10.
The inclusion of monotonic timestamps doubled the complexity of the Sub
method such that my inlined version no longer works. I'm not sure how to optimize this anymore.
If this is ever made inlineable, be sure to add it to TestIntendedInlining
to ensure that it's kept that way - see #21851.
Change https://golang.org/cl/107056 mentions this issue: time: increase test coverage for Time.Sub
Change https://golang.org/cl/131196 mentions this issue: time: optimize Sub
https://golang.org/cl/131196 was reverted in https://golang.org/cl/190497 because it's time calculation was incorrect in some edge cases.
Change https://golang.org/cl/190524 mentions this issue: time: add TestSub test case to avoid future regressions