From 0d18c6d7ce076d74a8de294a601191d3d64bf8f3 Mon Sep 17 00:00:00 2001 From: Masaaki Goshima Date: Mon, 27 Dec 2021 21:48:21 +0900 Subject: [PATCH 1/3] Optimize encoding path for escaped string --- internal/encoder/decode_rune.go | 142 ++++++++++++++++++++++++++++++++ internal/encoder/string.go | 69 ++++++++-------- 2 files changed, 176 insertions(+), 35 deletions(-) create mode 100644 internal/encoder/decode_rune.go diff --git a/internal/encoder/decode_rune.go b/internal/encoder/decode_rune.go new file mode 100644 index 0000000..5c8de16 --- /dev/null +++ b/internal/encoder/decode_rune.go @@ -0,0 +1,142 @@ +package encoder + +import "unicode/utf8" + +// Code points in the surrogate range are not valid for UTF-8. +const ( + surrogateMin = 0xD800 + surrogateMax = 0xDFFF +) + +const ( + maskx = 63 //0b00111111 + mask2 = 31 //0b00011111 + mask3 = 15 //0b00001111 + mask4 = 7 //0b00000111 + + rune1Max = 1<<7 - 1 + rune2Max = 1<<11 - 1 + rune3Max = 1<<16 - 1 + + // The default lowest and highest continuation byte. + locb = 128 //0b10000000 + hicb = 191 //0b10111111 + + // These names of these constants are chosen to give nice alignment in the + // table below. The first nibble is an index into acceptRanges or F for + // special one-byte cases. The second nibble is the Rune length or the + // Status for the special one-byte case. + xx = 0xF1 // invalid: size 1 + as = 0xF0 // ASCII: size 1 + s1 = 0x02 // accept 0, size 2 + s2 = 0x13 // accept 1, size 3 + s3 = 0x03 // accept 0, size 3 + s4 = 0x23 // accept 2, size 3 + s5 = 0x34 // accept 3, size 4 + s6 = 0x04 // accept 0, size 4 + s7 = 0x44 // accept 4, size 4 +) + +// first is information about the first byte in a UTF-8 sequence. +var first = [256]uint8{ + // 1 2 3 4 5 6 7 8 9 A B C D E F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F + as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F + // 1 2 3 4 5 6 7 8 9 A B C D E F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF + xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF + s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF + s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF + s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF +} + +// acceptRange gives the range of valid values for the second byte in a UTF-8 +// sequence. +type acceptRange struct { + lo uint8 // lowest value for second byte. + hi uint8 // highest value for second byte. +} + +var ( + sep = [2]byte{226, 128} + lineSep = byte(168) //'\u2028' + paragraphSep = byte(169) //'\u2029' +) + +type decodeRuneState int + +const ( + validUTF8State decodeRuneState = iota + runeErrorState + lineSepState + paragraphSepState +) + +func decodeRuneInString(s string) (decodeRuneState, int) { + n := len(s) + s0 := s[0] + x := first[s0] + if x >= as { + // The following code simulates an additional check for x == xx and + // handling the ASCII and invalid cases accordingly. This mask-and-or + // approach prevents an additional branch. + mask := rune(x) << 31 >> 31 // Create 0x0000 or 0xFFFF. + if rune(s[0])&^mask|utf8.RuneError&mask == utf8.RuneError { + return runeErrorState, 1 + } + return validUTF8State, 1 + } + sz := int(x & 7) + var accept acceptRange + switch x >> 4 { + case 0: + accept = acceptRange{locb, hicb} + case 1: + accept = acceptRange{0xA0, hicb} + case 2: + accept = acceptRange{locb, 0x9F} + case 3: + accept = acceptRange{0x90, hicb} + case 4: + accept = acceptRange{locb, 0x8F} + } + if n < sz { + return runeErrorState, 1 + } + s1 := s[1] + if s1 < accept.lo || accept.hi < s1 { + return runeErrorState, 1 + } + if sz <= 2 { + return validUTF8State, 2 + } + s2 := s[2] + if s2 < locb || hicb < s2 { + return runeErrorState, 1 + } + if sz <= 3 { + if s[0] == sep[0] && s[1] == sep[1] { + switch s[2] { + case lineSep: + return lineSepState, 3 + case paragraphSep: + return paragraphSepState, 3 + } + } + return validUTF8State, 3 + } + s3 := s[3] + if s3 < locb || hicb < s3 { + return runeErrorState, 1 + } + return validUTF8State, 4 +} diff --git a/internal/encoder/string.go b/internal/encoder/string.go index a699dba..2d7e363 100644 --- a/internal/encoder/string.go +++ b/internal/encoder/string.go @@ -3,7 +3,6 @@ package encoder import ( "math/bits" "reflect" - "unicode/utf8" "unsafe" ) @@ -489,10 +488,9 @@ ESCAPE_END: i = j + 1 j = j + 1 continue - } - // This encodes bytes < 0x20 except for \t, \n and \r. - if c < 0x20 { + case 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, // 0x00-0x0F + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F: // 0x10-0x1F buf = append(buf, s[i:j]...) buf = append(buf, `\u00`...) buf = append(buf, hex[c>>4], hex[c&0xF]) @@ -501,18 +499,14 @@ ESCAPE_END: continue } - r, size := utf8.DecodeRuneInString(s[j:]) - - if r == utf8.RuneError && size == 1 { + state, size := decodeRuneInString(s[j:]) + switch state { + case runeErrorState: buf = append(buf, s[i:j]...) buf = append(buf, `\ufffd`...) - i = j + size - j = j + size + i = j + 1 + j = j + 1 continue - } - - switch r { - case '\u2028', '\u2029': // U+2028 is LINE SEPARATOR. // U+2029 is PARAGRAPH SEPARATOR. // They are both technically valid characters in JSON strings, @@ -520,14 +514,19 @@ ESCAPE_END: // and can lead to security holes there. It is valid JSON to // escape them, so we do so unconditionally. // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + case lineSepState: buf = append(buf, s[i:j]...) - buf = append(buf, `\u202`...) - buf = append(buf, hex[r&0xF]) - i = j + size - j = j + size + buf = append(buf, `\u2028`...) + i = j + 3 + j = j + 3 + continue + case paragraphSepState: + buf = append(buf, s[i:j]...) + buf = append(buf, `\u2029`...) + i = j + 3 + j = j + 3 continue } - j += size } @@ -594,10 +593,9 @@ func appendString(buf []byte, s string) []byte { i = j + 1 j = j + 1 continue - } - // This encodes bytes < 0x20 except for \t, \n and \r. - if c < 0x20 { + case 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, // 0x00-0x0F + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F: // 0x10-0x1F buf = append(buf, s[i:j]...) buf = append(buf, `\u00`...) buf = append(buf, hex[c>>4], hex[c&0xF]) @@ -606,18 +604,14 @@ func appendString(buf []byte, s string) []byte { continue } - r, size := utf8.DecodeRuneInString(s[j:]) - - if r == utf8.RuneError && size == 1 { + state, size := decodeRuneInString(s[j:]) + switch state { + case runeErrorState: buf = append(buf, s[i:j]...) buf = append(buf, `\ufffd`...) - i = j + size - j = j + size + i = j + 1 + j = j + 1 continue - } - - switch r { - case '\u2028', '\u2029': // U+2028 is LINE SEPARATOR. // U+2029 is PARAGRAPH SEPARATOR. // They are both technically valid characters in JSON strings, @@ -625,14 +619,19 @@ func appendString(buf []byte, s string) []byte { // and can lead to security holes there. It is valid JSON to // escape them, so we do so unconditionally. // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + case lineSepState: buf = append(buf, s[i:j]...) - buf = append(buf, `\u202`...) - buf = append(buf, hex[r&0xF]) - i = j + size - j = j + size + buf = append(buf, `\u2028`...) + i = j + 3 + j = j + 3 + continue + case paragraphSepState: + buf = append(buf, s[i:j]...) + buf = append(buf, `\u2029`...) + i = j + 3 + j = j + 3 continue } - j += size } From 2d022aa037b9eab04ef089608176eea1680f4616 Mon Sep 17 00:00:00 2001 From: Masaaki Goshima Date: Mon, 27 Dec 2021 22:28:25 +0900 Subject: [PATCH 2/3] Remove unnecessary codes --- internal/encoder/decode_rune.go | 15 ------- internal/encoder/string.go | 79 +++++++++++---------------------- 2 files changed, 25 insertions(+), 69 deletions(-) diff --git a/internal/encoder/decode_rune.go b/internal/encoder/decode_rune.go index 5c8de16..1fa1074 100644 --- a/internal/encoder/decode_rune.go +++ b/internal/encoder/decode_rune.go @@ -2,22 +2,7 @@ package encoder import "unicode/utf8" -// Code points in the surrogate range are not valid for UTF-8. const ( - surrogateMin = 0xD800 - surrogateMax = 0xDFFF -) - -const ( - maskx = 63 //0b00111111 - mask2 = 31 //0b00011111 - mask3 = 15 //0b00001111 - mask4 = 7 //0b00000111 - - rune1Max = 1<<7 - 1 - rune2Max = 1<<11 - 1 - rune3Max = 1<<16 - 1 - // The default lowest and highest continuation byte. locb = 128 //0b10000000 hicb = 191 //0b10111111 diff --git a/internal/encoder/string.go b/internal/encoder/string.go index 2d7e363..236e2e9 100644 --- a/internal/encoder/string.go +++ b/internal/encoder/string.go @@ -348,53 +348,6 @@ var needEscape = [256]bool{ var hex = "0123456789abcdef" -// escapeIndex finds the index of the first char in `s` that requires escaping. -// A char requires escaping if it's outside of the range of [0x20, 0x7F] or if -// it includes a double quote or backslash. -// If no chars in `s` require escaping, the return value is -1. -func escapeIndex(s string) int { - chunks := stringToUint64Slice(s) - for _, n := range chunks { - // combine masks before checking for the MSB of each byte. We include - // `n` in the mask to check whether any of the *input* byte MSBs were - // set (i.e. the byte was outside the ASCII range). - mask := n | below(n, 0x20) | contains(n, '"') | contains(n, '\\') - if (mask & msb) != 0 { - return bits.TrailingZeros64(mask&msb) / 8 - } - } - - valLen := len(s) - for i := len(chunks) * 8; i < valLen; i++ { - if needEscape[s[i]] { - return i - } - } - - return -1 -} - -// below return a mask that can be used to determine if any of the bytes -// in `n` are below `b`. If a byte's MSB is set in the mask then that byte was -// below `b`. The result is only valid if `b`, and each byte in `n`, is below -// 0x80. -func below(n uint64, b byte) uint64 { - return n - expand(b) -} - -// contains returns a mask that can be used to determine if any of the -// bytes in `n` are equal to `b`. If a byte's MSB is set in the mask then -// that byte is equal to `b`. The result is only valid if `b`, and each -// byte in `n`, is below 0x80. -func contains(n uint64, b byte) uint64 { - return (n ^ expand(b)) - lsb -} - -// expand puts the specified byte into each of the 8 bytes of a uint64. -func expand(b byte) uint64 { - return lsb * uint64(b) -} - //nolint:govet func stringToUint64Slice(s string) []uint64 { return *(*[]uint64)(unsafe.Pointer(&reflect.SliceHeader{ @@ -539,19 +492,37 @@ func appendString(buf []byte, s string) []byte { return append(buf, `""`...) } buf = append(buf, '"') - var escapeIdx int + var ( + i, j int + ) if valLen >= 8 { - if escapeIdx = escapeIndex(s); escapeIdx < 0 { - return append(append(buf, s...), '"') + chunks := stringToUint64Slice(s) + for _, n := range chunks { + // combine masks before checking for the MSB of each byte. We include + // `n` in the mask to check whether any of the *input* byte MSBs were + // set (i.e. the byte was outside the ASCII range). + mask := n | (n - (lsb * 0x20)) | + ((n ^ (lsb * '"')) - lsb) | + ((n ^ (lsb * '\\')) - lsb) + if (mask & msb) != 0 { + j = bits.TrailingZeros64(mask&msb) / 8 + goto ESCAPE_END + } } + valLen := len(s) + for i := len(chunks) * 8; i < valLen; i++ { + if needEscape[s[i]] { + j = i + goto ESCAPE_END + } + } + return append(append(buf, s...), '"') } - - i := 0 - j := escapeIdx +ESCAPE_END: for j < valLen { c := s[j] - if c >= 0x20 && c <= 0x7f && c != '\\' && c != '"' { + if !needEscape[c] { // fast path: most of the time, printable ascii characters are used j++ continue From 1bb8b1620082bc8d1eff22c52eb48204b1d4c943 Mon Sep 17 00:00:00 2001 From: Masaaki Goshima Date: Mon, 27 Dec 2021 22:40:43 +0900 Subject: [PATCH 3/3] Optimize variables --- internal/encoder/decode_rune.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/encoder/decode_rune.go b/internal/encoder/decode_rune.go index 1fa1074..1087f0b 100644 --- a/internal/encoder/decode_rune.go +++ b/internal/encoder/decode_rune.go @@ -51,8 +51,7 @@ type acceptRange struct { hi uint8 // highest value for second byte. } -var ( - sep = [2]byte{226, 128} +const ( lineSep = byte(168) //'\u2028' paragraphSep = byte(169) //'\u2029' ) @@ -109,8 +108,9 @@ func decodeRuneInString(s string) (decodeRuneState, int) { return runeErrorState, 1 } if sz <= 3 { - if s[0] == sep[0] && s[1] == sep[1] { - switch s[2] { + // separator character prefixes: [2]byte{226, 128} + if s0 == 226 && s1 == 128 { + switch s2 { case lineSep: return lineSepState, 3 case paragraphSep: