[go] time: optimize parsing of common fixed-width layouts

2 views
Skip to first unread message

Chencheng Jiang (Gerrit)

unread,
Jul 17, 2026, 1:00:43 PM (4 days ago) Jul 17
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Chencheng Jiang has uploaded the change for review

Commit message

time: optimize parsing of common fixed-width layouts

Add specialized parsing paths for the exact fixed-width forms of DateTime, DateOnly, and TimeOnly. Values outside those forms continue through the general parser, preserving fractional-second handling and existing errors.

On linux/amd64, representative results are:

ParseDateTime 70.8 ns/op -> 22.2 ns/op (-69%)
ParseDateOnly 42.4 ns/op -> 14.6 ns/op (-66%)
ParseTimeOnly 37.2 ns/op -> 14.5 ns/op (-61%)

All paths remain allocation-free. Parsing unrelated layouts and the existing RFC3339 fast paths is unchanged within benchmark noise.

Add differential tests and fuzz coverage comparing the specialized paths with the general parser.

Fixes #80437
Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948

Change diff

diff --git a/src/time/format.go b/src/time/format.go
index cf5d6c7..f710ecf 100644
--- a/src/time/format.go
+++ b/src/time/format.go
@@ -1024,11 +1024,24 @@
// differ by the actual zone offset. To avoid such problems, prefer time layouts
// that use a numeric zone offset, or use [ParseInLocation].
func Parse(layout, value string) (Time, error) {
- // Optimize for RFC3339 as it accounts for over half of all representations.
- if layout == RFC3339 || layout == RFC3339Nano {
+ switch layout {
+ case RFC3339, RFC3339Nano:
+ // Optimize for RFC3339 as it accounts for over half of all representations.
if t, ok := parseRFC3339(value, Local); ok {
return t, nil
}
+ case DateTime:
+ if t, ok := parseDateTime(value, UTC); ok {
+ return t, nil
+ }
+ case DateOnly:
+ if t, ok := parseDateOnly(value, UTC); ok {
+ return t, nil
+ }
+ case TimeOnly:
+ if t, ok := parseTimeOnly(value, UTC); ok {
+ return t, nil
+ }
}
return parse(layout, value, UTC, Local)
}
@@ -1039,11 +1052,24 @@
// Second, when given a zone offset or abbreviation, Parse tries to match it
// against the Local location; ParseInLocation uses the given location.
func ParseInLocation(layout, value string, loc *Location) (Time, error) {
- // Optimize for RFC3339 as it accounts for over half of all representations.
- if layout == RFC3339 || layout == RFC3339Nano {
+ switch layout {
+ case RFC3339, RFC3339Nano:
+ // Optimize for RFC3339 as it accounts for over half of all representations.
if t, ok := parseRFC3339(value, loc); ok {
return t, nil
}
+ case DateTime:
+ if t, ok := parseDateTime(value, loc); ok {
+ return t, nil
+ }
+ case DateOnly:
+ if t, ok := parseDateOnly(value, loc); ok {
+ return t, nil
+ }
+ case TimeOnly:
+ if t, ok := parseTimeOnly(value, loc); ok {
+ return t, nil
+ }
}
return parse(layout, value, loc, loc)
}
diff --git a/src/time/format_datetime.go b/src/time/format_datetime.go
new file mode 100644
index 0000000..9d1cbda
--- /dev/null
+++ b/src/time/format_datetime.go
@@ -0,0 +1,92 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package time
+
+// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,
+// and TimeOnly. Parse and ParseInLocation fall back to the general parser for
+// other accepted forms, such as times with fractional seconds.
+
+func parseDateTime(value string, loc *Location) (Time, bool) {
+ if len(value) != len(DateTime) ||
+ value[4] != '-' || value[7] != '-' || value[10] != ' ' ||
+ value[13] != ':' || value[16] != ':' {
+ return Time{}, false
+ }
+ year, month, day, ok := parseDateFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ hour, min, sec, ok := parseTimeFields(value[11:])
+ if !ok {
+ return Time{}, false
+ }
+ return Date(year, month, day, hour, min, sec, 0, loc), true
+}
+
+func parseDateOnly(value string, loc *Location) (Time, bool) {
+ if len(value) != len(DateOnly) || value[4] != '-' || value[7] != '-' {
+ return Time{}, false
+ }
+ year, month, day, ok := parseDateFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ return Date(year, month, day, 0, 0, 0, 0, loc), true
+}
+
+func parseTimeOnly(value string, loc *Location) (Time, bool) {
+ if len(value) != len(TimeOnly) || value[2] != ':' || value[5] != ':' {
+ return Time{}, false
+ }
+ hour, min, sec, ok := parseTimeFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ return Date(0, January, 1, hour, min, sec, 0, loc), true
+}
+
+func parseDateFields(value string) (year int, month Month, day int, ok bool) {
+ year, ok = parseFixedDigits(value[:4])
+ if !ok {
+ return 0, 0, 0, false
+ }
+ monthNumber, ok := parseFixedDigits(value[5:7])
+ if !ok || monthNumber < 1 || monthNumber > 12 {
+ return 0, 0, 0, false
+ }
+ month = Month(monthNumber)
+ day, ok = parseFixedDigits(value[8:10])
+ if !ok || day < 1 || day > daysIn(month, year) {
+ return 0, 0, 0, false
+ }
+ return year, month, day, true
+}
+
+func parseTimeFields(value string) (hour, min, sec int, ok bool) {
+ hour, ok = parseFixedDigits(value[:2])
+ if !ok || hour >= 24 {
+ return 0, 0, 0, false
+ }
+ min, ok = parseFixedDigits(value[3:5])
+ if !ok || min >= 60 {
+ return 0, 0, 0, false
+ }
+ sec, ok = parseFixedDigits(value[6:8])
+ if !ok || sec >= 60 {
+ return 0, 0, 0, false
+ }
+ return hour, min, sec, true
+}
+
+func parseFixedDigits(value string) (int, bool) {
+ var n int
+ for i := range len(value) {
+ if value[i] < '0' || value[i] > '9' {
+ return 0, false
+ }
+ n = n*10 + int(value[i]-'0')
+ }
+ return n, true
+}
diff --git a/src/time/format_test.go b/src/time/format_test.go
index 2537c76..3f87c84 100644
--- a/src/time/format_test.go
+++ b/src/time/format_test.go
@@ -366,6 +366,62 @@
}
}

