Browse Source

Performance optimizations

- Removed superflouos memory allocations from NewTemplate
- Added Template.Reset, so the Template object could be re-used
Aliaksandr Valialkin 9 năm trước cách đây
mục cha
commit
309591eb61
4 tập tin đã thay đổi với 123 bổ sung19 xóa
  1. 61 19
      template.go
  2. 25 0
      template_test.go
  3. 17 0
      template_timing_test.go
  4. 20 0
      unsafe.go

+ 61 - 19
template.go

@@ -16,6 +16,10 @@ import (
 // Template implements simple template engine, which can be used for fast
 // tags' (aka placeholders) substitution.
 type Template struct {
+	template string
+	startTag string
+	endTag   string
+
 	texts           [][]byte
 	tags            []string
 	bytesBufferPool sync.Pool
@@ -44,6 +48,40 @@ func New(template, startTag, endTag string) *Template {
 // using Execute* methods.
 func NewTemplate(template, startTag, endTag string) (*Template, error) {
 	var t Template
+	t.bytesBufferPool.New = newBytesBuffer
+	err := t.Reset(template, startTag, endTag)
+	if err != nil {
+		return nil, err
+	}
+	return &t, nil
+}
+
+func newBytesBuffer() interface{} {
+	return &bytes.Buffer{}
+}
+
+// TagFunc can be used as a substitution value in the map passed to Execute*.
+// Execute* functions pass tag (placeholder) name in 'tag' argument.
+//
+// TagFunc must be safe to call from concurrently running goroutines.
+//
+// TagFunc must write contents to w and return the number of bytes written.
+type TagFunc func(w io.Writer, tag string) (int, error)
+
+// Reset resets the template t to new one defined by
+// template, startTag and endTag.
+//
+// Reset allows Template object re-use.
+//
+// Reset may be called only if no other goroutines call t methods at the moment.
+func (t *Template) Reset(template, startTag, endTag string) error {
+	// Keep these vars in t, so GC won't collect them and won't break
+	// vars derived via unsafe*
+	t.template = template
+	t.startTag = startTag
+	t.endTag = endTag
+	t.texts = t.texts[:0]
+	t.tags = t.tags[:0]
 
 	if len(startTag) == 0 {
 		panic("startTag cannot be empty")
@@ -52,9 +90,21 @@ func NewTemplate(template, startTag, endTag string) (*Template, error) {
 		panic("endTag cannot be empty")
 	}
 
-	s := []byte(template)
-	a := []byte(startTag)
-	b := []byte(endTag)
+	s := unsafeString2Bytes(template)
+	a := unsafeString2Bytes(startTag)
+	b := unsafeString2Bytes(endTag)
+
+	tagsCount := bytes.Count(s, a)
+	if tagsCount == 0 {
+		return nil
+	}
+
+	if tagsCount+1 > cap(t.texts) {
+		t.texts = make([][]byte, 0, tagsCount+1)
+	}
+	if tagsCount > cap(t.tags) {
+		t.tags = make([]string, 0, tagsCount)
+	}
 
 	for {
 		n := bytes.Index(s, a)
@@ -67,29 +117,16 @@ func NewTemplate(template, startTag, endTag string) (*Template, error) {
 		s = s[n+len(a):]
 		n = bytes.Index(s, b)
 		if n < 0 {
-			return nil, fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s)
+			return fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s)
 		}
 
-		t.tags = append(t.tags, string(s[:n]))
+		t.tags = append(t.tags, unsafeBytes2String(s[:n]))
 		s = s[n+len(b):]
 	}
 
-	t.bytesBufferPool.New = newBytesBuffer
-	return &t, nil
-}
-
-func newBytesBuffer() interface{} {
-	return &bytes.Buffer{}
+	return nil
 }
 
-// TagFunc can be used as a substitution value in the map passed to Execute*.
-// Execute* functions pass tag (placeholder) name in 'tag' argument.
-//
-// TagFunc must be safe to call from concurrently running goroutines.
-//
-// TagFunc must write contents to w and return the number of bytes written.
-type TagFunc func(w io.Writer, tag string) (int, error)
-
 // ExecuteFunc calls f on each template tag (placeholder) occurrence.
 //
 // Returns the number of bytes written to w.
@@ -97,6 +134,11 @@ func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) {
 	var nn int64
 
 	n := len(t.texts) - 1
+	if n == -1 {
+		ni, err := w.Write(unsafeString2Bytes(t.template))
+		return int64(ni), err
+	}
+
 	for i := 0; i < n; i++ {
 		ni, err := w.Write(t.texts[i])
 		if err != nil {

+ 25 - 0
template_test.go

@@ -76,6 +76,31 @@ func TestEndWithTag(t *testing.T) {
 	}
 }
 
+func TestTemplateReset(t *testing.T) {
+	template := "foo{bar}baz"
+	tpl := New(template, "{", "}")
+	s := tpl.ExecuteString(map[string]interface{}{"bar": "111"})
+	result := "foo111baz"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+
+	template = "[xxxyyyzz"
+	if err := tpl.Reset(template, "[", "]"); err == nil {
+		t.Fatalf("expecting error for unclosed tag on %q", template)
+	}
+
+	template = "[xxx]yyy[zz]"
+	if err := tpl.Reset(template, "[", "]"); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	s = tpl.ExecuteString(map[string]interface{}{"xxx": "11", "zz": "2222"})
+	result = "11yyy2222"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
 func TestDuplicateTags(t *testing.T) {
 	template := "[foo]bar[foo][foo]baz"
 	tpl := New(template, "[", "]")

+ 17 - 0
timing_test.go → template_timing_test.go

@@ -229,3 +229,20 @@ func BenchmarkFastTemplateExecuteTagFunc(b *testing.B) {
 		}
 	})
 }
+
+func BenchmarkNewTemplate(b *testing.B) {
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			_ = New(source, "{{", "}}")
+		}
+	})
+}
+
+func BenchmarkTemplateReset(b *testing.B) {
+	b.RunParallel(func(pb *testing.PB) {
+		t := New(source, "{{", "}}")
+		for pb.Next() {
+			t.Reset(source, "{{", "}}")
+		}
+	})
+}

+ 20 - 0
unsafe.go

@@ -0,0 +1,20 @@
+package fasttemplate
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+func unsafeBytes2String(b []byte) string {
+	return *(*string)(unsafe.Pointer(&b))
+}
+
+func unsafeString2Bytes(s string) []byte {
+	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
+	bh := reflect.SliceHeader{
+		Data: sh.Data,
+		Len:  sh.Len,
+		Cap:  sh.Len,
+	}
+	return *(*[]byte)(unsafe.Pointer(&bh))
+}