1
0
mirror of https://github.com/golang/go synced 2024-09-29 20:34:36 -06:00

time: optimize appendInt and appendNanos

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>
This commit is contained in:
Joe Tsai 2022-09-24 23:40:02 -07:00 committed by Joseph Tsai
parent f841722853
commit 7f04b84162
4 changed files with 100 additions and 38 deletions

View 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

View 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

View 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 {

View 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