From 7b82396f9e3b84e76b88f464d84aa7dce729f908 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 8 Jan 2018 19:23:02 +0000 Subject: [PATCH 1/5] Add benchmarks for JSON marshalling --- model/time_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/model/time_test.go b/model/time_test.go index 3efdd65f..80627be7 100644 --- a/model/time_test.go +++ b/model/time_test.go @@ -130,3 +130,18 @@ func TestParseDuration(t *testing.T) { } } } + +func BenchmarkMarshalJSON(b *testing.B) { + t := TimeFromUnixNano(1514834431600000000) + for i := 0; i < b.N; i++ { + t.MarshalJSON() + } +} + +func BenchmarkUnmarshalJSON(b *testing.B) { + t := TimeFromUnixNano(1514834431600000000) + buf, _ := t.MarshalJSON() + for i := 0; i < b.N; i++ { + t.UnmarshalJSON(buf) + } +} From 41ade241aab2a3d443b6c4dea28fe77831b04933 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 8 Jan 2018 18:48:30 +0000 Subject: [PATCH 2/5] Eliminate two memory allocations in Time.MarshalJSON Converting from string to []byte makes a copy, as strings are immutable and slices are not. Creating a byte slice using AppendFloat() instead of FormatFloat() avoids creating both the string and the copy. --- model/time.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/time.go b/model/time.go index 74ed5a9f..58b95f3d 100644 --- a/model/time.go +++ b/model/time.go @@ -117,7 +117,7 @@ func (t Time) String() string { // MarshalJSON implements the json.Marshaler interface. func (t Time) MarshalJSON() ([]byte, error) { - return []byte(t.String()), nil + return strconv.AppendFloat(nil, float64(t)/float64(second), 'f', -1, 64), nil } // UnmarshalJSON implements the json.Unmarshaler interface. From 4b3219cde072ba99aaadbaf3bc5cb630ce1b4db9 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 8 Jan 2018 19:26:34 +0000 Subject: [PATCH 3/5] Eliminate a memory allocation in Time.UnarshalJSON Split() uses heap storage for the result array: since we never want more than two parts we can just use IndexByte() to find the split. And we don't need a check for the case with more than one '.' since ParseInt() will error. --- model/time.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/model/time.go b/model/time.go index 58b95f3d..5b8c62d2 100644 --- a/model/time.go +++ b/model/time.go @@ -122,16 +122,16 @@ func (t Time) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Time) UnmarshalJSON(b []byte) error { - p := strings.Split(string(b), ".") - switch len(p) { - case 1: - v, err := strconv.ParseInt(string(p[0]), 10, 64) + s := string(b) + i := strings.IndexByte(s, '.') + if i == -1 { + v, err := strconv.ParseInt(s, 10, 64) if err != nil { return err } *t = Time(v * second) - - case 2: + } else { + p := [2]string{s[:i], s[i+1:]} v, err := strconv.ParseInt(string(p[0]), 10, 64) if err != nil { return err @@ -151,9 +151,6 @@ func (t *Time) UnmarshalJSON(b []byte) error { } *t = Time(v + va) - - default: - return fmt.Errorf("invalid time %q", string(b)) } return nil } From 9a7a45cba120c9db3b1c949b18d111f62f150ecf Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 9 Jan 2018 08:38:56 +0000 Subject: [PATCH 4/5] Pre-allocate a string of zeros for Time.UnmarshalJSON For performance: it saves one or two allocations every time you hit this path. --- model/time.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/model/time.go b/model/time.go index 5b8c62d2..cccbb63a 100644 --- a/model/time.go +++ b/model/time.go @@ -110,6 +110,9 @@ func (t Time) UnixNano() int64 { // The number of digits after the dot. var dotPrecision = int(math.Log10(float64(second))) +// Enough zeros to pad a number with fewer than dotPrecision +var zeros = strings.Repeat("0", dotPrecision) + // String returns a string representation of the Time. func (t Time) String() string { return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64) @@ -142,7 +145,7 @@ func (t *Time) UnmarshalJSON(b []byte) error { if prec < 0 { p[1] = p[1][:dotPrecision] } else if prec > 0 { - p[1] = p[1] + strings.Repeat("0", prec) + p[1] = p[1] + zeros[:prec] } va, err := strconv.ParseInt(p[1], 10, 32) From 66e9e6025cbe42a914598cce6c84e5266cd0b015 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 19 Jan 2018 15:08:00 +0000 Subject: [PATCH 5/5] Give new benchmarks clearer names --- model/time_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/time_test.go b/model/time_test.go index 80627be7..4f82fcd9 100644 --- a/model/time_test.go +++ b/model/time_test.go @@ -131,14 +131,14 @@ func TestParseDuration(t *testing.T) { } } -func BenchmarkMarshalJSON(b *testing.B) { +func BenchmarkTimeMarshalJSON(b *testing.B) { t := TimeFromUnixNano(1514834431600000000) for i := 0; i < b.N; i++ { t.MarshalJSON() } } -func BenchmarkUnmarshalJSON(b *testing.B) { +func BenchmarkTimeUnmarshalJSON(b *testing.B) { t := TimeFromUnixNano(1514834431600000000) buf, _ := t.MarshalJSON() for i := 0; i < b.N; i++ {