1
0
mirror of https://github.com/rs/zerolog synced 2026-06-08 17:13:30 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Olivier Poitrey fde564e937 Optimize JSON encoding even further
Last optimization was for JSON string with no character to encode. This
version focuses on strings with some chars to encode, trying to apply
the same trick for substrings that do not need encoding.

benchmark                old ns/op     new ns/op    delta
.../NoEncoding-8         60.2          51.3         -14.78%
.../EncodingFirst-8      140           116          -17.14%
.../EncodingMiddle-8     112           86.4         -22.86%
.../EncodingLast-8       62.8          61.1         -2.71%
.../MultiBytesFirst-8    164           129          -21.34%
.../MultiBytesMiddle-8   133           96.9         -27.14%
.../MultiBytesLast-8     81.9          73.5         -10.26%
2017-06-25 01:30:02 -07:00
Olivier Poitrey 274f2e4c61 Add some json encoder benchmarks 2017-06-24 18:30:15 -07:00
Olivier Poitrey b42b460ec6 Update bench 2017-06-23 20:37:30 -07:00
Josh Baker f8aa7a1962 Optimistically expect simple strings for json (#6)
Performance update to appendJSONString so that it now checks if
the input is a simple string that contains no json delimiters, control
characters, or unicode. If simple then the operation is only three
appends. [double-quote, string, double-quote].

If a non-simple character is encountered then all of the previous
characters are appended and the operation falls back to the original
method for the remaining characters.

Before:

  BenchmarkLogEmpty-8	    100000000	        17.1 ns/op
  BenchmarkDisabled-8	    500000000	         4.12 ns/op
  BenchmarkInfo-8		    20000000	       101 ns/op
  BenchmarkContextFields-8    20000000	       105 ns/op
  BenchmarkLogFields-8	     5000000	       281 ns/op

After:

  BenchmarkLogEmpty-8	    100000000	        16.7 ns/op
  BenchmarkDisabled-8	    500000000	         3.79 ns/op
  BenchmarkInfo-8		    30000000	        44.8 ns/op
  BenchmarkContextFields-8    30000000	        67.5 ns/op
  BenchmarkLogFields-8	    10000000	       197 ns/op
2017-06-23 20:28:33 -07:00
3 changed files with 124 additions and 41 deletions
+8 -8
View File
@@ -255,11 +255,11 @@ Some settings can be changed and will by applied to all loggers:
All operations are allocation free (those numbers *include* JSON encoding):
```
BenchmarkLogEmpty-8 50000000 19.8 ns/op 0 B/op 0 allocs/op
BenchmarkDisabled-8 100000000 4.73 ns/op 0 B/op 0 allocs/op
BenchmarkInfo-8 10000000 85.1 ns/op 0 B/op 0 allocs/op
BenchmarkContextFields-8 10000000 81.9 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 5000000 247 ns/op 0 B/op 0 allocs/op
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op
BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op
BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
```
Using Uber's zap [comparison benchmark](https://github.com/uber-go/zap#performance):
@@ -268,7 +268,7 @@ Log a message and 10 fields:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 787 ns/op | 80 B/op | 6 allocs/op |
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
@@ -281,7 +281,7 @@ Log a message with a logger that already has 10 fields of context:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 80 ns/op | 0 B/op | 0 allocs/op |
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
@@ -294,7 +294,7 @@ Log a static string, without any context or `printf`-style templating:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 76.2 ns/op | 0 B/op | 0 allocs/op |
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
+84 -32
View File
@@ -4,41 +4,93 @@ import "unicode/utf8"
const hex = "0123456789abcdef"
// appendJSONString encodes the input string to json and appends
// the encoded string to the input byte slice.
//
// The operation loops though each byte in the string looking
// for characters that need json or utf8 encoding. If the string
// does not need encoding, then the string is appended in it's
// entirety to the byte slice.
// If we encounter a byte that does need encoding, switch up
// the operation and perform a byte-by-byte read-encode-append.
func appendJSONString(dst []byte, s string) []byte {
// Start with a double quote.
dst = append(dst, '"')
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
switch b {
case '"', '\\':
dst = append(dst, '\\', b)
case '\b':
dst = append(dst, '\\', 'b')
case '\f':
dst = append(dst, '\\', 'f')
case '\n':
dst = append(dst, '\\', 'n')
case '\r':
dst = append(dst, '\\', 'r')
case '\t':
dst = append(dst, '\\', 't')
default:
if b >= 0x20 {
dst = append(dst, b)
} else {
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
}
}
i++
continue
// Loop through each character in the string.
for i := 0; i < len(s); i++ {
// Check if the character needs encoding. Control characters, slashes,
// and the double quote need json encoding. Bytes above the ascii
// boundary needs utf8 encoding.
if s[i] < 0x20 || s[i] > 0x7e || s[i] == '\\' || s[i] == '"' {
// We encountered a character that needs to be encoded. Switch
// to complex version of the algorithm.
dst = appendJSONStringComplex(dst, s, i)
return append(dst, '"')
}
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
dst = append(dst, `\ufffd`...)
i++
continue
}
dst = append(dst, s[i:i+size]...)
i += size
}
// The string has no need for encoding an therefore is directly
// appended to the byte slice.
dst = append(dst, s...)
// End with a double quote
return append(dst, '"')
}
// appendJSONStringComplex is used by appendJSONString to take over an in
// progress JSON string encoding that encountered a character that needs
// to be encoded.
func appendJSONStringComplex(dst []byte, s string, i int) []byte {
start := 0
for i < len(s) {
b := s[i]
if b >= utf8.RuneSelf {
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
// In case of error, first append previous simple characters to
// the byte slice if any and append a remplacement character code
// in place of the invalid sequence.
if start < i {
dst = append(dst, s[start:i]...)
}
dst = append(dst, `\ufffd`...)
i += size
start = i
continue
}
i += size
continue
}
if b >= 0x20 && b <= 0x7e && b != '\\' && b != '"' {
i++
continue
}
// We encountered a character that needs to be encoded.
// Let's append the previous simple characters to the byte slice
// and switch our operation to read and encode the remainder
// characters byte-by-byte.
if start < i {
dst = append(dst, s[start:i]...)
}
switch b {
case '"', '\\':
dst = append(dst, '\\', b)
case '\b':
dst = append(dst, '\\', 'b')
case '\f':
dst = append(dst, '\\', 'f')
case '\n':
dst = append(dst, '\\', 'n')
case '\r':
dst = append(dst, '\\', 'r')
case '\t':
dst = append(dst, '\\', 't')
default:
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
}
i++
start = i
}
if start < len(s) {
dst = append(dst, s[start:]...)
}
return dst
}
+32 -1
View File
@@ -1,12 +1,15 @@
package zerolog
import "testing"
import (
"testing"
)
func TestAppendJSONString(t *testing.T) {
encodeStringTests := []struct {
in string
out string
}{
{"", `""`},
{"\\", `"\\"`},
{"\x00", `"\u0000"`},
{"\x01", `"\u0001"`},
@@ -40,6 +43,14 @@ func TestAppendJSONString(t *testing.T) {
{"\x1d", `"\u001d"`},
{"\x1e", `"\u001e"`},
{"\x1f", `"\u001f"`},
{"✭", `"✭"`},
{"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence
{"ascii", `"ascii"`},
{"\"a", `"\"a"`},
{"\x1fa", `"\u001fa"`},
{"foo\"bar\"baz", `"foo\"bar\"baz"`},
{"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`},
{"emoji \u2764\ufe0f!", `"emoji ❤️!"`},
}
for _, tt := range encodeStringTests {
@@ -49,3 +60,23 @@ func TestAppendJSONString(t *testing.T) {
}
}
}
func BenchmarkAppendJSONString(b *testing.B) {
tests := map[string]string{
"NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
"EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
"EncodingMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa"aaaaaaaaaaaaaaaaaaaaaaaa`,
"EncodingLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`,
"MultiBytesFirst": `❤️aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
"MultiBytesMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa❤️aaaaaaaaaaaaaaaaaaaaaaaa`,
"MultiBytesLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa❤️`,
}
for name, str := range tests {
b.Run(name, func(b *testing.B) {
buf := make([]byte, 0, 100)
for i := 0; i < b.N; i++ {
_ = appendJSONString(buf, str)
}
})
}
}