From e20106ac001a58e273738cb24ea61096384dc2f8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 22 Aug 2022 18:19:24 -0700 Subject: [PATCH] time: optimize FixedZone by caching unnamed zones by the hour MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit FixedZone is transitively called by Time.UnmarshalJSON or Time.UnmarshalText for any RFC 3339 timestamp that is not in UTC. This function is relatively slow since it allocates 3 times. Given that RFC 3339 never has a zone name and most offsets are by the hour, we can cache unnamed zones on hour offsets. Caching a Location should be safe since it has no exported fields or methods that can mutate the Location. It is functionally immutable. The only way to observe that the Location was cached is either by pointer comparison or by shallow copying the struct value. Neither operation seems sensible to do with a *time.Location. Performance: name old time/op new time/op delta UnmarshalText 268ns ± 2% 182ns ± 1% -32.01% (p=0.000 n=10+10) Change-Id: Iab5432f34bdbb485512bb8b5464e076c03fd106f Reviewed-on: https://go-review.googlesource.com/c/go/+/425116 TryBot-Result: Gopher Robot Reviewed-by: Michael Knyszek Run-TryBot: Joseph Tsai Auto-Submit: Joseph Tsai Reviewed-by: Ian Lance Taylor --- src/time/time_test.go | 8 ++++++++ src/time/zoneinfo.go | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/time/time_test.go b/src/time/time_test.go index 8ab79d3801..f2c6c3977e 100644 --- a/src/time/time_test.go +++ b/src/time/time_test.go @@ -1494,6 +1494,14 @@ func BenchmarkGoString(b *testing.B) { } } +func BenchmarkUnmarshalText(b *testing.B) { + var t Time + in := []byte("2020-08-22T11:27:43.123456789-02:00") + for i := 0; i < b.N; i++ { + t.UnmarshalText(in) + } +} + func TestMarshalBinaryZeroTime(t *testing.T) { t0 := Time{} enc, err := t0.MarshalBinary() diff --git a/src/time/zoneinfo.go b/src/time/zoneinfo.go index 3c4aac375b..dd3b4edd01 100644 --- a/src/time/zoneinfo.go +++ b/src/time/zoneinfo.go @@ -100,9 +100,30 @@ func (l *Location) String() string { return l.get().name } +var unnamedFixedZones []*Location +var unnamedFixedZonesOnce sync.Once + // FixedZone returns a Location that always uses // the given zone name and offset (seconds east of UTC). func FixedZone(name string, offset int) *Location { + // Most calls to FixedZone have an unnamed zone with an offset by the hour. + // Optimize for that case by returning the same *Location for a given hour. + const hoursBeforeUTC = 12 + const hoursAfterUTC = 14 + hour := offset / 60 / 60 + if name == "" && -hoursBeforeUTC <= hour && hour <= +hoursAfterUTC && hour*60*60 == offset { + unnamedFixedZonesOnce.Do(func() { + unnamedFixedZones = make([]*Location, hoursBeforeUTC+1+hoursAfterUTC) + for hr := -hoursBeforeUTC; hr <= +hoursAfterUTC; hr++ { + unnamedFixedZones[hr+hoursBeforeUTC] = fixedZone("", hr*60*60) + } + }) + return unnamedFixedZones[hour+hoursBeforeUTC] + } + return fixedZone(name, offset) +} + +func fixedZone(name string, offset int) *Location { l := &Location{ name: name, zone: []zone{{name, offset, false}}, -- 2.50.0