]> Cypherpunks repositories - gostls13.git/commitdiff
time: optimize appendInt and appendNanos
authorJoe Tsai <joetsai@digital-static.net>
Sun, 25 Sep 2022 06:40:02 +0000 (23:40 -0700)
committerJoseph Tsai <joetsai@digital-static.net>
Mon, 24 Oct 2022 19:23:32 +0000 (19:23 +0000)
The appendInt function previously performed a double pass
over the formatted integer. We can avoid the second pass
if we knew the exact length of formatted integer,
allowing us to directly serialize into the output buffer.

Rename formatNano to appendNano to be consistent with
other append-like functionality.

Performance:

name               old time/op  new time/op  delta
FormatRFC3339Nano  109ns ± 1%   72ns ± 1%    -34.06%  (p=0.000 n=10+10)

Change-Id: Id48f77eb4976fb1dcd6e27fb6a02d29cbf0c026a
Reviewed-on: https://go-review.googlesource.com/c/go/+/444278
Run-TryBot: Joseph Tsai <joetsai@digital-static.net>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: David Chase <drchase@google.com>
src/time/export_test.go
src/time/format.go
src/time/format_rfc3339.go
src/time/format_test.go

index fb103fcbf7c6c550b810530b5ff6ea2642b8318d..a4940d12f91d802c40d44fba07e5da1627e4ff3c 100644 (file)
@@ -133,6 +133,7 @@ var StdChunkNames = map[int]string{
 
 var Quote = quote
 
+var AppendInt = appendInt
 var AppendFormatAny = Time.appendFormat
 var AppendFormatRFC3339 = Time.appendFormatRFC3339
 var ParseAny = parse
index 6b35d302281a5568214bfe2f084c8c8908962f9c..89a3ce259cf72bd40c9884e6ab72087f0af2c23f 100644 (file)
@@ -403,24 +403,46 @@ func appendInt(b []byte, x int, width int) []byte {
                u = uint(-x)
        }
 
-       // Assemble decimal in reverse order.
-       var buf [20]byte
-       i := len(buf)
-       for u >= 10 {
-               i--
-               q := u / 10
-               buf[i] = byte('0' + u - q*10)
-               u = q
+       // 2-digit and 4-digit fields are the most common in time formats.
+       utod := func(u uint) byte { return '0' + byte(u) }
+       switch {
+       case width == 2 && u < 1e2:
+               return append(b, utod(u/1e1), utod(u%1e1))
+       case width == 4 && u < 1e4:
+               return append(b, utod(u/1e3), utod(u/1e2%1e1), utod(u/1e1%1e1), utod(u%1e1))
+       }
+
+       // Compute the number of decimal digits.
+       var n int
+       if u == 0 {
+               n = 1
+       }
+       for u2 := u; u2 > 0; u2 /= 10 {
+               n++
        }
-       i--
-       buf[i] = byte('0' + u)
 
        // Add 0-padding.
-       for w := len(buf) - i; w < width; w++ {
+       for pad := width - n; pad > 0; pad-- {
                b = append(b, '0')
        }
 
-       return append(b, buf[i:]...)
+       // Ensure capacity.
+       if len(b)+n <= cap(b) {
+               b = b[:len(b)+n]
+       } else {
+               b = append(b, make([]byte, n)...)
+       }
+
+       // Assemble decimal in reverse order.
+       i := len(b) - 1
+       for u >= 10 && i > 0 {
+               q := u / 10
+               b[i] = utod(u - q*10)
+               u = q
+               i--
+       }
+       b[i] = utod(u)
+       return b
 }
 
 // Never printed, just needs to be non-nil for return by atoi.
@@ -444,7 +466,7 @@ func atoi[bytes []byte | string](s bytes) (x int, err error) {
        return x, nil
 }
 
-// The "std" value passed to formatNano contains two packed fields: the number of
+// The "std" value passed to appendNano contains two packed fields: the number of
 // digits after the decimal and the separator character (period or comma).
 // These functions pack and unpack that variable.
 func stdFracSecond(code, n, c int) int {
@@ -466,35 +488,29 @@ func separator(std int) byte {
        return ','
 }
 
-// formatNano appends a fractional second, as nanoseconds, to b
-// and returns the result.
-func formatNano(b []byte, nanosec uint, std int) []byte {
-       var (
-               n         = digitsLen(std)
-               separator = separator(std)
-               trim      = std&stdMask == stdFracSecond9
-       )
-       u := nanosec
-       var buf [9]byte
-       for start := len(buf); start > 0; {
-               start--
-               buf[start] = byte(u%10 + '0')
-               u /= 10
+// appendNano appends a fractional second, as nanoseconds, to b
+// and returns the result. The nanosec must be within [0, 999999999].
+func appendNano(b []byte, nanosec int, std int) []byte {
+       trim := std&stdMask == stdFracSecond9
+       n := digitsLen(std)
+       if trim && (n == 0 || nanosec == 0) {
+               return b
        }
-
-       if n > 9 {
-               n = 9
+       dot := separator(std)
+       b = append(b, dot)
+       b = appendInt(b, nanosec, 9)
+       if n < 9 {
+               b = b[:len(b)-9+n]
        }
        if trim {
-               for n > 0 && buf[n-1] == '0' {
-                       n--
+               for len(b) > 0 && b[len(b)-1] == '0' {
+                       b = b[:len(b)-1]
                }
-               if n == 0 {
-                       return b
+               if len(b) > 0 && b[len(b)-1] == dot {
+                       b = b[:len(b)-1]
                }
        }
-       b = append(b, separator)
-       return append(b, buf[:n]...)
+       return b
 }
 
 // String returns the time formatted using the format string
@@ -791,7 +807,7 @@ func (t Time) appendFormat(b []byte, layout string) []byte {
                        b = appendInt(b, zone/60, 2)
                        b = appendInt(b, zone%60, 2)
                case stdFracSecond0, stdFracSecond9:
-                       b = formatNano(b, uint(t.Nanosecond()), std)
+                       b = appendNano(b, t.Nanosecond(), std)
                }
        }
        return b
index 7538de87c765880d211c3282b4a2501b8b4d3870..a9c295df97e5bb12224797f15bbef040d729cb3e 100644 (file)
@@ -38,7 +38,7 @@ func (t Time) appendFormatRFC3339(b []byte, nanos bool) []byte {
 
        if nanos {
                std := stdFracSecond(stdFracSecond9, 9, '.')
-               b = formatNano(b, uint(t.Nanosecond()), std)
+               b = appendNano(b, t.Nanosecond(), std)
        }
 
        if offset == 0 {
index ae2dc9036fccadec2a3ede3f2394d6ecab758a09..b1d85f510be4c9f5875eeddb6937ec8c102ca4c0 100644 (file)
@@ -90,6 +90,51 @@ func TestRFC3339Conversion(t *testing.T) {
        }
 }
 
+func TestAppendInt(t *testing.T) {
+       tests := []struct {
+               in    int
+               width int
+               want  string
+       }{
+               {0, 0, "0"},
+               {0, 1, "0"},
+               {0, 2, "00"},
+               {0, 3, "000"},
+               {1, 0, "1"},
+               {1, 1, "1"},
+               {1, 2, "01"},
+               {1, 3, "001"},
+               {-1, 0, "-1"},
+               {-1, 1, "-1"},
+               {-1, 2, "-01"},
+               {-1, 3, "-001"},
+               {99, 2, "99"},
+               {100, 2, "100"},
+               {1, 4, "0001"},
+               {12, 4, "0012"},
+               {123, 4, "0123"},
+               {1234, 4, "1234"},
+               {12345, 4, "12345"},
+               {1, 5, "00001"},
+               {12, 5, "00012"},
+               {123, 5, "00123"},
+               {1234, 5, "01234"},
+               {12345, 5, "12345"},
+               {123456, 5, "123456"},
+               {0, 9, "000000000"},
+               {123, 9, "000000123"},
+               {123456, 9, "000123456"},
+               {123456789, 9, "123456789"},
+       }
+       var got []byte
+       for _, tt := range tests {
+               got = AppendInt(got[:0], tt.in, tt.width)
+               if string(got) != tt.want {
+                       t.Errorf("appendInt(%d, %d) = %s, want %s", tt.in, tt.width, got, tt.want)
+               }
+       }
+}
+
 type FormatTest struct {
        name   string
        format string