Skip to content

Commit

Permalink
time: optimize appendInt and appendNanos
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
dsnet authored and pull[bot] committed Jan 28, 2023
1 parent 3478037 commit e5dd05d
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 38 deletions.
1 change: 1 addition & 0 deletions src/time/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 53 additions & 37 deletions src/time/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/time/format_rfc3339.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions src/time/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e5dd05d

Please sign in to comment.