+var commonParseLocation = FixedZone("Common", 4*60*60+30*60)
+
+func testParseCommonLayout(t *testing.T, layout, value string) {
+ t.Helper()
+
+ check := func(name string, got Time, gotErr error, want Time, wantErr error) {
+ t.Helper()
+ switch {
+ case (gotErr == nil) != (wantErr == nil):
+ t.Fatalf("%s(%q, %q) error mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, gotErr, wantErr)
+ case gotErr != nil && gotErr.Error() != wantErr.Error():
+ t.Fatalf("%s(%q, %q) error mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, gotErr, wantErr)
+ case got != want:
+ t.Fatalf("%s(%q, %q) value mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, got, want)
+ }
+ }
+
+ got, gotErr := Parse(layout, value)
+ want, wantErr := ParseAny(layout, value, UTC, Local)
+ check("Parse", got, gotErr, want, wantErr)
+
+ got, gotErr = ParseInLocation(layout, value, commonParseLocation)
+ want, wantErr = ParseAny(layout, value, commonParseLocation, commonParseLocation)
+ check("ParseInLocation", got, gotErr, want, wantErr)
+}
+
+func TestParseCommonLayouts(t *testing.T) {
+ tests := []struct {
+ layout string
+ value string
+ }{
+ {DateTime, "0000-01-01 00:00:00"},
+ {DateTime, "2000-02-29 23:59:59"},
+ {DateTime, "9999-12-31 23:59:59"},
+ {DateTime, "2000-02-29 23:59:59.123456789"},
+ {DateTime, "2000-02-29 23:59:59"},
+ {DateTime, "2001-02-29 23:59:59"},
+ {DateTime, "2000-02-29T23:59:59"},
+ {DateOnly, "0000-01-01"},
+ {DateOnly, "2000-02-29"},
+ {DateOnly, "9999-12-31"},
+ {DateOnly, "2001-02-29"},
+ {DateOnly, "2000/02/29"},
+ {TimeOnly, "00:00:00"},
+ {TimeOnly, "23:59:59"},
+ {TimeOnly, "23:59:59.123456789"},
+ {TimeOnly, "24:00:00"},
+ {TimeOnly, "23-59-59"},
+ }
+ for _, test := range tests {
+ t.Run(test.layout+"/"+test.value, func(t *testing.T) {
+ testParseCommonLayout(t, test.layout, test.value)
+ })
+ }
+}
+
// All parsed with ANSIC.
var dayOutOfRangeTests = []struct {
date string
@@ -1091,3 +1147,17 @@
}
})
}
+
+func FuzzParseCommonLayouts(f *testing.F) {
+ f.Add(uint8(0), "2000-02-29 23:59:59")
+ f.Add(uint8(0), "2000-02-29 23:59:59.123456789")
+ f.Add(uint8(1), "2000-02-29")
+ f.Add(uint8(1), "2001-02-29")
+ f.Add(uint8(2), "23:59:59")
+ f.Add(uint8(2), "23:59:59.123456789")
+
+ f.Fuzz(func(t *testing.T, layoutIndex uint8, value string) {
+ layouts := [...]string{DateTime, DateOnly, TimeOnly}
+ testParseCommonLayout(t, layouts[int(layoutIndex)%len(layouts)], value)
+ })
+}
diff --git a/src/time/time_test.go b/src/time/time_test.go
index 5cd7471..4dcea6a 100644
--- a/src/time/time_test.go
+++ b/src/time/time_test.go
@@ -1581,6 +1581,24 @@
}
}

+func BenchmarkParseDateTime(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(DateTime, "2026-07-16 16:42:31")
+ }
+}
+
+func BenchmarkParseDateOnly(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(DateOnly, "2026-07-16")
+ }
+}
+
+func BenchmarkParseTimeOnly(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(TimeOnly, "16:42:31")
+ }
+}
+
const testdataRFC3339UTC = "2020-08-22T11:27:43.123456789Z"

func BenchmarkParseRFC3339UTC(b *testing.B) {

Change information

Files:
  • M src/time/format.go
  • A src/time/format_datetime.go
  • M src/time/format_test.go
  • M src/time/time_test.go
Change size: M
Delta: 4 files changed, 210 insertions(+), 4 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948
Gerrit-Change-Number: 802240
Gerrit-PatchSet: 1
Gerrit-Owner: Chencheng Jiang <dorb...@gmail.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Sean Liao (Gerrit)

unread,
Jul 21, 2026, 2:42:02 PM (10 hours ago) Jul 21
to Chencheng Jiang, goph...@pubsubhelper.golang.org, Russ Cox, Gopher Robot, golang-co...@googlegroups.com
Attention needed from Chencheng Jiang and Russ Cox

Sean Liao added 3 comments

Commit Message
Line 9, Patchset 1 (Latest):Add specialized parsing paths for the exact fixed-width forms of DateTime, DateOnly, and TimeOnly. Values outside those forms continue through the general parser, preserving fractional-second handling and existing errors.
Line 13, Patchset 1 (Latest):ParseDateTime 70.8 ns/op -> 22.2 ns/op (-69%)
File src/time/format_datetime.go
Line 7, Patchset 1 (Latest):// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,
Sean Liao . unresolved

given that these share the same general format, shouldn't it be possible to share code with parseRFC3339 ?

Open in Gerrit

Related details

Attention is currently required from:
  • Chencheng Jiang
  • Russ Cox
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948
    Gerrit-Change-Number: 802240
    Gerrit-PatchSet: 1
    Gerrit-Owner: Chencheng Jiang <dorb...@gmail.com>
    Gerrit-Reviewer: Russ Cox <r...@golang.org>
    Gerrit-CC: Gopher Robot <go...@golang.org>
    Gerrit-CC: Sean Liao <se...@liao.dev>
    Gerrit-Attention: Russ Cox <r...@golang.org>
    Gerrit-Attention: Chencheng Jiang <dorb...@gmail.com>
    Gerrit-Comment-Date: Tue, 21 Jul 2026 18:41:52 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    open
    diffy

    Chencheng Jiang (Gerrit)

    unread,
    Jul 21, 2026, 4:43:25 PM (8 hours ago) Jul 21
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Chencheng Jiang and Russ Cox

    Chencheng Jiang uploaded new patchset

    Chencheng Jiang uploaded patch set #2 to this change.
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Chencheng Jiang
    • Russ Cox
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948
    Gerrit-Change-Number: 802240
    Gerrit-PatchSet: 2
    unsatisfied_requirement
    open
    diffy

    Chencheng Jiang (Gerrit)

    unread,
    Jul 21, 2026, 4:44:18 PM (8 hours ago) Jul 21
    to goph...@pubsubhelper.golang.org, Russ Cox, Gopher Robot, golang-co...@googlegroups.com
    Attention needed from Russ Cox and Sean Liao

    Chencheng Jiang added 3 comments

    Commit Message
    Line 9, Patchset 2 (Latest):Add specialized parsing paths for the exact fixed-width forms of
    Chencheng Jiang . resolved

    Done. Wrapped the commit message at 72 columns.

    Line 13, Patchset 2 (Latest):
    Chencheng Jiang . resolved

    Done. Replaced the hand-written benchmark summary with fresh benchstat output using 10 samples per side.

    File src/time/format_datetime.go
    Line 7, Patchset 2 (Latest):// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,
    Chencheng Jiang . resolved

    Done. Added a generic fixed-width date/time field parser shared by DateTime and parseRFC3339, while keeping the subset parsers specialized to avoid regressing the existing RFC3339 fast path.

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Russ Cox
    • Sean Liao
    Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement is not satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: go
      Gerrit-Branch: master
      Gerrit-Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948
      Gerrit-Change-Number: 802240
      Gerrit-PatchSet: 2
      Gerrit-Owner: Chencheng Jiang <dorb...@gmail.com>
      Gerrit-Reviewer: Russ Cox <r...@golang.org>
      Gerrit-CC: Gopher Robot <go...@golang.org>
      Gerrit-CC: Sean Liao <se...@liao.dev>
      Gerrit-Attention: Russ Cox <r...@golang.org>
      Gerrit-Attention: Sean Liao <se...@liao.dev>
      Gerrit-Comment-Date: Tue, 21 Jul 2026 20:44:13 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      unsatisfied_requirement
      satisfied_requirement
      open
      diffy

      Sean Liao (Gerrit)

      unread,
      Jul 21, 2026, 6:26:19 PM (6 hours ago) Jul 21
      to Chencheng Jiang, goph...@pubsubhelper.golang.org, Russ Cox, Gopher Robot, golang-co...@googlegroups.com
      Attention needed from Chencheng Jiang and Russ Cox

      Sean Liao voted and added 2 comments

      Votes added by Sean Liao

      Commit-Queue+1

      2 comments

      Commit Message
      Line 27, Patchset 2 (Latest):ParseTimeOnly 35.92n ± 2% 13.90n ± 3% -61.30% (p=0.000 n=10)
      Sean Liao . unresolved

      This should also include the RFC3339 benchmarks since the implementation changes.
      and include -benchmem.

      File src/time/format_datetime.go
      Line 46, Patchset 2 (Latest):func parseDateTimeFields[bytes []byte | string](value bytes) (year int, month Month, day, hour, min, sec int, ok bool) {
      Sean Liao . unresolved

      why can't this call the other 2 funcs instead of duplicating the code?

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Chencheng Jiang
      • Russ Cox
      Submit Requirements:
        • requirement is not satisfiedCode-Review
        • requirement is not satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement is not satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: go
        Gerrit-Branch: master
        Gerrit-Change-Id: Ic183a7f36e5598b29c287e9a1df1ebeb37e36948
        Gerrit-Change-Number: 802240
        Gerrit-PatchSet: 2
        Gerrit-Owner: Chencheng Jiang <dorb...@gmail.com>
        Gerrit-Reviewer: Russ Cox <r...@golang.org>
        Gerrit-Reviewer: Sean Liao <se...@liao.dev>
        Gerrit-CC: Gopher Robot <go...@golang.org>
        Gerrit-Attention: Russ Cox <r...@golang.org>
        Gerrit-Attention: Chencheng Jiang <dorb...@gmail.com>
        Gerrit-Comment-Date: Tue, 21 Jul 2026 22:26:11 +0000
        Gerrit-HasComments: Yes
        Gerrit-Has-Labels: Yes
        unsatisfied_requirement
        open
        diffy
        Reply all
        Reply to author
        Forward
        0 new messages