Repository: apache/dubbo-go-hessian2
Pull request #188 (2020-05-14 13:49:28)

Perf decode string
<!--  Thanks for sending a pull request! 
-->

**What this PR does**:

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #https://github.com/apache/dubbo-go-hessian2/issues/186

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
NONE
```

优化思路：

1.  直接使用utf-8 byte解码，性能最高；之前先解码成 rune, 对rune解码成string，及其耗费性能
2.  增加批量string chunk copy, 降低read调用
3.  使用unsafe转换string


Patch:
diff --git a/.asf.yaml b/.asf.yaml
deleted file mode 100644
index 8d84e69..0000000
--- a/.asf.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-notifications:
-    commits:      commits@dubbo.apache.org
-    issues:       notifications@dubbo.apache.org
-    pullrequests: notifications@dubbo.apache.org
-    jira_options: link label link label
diff --git a/decode.go b/decode.go
index c5aafac..f10456c 100644
--- a/decode.go
+++ b/decode.go
@@ -50,15 +50,54 @@ func NewDecoder(b []byte) *Decoder {
 	return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), typeRefs: &TypeRefs{records: map[string]bool{}}}
 }
 
+// NewDecoder generate a decoder instance
+func NewDecoderSize(b []byte, size int) *Decoder {
+	return &Decoder{reader: bufio.NewReaderSize(bytes.NewReader(b), size), typeRefs: &TypeRefs{records: map[string]bool{}}}
+}
+
 // NewDecoder generate a decoder instance with skip
 func NewDecoderWithSkip(b []byte) *Decoder {
 	return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), typeRefs: &TypeRefs{records: map[string]bool{}}, isSkip: true}
 }
 
+// NewCheapDecoderWithSkip generate a decoder instance with skip,
+// only for cache pool, before decode Reset should be called.
+// For example, with pooling use, will effectively improve performance
+//
+//	var hessianPool = &sync.Pool{
+//		New: func() interface{} {
+//			return hessian.NewCheapDecoderWithSkip([]byte{})
+//		},
+//	}
+//
+//	decoder := hessianPool.Get().(*hessian.Decoder)
+//	fill decode data
+//	decoder.Reset(data[:])
+//  decode anything ...
+//	hessianPool.Put(decoder)
+func NewCheapDecoderWithSkip(b []byte) *Decoder {
+	return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), isSkip: true}
+}
+
 /////////////////////////////////////////
 // utilities
 /////////////////////////////////////////
 
+func (d *Decoder) Reset(b []byte) *Decoder {
+	// reuse reader buf, avoid allocate
+	d.reader.Reset(bytes.NewReader(b))
+	d.typeRefs = &TypeRefs{records: map[string]bool{}}
+
+	if d.refs != nil {
+		d.refs = nil
+	}
+	if d.classInfoList != nil {
+		d.classInfoList = nil
+	}
+
+	return d
+}
+
 // peek a byte
 func (d *Decoder) peekByte() byte {
 	return d.peek(1)[0]
@@ -150,6 +189,8 @@ func (d *Decoder) Decode() (interface{}, error) {
 	return EnsureInterface(d.DecodeValue())
 }
 
+func (d *Decoder) Buffered() int { return d.reader.Buffered() }
+
 // DecodeValue parse hessian data, the return value maybe a reflection value when it's a map, list, object, or ref.
 func (d *Decoder) DecodeValue() (interface{}, error) {
 	var (
diff --git a/string.go b/string.go
index f692438..6a217c5 100644
--- a/string.go
+++ b/string.go
@@ -217,32 +217,37 @@ func encString(b []byte, v string) []byte {
 // ::= 'S' b1 b0 <utf8-data>         # string of length 0-65535
 // ::= [x00-x1f] <utf8-data>         # string of length 0-31
 // ::= [x30-x34] <utf8-data>         # string of length 0-1023
-func (d *Decoder) getStringLength(tag byte) (int32, error) {
+func (d *Decoder) getStringLength(tag byte) (int, error) {
 	var (
 		err    error
-		buf    [2]byte
-		length int32
+		length int
 	)
 
 	switch {
 	case tag >= BC_STRING_DIRECT && tag <= STRING_DIRECT_MAX:
-		return int32(tag - 0x00), nil
+		return int(tag - 0x00), nil
 
 	case tag >= 0x30 && tag <= 0x33:
-		_, err = io.ReadFull(d.reader, buf[:1])
+		b, err := d.readByte()
 		if err != nil {
 			return -1, perrors.WithStack(err)
 		}
 
-		length = int32(tag-0x30)<<8 + int32(buf[0])
+		length = int(tag-0x30)<<8 + int(b)
 		return length, nil
 
 	case tag == BC_STRING_CHUNK || tag == BC_STRING:
-		_, err = io.ReadFull(d.reader, buf[:2])
+		b0, err := d.readByte()
 		if err != nil {
 			return -1, perrors.WithStack(err)
 		}
-		length = int32(buf[0])<<8 + int32(buf[1])
+
+		b1, err := d.readByte()
+		if err != nil {
+			return -1, perrors.WithStack(err)
+		}
+
+		length = int(b0)<<8 + int(b1)
 		return length, nil
 
 	default:
@@ -252,11 +257,10 @@ func (d *Decoder) getStringLength(tag byte) (int32, error) {
 
 func (d *Decoder) decString(flag int32) (string, error) {
 	var (
-		tag       byte
-		charTotal int32
-		last      bool
-		s         string
-		r         rune
+		tag      byte
+		chunkLen int
+		last     bool
+		s        string
 	)
 
 	if flag != TAG_READ {
@@ -311,24 +315,19 @@ func (d *Decoder) decString(flag int32) (string, error) {
 			last = true
 		}
 
-		l, err := d.getStringLength(tag)
+		charLen, err := d.getStringLength(tag)
 		if err != nil {
 			return s, perrors.WithStack(err)
 		}
-		charTotal = l
-		charCount := 0
-
-		runeData := make([]rune, charTotal)
-		runeIndex := 0
-
-		byteCount := 0
-		byteLen := 0
-		charLen := 0
+		chunkLen = charLen
+		bytesBuf := make([]byte, chunkLen<<2)
+		offset := 0
 
 		for {
-			if int32(charCount) == charTotal {
+			if chunkLen <= 0 {
 				if last {
-					return string(runeData[:runeIndex]), nil
+					b := bytesBuf[:offset]
+					return *(*string)(unsafe.Pointer(&b)), nil
 				}
 
 				b, _ := d.readByte()
@@ -343,21 +342,193 @@ func (d *Decoder) decString(flag int32) (string, error) {
 						last = true
 					}
 
-					l, err := d.getStringLength(b)
+					charLen, err = d.getStringLength(b)
 					if err != nil {
 						return s, perrors.WithStack(err)
 					}
-					charTotal += l
-					bs := make([]rune, charTotal)
-					copy(bs, runeData)
-					runeData = bs
 
+					if chunkLen < 0 {
+						chunkLen = 0
+					}
+					if charLen < 0 {
+						charLen = 0
+					}
+
+					chunkLen += charLen
+					remain, cap := len(bytesBuf)-offset, charLen<<2
+					if remain < cap {
+						grow := len(bytesBuf) + cap
+						bs := make([]byte, grow)
+						copy(bs, bytesBuf)
+						bytesBuf = bs
+					}
 				default:
 					return s, perrors.New("expect string tag")
 				}
 			}
 
-			r, charLen, byteLen, err = decodeUcs4Rune(d.reader)
+			if chunkLen > 0 {
+				nread, err := d.next(bytesBuf[offset : offset+chunkLen])
+				if err != nil {
+					if err == io.EOF {
+						break
+					}
+					return s, perrors.WithStack(err)
+				}
+
+				// quickly detect the actual number of bytes
+				prev, i := offset, offset
+				len := offset + nread
+				copied := false
+				for r, r1 := len-1, len-2; i < len; chunkLen-- {
+					ch := bytesBuf[offset]
+					if ch < 0x80 {
+						i++
+						offset++
+					} else if (ch & 0xe0) == 0xc0 {
+						i += 2
+						offset += 2
+					} else if (ch & 0xf0) == 0xe0 {
+						// handle the 3-byte right edge
+						// case:
+						// 1. Expect 3 bytes, but the current byte is on the right
+						// 2. Expect 3 bytes, but the current byte is second to last to the right
+						if i == r {
+							bytesBuf[i+1], err = d.reader.ReadByte()
+							if err != nil {
+								return s, perrors.WithStack(err)
+							}
+							bytesBuf[i+2], err = d.reader.ReadByte()
+							if err != nil {
+								return s, perrors.WithStack(err)
+							}
+							nread += 2
+							len += 2
+						} else if i == r1 {
+							bytesBuf[i+2], err = d.reader.ReadByte()
+							if err != nil {
+								return s, perrors.WithStack(err)
+							}
+							nread++
+							len++
+						}
+
+						// we detect emoji first
+						c1 := ((uint32(ch) & 0x0f) << 12) + ((uint32(bytesBuf[i+1]) & 0x3f) << 6) + (uint32(bytesBuf[i+2]) & 0x3f)
+						if c1 >= 0xD800 && c1 <= 0xDBFF {
+
+							var (
+								c2  rune
+								n2  int
+								err error
+								ch0 byte
+							)
+
+							// more cache byte available
+							if i+3 < len {
+								ch0 = bytesBuf[i+3]
+							} else {
+								ch0, err = d.reader.ReadByte()
+								if err != nil {
+									return s, perrors.WithStack(err)
+								}
+								// update accumulates read bytes,
+								// because it reads more than thunk bytes
+								nread++
+								len++
+							}
+
+							if ch0 < 0x80 {
+								c2, n2 = rune(ch0), 1
+							} else if (ch0 & 0xe0) == 0xc0 {
+								var ch1 byte
+								if i+4 < len {
+									ch1 = bytesBuf[i+4]
+								} else {
+									// out of the chunk byte data
+									bytesBuf[i+4], err = d.reader.ReadByte()
+									ch1 = bytesBuf[i+4]
+									nread++
+									len++
+								}
+								c2, n2 = rune(((uint32(ch0)&0x1f)<<6)+(uint32(ch1)&0x3f)), 2
+							} else if (ch0 & 0xf0) == 0xe0 {
+								var ch1, ch2 byte
+								if i+5 < len {
+									ch1 = bytesBuf[i+4]
+									ch2 = bytesBuf[i+5]
+								} else {
+									ch1, err = d.reader.ReadByte()
+									if err != nil {
+										return s, perrors.WithStack(err)
+									}
+									ch2, err = d.reader.ReadByte()
+									len += 2
+									nread += 2
+								}
+								c := ((uint32(ch0) & 0x0f) << 12) + ((uint32(ch1) & 0x3f) << 6) + (uint32(ch2) & 0x3f)
+								c2, n2 = rune(c), 3
+							}
+
+							c := rune(c1-0xD800)<<10 + (c2 - 0xDC00) + 0x10000
+							n3 := utf8.EncodeRune(bytesBuf[i:], c)
+							if copied = n3 > 0 && n3 < /** front three byte */ 3+n2; copied {
+								// We need to move the bytes,
+								// for example, less bytes after decoding
+								offset = i + n3
+								copy(bytesBuf[offset:], bytesBuf[i+3+n2:len])
+							}
+
+							i += n2
+							chunkLen--
+						}
+						i += 3
+
+						// fix read the next byte index
+						if copied {
+							copied = false
+							continue
+						}
+
+						offset += 3
+					} else {
+						return s, perrors.Errorf("bad utf-8 encoding")
+					}
+				}
+
+				if remain := offset - prev - nread; remain > 0 {
+					if remain == 1 {
+						ch, err := d.readByte()
+						if err != nil {
+							return s, perrors.WithStack(err)
+						}
+						bytesBuf[offset-1] = ch
+					} else {
+						var err error
+						if buffed := d.Buffered(); buffed < remain {
+							// trigger fill data if required
+							copy(bytesBuf[offset-remain:offset], d.peek(remain))
+							_, err = d.reader.Discard(remain)
+						} else {
+							// copy remaining bytes.
+							_, err = d.next(bytesBuf[offset-remain : offset])
+						}
+
+						if err != nil {
+							return s, perrors.WithStack(err)
+						}
+					}
+				}
+
+				// the expected length string has been processed.
+				if chunkLen <= 0 {
+					// we need to detect next chunk
+					continue
+				}
+			}
+
+			// decode byte
+			ch, err := d.readByte()
 			if err != nil {
 				if err == io.EOF {
 					break
@@ -365,14 +536,58 @@ func (d *Decoder) decString(flag int32) (string, error) {
 				return s, perrors.WithStack(err)
 			}
 
-			runeData[runeIndex] = r
-			runeIndex++
+			if ch < 0x80 {
+				bytesBuf[offset] = ch
+				offset++
+			} else if (ch & 0xe0) == 0xc0 {
+				ch1, err := d.readByte()
+				if err != nil {
+					return s, perrors.WithStack(err)
+				}
+				bytesBuf[offset] = ch
+				bytesBuf[offset+1] = ch1
+				offset += 2
+			} else if (ch & 0xf0) == 0xe0 {
+				var err error
+				if buffed := d.Buffered(); buffed < 2 {
+					// trigger fill data if required
+					copy(bytesBuf[offset+1:offset+3], d.peek(2))
+					_, err = d.reader.Discard(2)
+				} else {
+					_, err = d.next(bytesBuf[offset+1 : offset+3])
+				}
+				if err != nil {
+					return s, perrors.WithStack(err)
+				}
 
-			charCount += charLen
-			byteCount += byteLen
+				bytesBuf[offset] = ch
+
+				// we detect emoji first
+				c1 := ((uint32(ch) & 0x0f) << 12) + ((uint32(bytesBuf[offset+1]) & 0x3f) << 6) + (uint32(bytesBuf[offset+2]) & 0x3f)
+				if c1 >= 0xD800 && c1 <= 0xDBFF {
+					c2, n2, err := decodeUcs2Rune(d.reader)
+					if err != nil {
+						return s, perrors.WithStack(err)
+					}
+
+					c := rune(c1-0xD800)<<10 + (c2 - 0xDC00) + 0x10000
+					utf8.EncodeRune(bytesBuf[offset:], c)
+
+					// update next rune
+					offset += n2
+					chunkLen--
+				}
+
+				offset += 3
+			} else {
+				return s, perrors.Errorf("bad utf-8 encoding, offset=%d\n", offset)
+			}
+
+			chunkLen--
 		}
 
-		return string(runeData[:runeIndex]), nil
+		b := bytesBuf[:offset]
+		return *(*string)(unsafe.Pointer(&b)), nil
 	}
 
 	return s, perrors.Errorf("unknown string tag %#x\n", tag)


Test patch:
diff --git a/decode_test.go b/decode_test.go
index f91aca0..3728fec 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -33,6 +33,7 @@ import (
 
 const (
 	hessianJar = "test_hessian/target/test_hessian-1.0.0.jar"
+	testString = "hello, world! 你好，世界！"
 )
 
 func isFileExist(file string) bool {
diff --git a/string_test.go b/string_test.go
index 5751332..0531740 100644
--- a/string_test.go
+++ b/string_test.go
@@ -158,11 +158,25 @@ func TestStringEncode(t *testing.T) {
 	testJavaDecode(t, "argString_65536", s65560[:65536])
 }
 
+func BenchmarkDecodeStringOptimized(t *testing.B) {
+	e := NewEncoder()
+	e.Encode(testString)
+	buf := e.buffer
+
+	d := NewDecoder(buf)
+
+	for i := 0; i < t.N; i++ {
+		d.DecodeValue()
+		d.Reset(buf)
+	}
+}
+
 func TestStringEmoji(t *testing.T) {
 	// see: test_hessian/src/main/java/test/TestString.java
 	s0 := "emoji🤣"
 	s0 += ",max" + string(rune(0x10FFFF))
 
+	// todo 这里正确拿到hessian解码字节数组，但是构造string的时候，不是rune，emoji表情符号显示有点问题，修改assert？？？
 	testDecodeFramework(t, "customReplyStringEmoji", s0)
 	testJavaDecode(t, "customArgString_emoji", s0)
 }