Go lang 中 html/template 的性能缓慢,有什么解决方法吗?
Slow performance of html/template in Go lang, any workaround?
我正在对 Go 中的此类代码进行压力测试(使用 loader.io),以创建一个包含 100 个项目的数组以及一些其他基本变量,并在模板中解析它们:
package main
import (
"html/template"
"net/http"
)
var templates map[string]*template.Template
// Load templates on program initialisation
func init() {
if templates == nil {
templates = make(map[string]*template.Template)
}
templates["index.html"] = template.Must(template.ParseFiles("index.html"))
}
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
var Posts [100]Post
// Fill posts
for i := 0; i < 100; i++ {
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts [100]Post
}
var p Page
p.Title = "Index Page of My Super Blog"
p.Subtitle = "A blog about everything"
p.Posts = Posts
tmpl := templates["index.html"]
tmpl.ExecuteTemplate(w, "index.html", p)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8888", nil)
}
我对 Loader 的测试是在 1 分钟内使用 5k 并发 connections/s。问题是,在开始测试几秒钟后,我得到了很高的平均延迟(将近 10 秒),结果是 5k 成功响应,测试停止,因为它达到了 50% 的错误率(超时)。
在同一台机器上,PHP给出50k+。
我知道这不是 Go 的性能问题,但可能与 html/template 有关。 Go 可以轻松地管理足够困难的计算,当然比 PHP 之类的任何东西都快得多,但是当涉及到将数据解析到模板时,为什么这么糟糕?
有什么变通办法,或者我只是做错了(我是 Go 新手)?
P.S。实际上,即使有 1 个项目,它也完全相同...... 5-6k 并在大量超时后停止。但这可能是因为带有帖子的数组保持相同的长度。
我的模板代码(index.html):
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
这是 github.com/pkg/profile 的分析结果:
root@Test:~# go tool pprof app /tmp/profile311243501/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2054 samples
97 4.7% 4.7% 726 35.3% reflect.Value.call
89 4.3% 9.1% 278 13.5% runtime.mallocgc
85 4.1% 13.2% 86 4.2% syscall.Syscall
66 3.2% 16.4% 75 3.7% runtime.MSpan_Sweep
58 2.8% 19.2% 1842 89.7% text/template.(*state).walk
54 2.6% 21.9% 928 45.2% text/template.(*state).evalCall
51 2.5% 24.3% 53 2.6% settype
47 2.3% 26.6% 47 2.3% runtime.stringiter2
44 2.1% 28.8% 149 7.3% runtime.makeslice
40 1.9% 30.7% 223 10.9% text/template.(*state).evalField
这些是优化代码后的分析结果(如 icza 在回答中所建议的):
root@Test:~# go tool pprof app /tmp/profile501566907/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2811 samples
137 4.9% 4.9% 442 15.7% runtime.mallocgc
126 4.5% 9.4% 999 35.5% reflect.Value.call
113 4.0% 13.4% 115 4.1% syscall.Syscall
110 3.9% 17.3% 122 4.3% runtime.MSpan_Sweep
102 3.6% 20.9% 2561 91.1% text/template.(*state).walk
74 2.6% 23.6% 337 12.0% text/template.(*state).evalField
68 2.4% 26.0% 72 2.6% settype
66 2.3% 28.3% 1279 45.5% text/template.(*state).evalCall
65 2.3% 30.6% 226 8.0% runtime.makeslice
57 2.0% 32.7% 57 2.0% runtime.stringiter2
(pprof)
您正在使用数组和结构,它们都是非指针类型,也不是描述符(如切片、映射或通道)。因此,传递它们总是会创建值的副本,将数组值分配给变量会复制所有元素。这很慢并且给 GC 带来了大量的工作。
此外,您只使用了 1 个 CPU 核心。要利用更多,请将其添加到您的 main()
函数中:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8888", nil))
}
编辑: 这只是 Go 1.5 之前的情况。由于 Go 1.5 runtime.NumCPU()
是默认值。
您的代码
var Posts [100]Post
已分配 space 100 Post
秒的数组。
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
您使用复合文字创建了一个 Post
值,然后将该值复制到数组中的第 i
个元素中。 (冗余)
var p Page
这将创建一个 Page
类型的变量。它是一个 struct
,因此分配的内存还包含一个字段 Posts [100]Post
,因此分配了另一个 100
元素的数组。
p.Posts = Posts
这会复制 100
个元素(一百个结构)!
tmpl.ExecuteTemplate(w, "index.html", p)
这会创建 p
的副本(类型为 Page
),因此会创建另一个 100
帖子数组并复制 p
中的元素,然后它被传递给 ExecuteTemplate()
.
并且由于 Page.Posts
是一个数组,很可能在处理它时(在模板引擎中迭代),将从每个元素制作一个副本(尚未检查 - 未验证)。
更高效代码的提案
一些可以加快代码速度的事情:
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
Posts := make([]*Post, 100) // A slice of pointers
// Fill posts
for i := range Posts {
// Initialize pointers: just copies the address of the created struct value
Posts[i]= &Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts []*Post // "Just" a slice type (it's a descriptor)
}
// Create a page, only the Posts slice descriptor is copied
p := Page{"Index Page of My Super Blog", "A blog about everything", Posts}
tmpl := templates["index.html"]
// Only pass the address of p
// Although since Page.Posts is now just a slice, passing by value would also be OK
tmpl.ExecuteTemplate(w, "index.html", &p)
}
请测试此代码并报告您的结果。
PHP 没有同时响应 5000 个请求。这些请求被多路复用到少数进程以进行串行执行。这样可以更有效地利用 CPU 和内存。 5000 个并发连接对于消息代理或类似物可能有意义,对小块数据进行有限的处理,但对于任何进行真正 I/O 或处理的服务来说意义不大。如果您的 Go 应用程序不在某种类型的会限制并发请求数量的代理之后,您将希望自己这样做,也许在您的处理程序开始时,使用缓冲通道或等待组,la https://blakemesdag.com/blog/2014/11/12/limiting-go-concurrency/.
使用 html/template
的等效应用程序比 PHP 变体慢的主要原因有两个。
首先html/template
提供了比PHP更多的功能。主要区别在于 html/template
将使用正确的转义规则(HTML、JS、CSS 等)自动转义变量,具体取决于它们在结果 HTML 输出中的位置(其中我觉得很酷!)。
其次,html/template
渲染代码大量使用反射和参数数量可变的方法,它们的速度不如静态编译代码。
下面的模板
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
转换为
{{ .Title | html_template_htmlescaper }}
{{ .Subtitle | html_template_htmlescaper }}
{{ range .Posts }}
{{ .Title | html_template_htmlescaper }}
{{ .Content | html_template_htmlescaper }}
{{ end }}
在循环中使用反射调用 html_template_htmlescaper
会降低性能。
话虽如此,html/template
的这个微基准不应该被用来决定是否使用 Go。一旦您将使用数据库的代码添加到请求处理程序中,我怀疑模板呈现时间几乎不会引人注意。
此外,我很确定随着时间的推移,Go 反射和 html/template
包都会变得更快。
如果在实际应用程序中您会发现 html/template
是一个瓶颈,它仍然可以切换到 text/template
并为其提供已经转义的数据。
html/template
很慢,因为它使用 reflection,尚未针对速度进行优化。
尝试 quicktemplate 作为缓慢 html/template
的解决方法。根据其源代码的基准,目前 quicktemplate
比 html/template
快 20 倍以上。
您可以在 goTemplateBenchmark. Personally, I think Hero 查看模板基准,它是效率和可读性最好的结合。
Typed strings是你的朋友,如果你想加速html/template
。 pre-render 重复 HTML-fragments.
有时很有用
假设大部分时间都花在渲染那 100 个 Post 对象上,那么 pre-render 这些对象可能是有意义的。
我正在对 Go 中的此类代码进行压力测试(使用 loader.io),以创建一个包含 100 个项目的数组以及一些其他基本变量,并在模板中解析它们:
package main
import (
"html/template"
"net/http"
)
var templates map[string]*template.Template
// Load templates on program initialisation
func init() {
if templates == nil {
templates = make(map[string]*template.Template)
}
templates["index.html"] = template.Must(template.ParseFiles("index.html"))
}
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
var Posts [100]Post
// Fill posts
for i := 0; i < 100; i++ {
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts [100]Post
}
var p Page
p.Title = "Index Page of My Super Blog"
p.Subtitle = "A blog about everything"
p.Posts = Posts
tmpl := templates["index.html"]
tmpl.ExecuteTemplate(w, "index.html", p)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8888", nil)
}
我对 Loader 的测试是在 1 分钟内使用 5k 并发 connections/s。问题是,在开始测试几秒钟后,我得到了很高的平均延迟(将近 10 秒),结果是 5k 成功响应,测试停止,因为它达到了 50% 的错误率(超时)。
在同一台机器上,PHP给出50k+。
我知道这不是 Go 的性能问题,但可能与 html/template 有关。 Go 可以轻松地管理足够困难的计算,当然比 PHP 之类的任何东西都快得多,但是当涉及到将数据解析到模板时,为什么这么糟糕?
有什么变通办法,或者我只是做错了(我是 Go 新手)?
P.S。实际上,即使有 1 个项目,它也完全相同...... 5-6k 并在大量超时后停止。但这可能是因为带有帖子的数组保持相同的长度。
我的模板代码(index.html):
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
这是 github.com/pkg/profile 的分析结果:
root@Test:~# go tool pprof app /tmp/profile311243501/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2054 samples
97 4.7% 4.7% 726 35.3% reflect.Value.call
89 4.3% 9.1% 278 13.5% runtime.mallocgc
85 4.1% 13.2% 86 4.2% syscall.Syscall
66 3.2% 16.4% 75 3.7% runtime.MSpan_Sweep
58 2.8% 19.2% 1842 89.7% text/template.(*state).walk
54 2.6% 21.9% 928 45.2% text/template.(*state).evalCall
51 2.5% 24.3% 53 2.6% settype
47 2.3% 26.6% 47 2.3% runtime.stringiter2
44 2.1% 28.8% 149 7.3% runtime.makeslice
40 1.9% 30.7% 223 10.9% text/template.(*state).evalField
这些是优化代码后的分析结果(如 icza 在回答中所建议的):
root@Test:~# go tool pprof app /tmp/profile501566907/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2811 samples
137 4.9% 4.9% 442 15.7% runtime.mallocgc
126 4.5% 9.4% 999 35.5% reflect.Value.call
113 4.0% 13.4% 115 4.1% syscall.Syscall
110 3.9% 17.3% 122 4.3% runtime.MSpan_Sweep
102 3.6% 20.9% 2561 91.1% text/template.(*state).walk
74 2.6% 23.6% 337 12.0% text/template.(*state).evalField
68 2.4% 26.0% 72 2.6% settype
66 2.3% 28.3% 1279 45.5% text/template.(*state).evalCall
65 2.3% 30.6% 226 8.0% runtime.makeslice
57 2.0% 32.7% 57 2.0% runtime.stringiter2
(pprof)
您正在使用数组和结构,它们都是非指针类型,也不是描述符(如切片、映射或通道)。因此,传递它们总是会创建值的副本,将数组值分配给变量会复制所有元素。这很慢并且给 GC 带来了大量的工作。
此外,您只使用了 1 个 CPU 核心。要利用更多,请将其添加到您的 main()
函数中:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8888", nil))
}
编辑: 这只是 Go 1.5 之前的情况。由于 Go 1.5 runtime.NumCPU()
是默认值。
您的代码
var Posts [100]Post
已分配 space 100 Post
秒的数组。
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
您使用复合文字创建了一个 Post
值,然后将该值复制到数组中的第 i
个元素中。 (冗余)
var p Page
这将创建一个 Page
类型的变量。它是一个 struct
,因此分配的内存还包含一个字段 Posts [100]Post
,因此分配了另一个 100
元素的数组。
p.Posts = Posts
这会复制 100
个元素(一百个结构)!
tmpl.ExecuteTemplate(w, "index.html", p)
这会创建 p
的副本(类型为 Page
),因此会创建另一个 100
帖子数组并复制 p
中的元素,然后它被传递给 ExecuteTemplate()
.
并且由于 Page.Posts
是一个数组,很可能在处理它时(在模板引擎中迭代),将从每个元素制作一个副本(尚未检查 - 未验证)。
更高效代码的提案
一些可以加快代码速度的事情:
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
Posts := make([]*Post, 100) // A slice of pointers
// Fill posts
for i := range Posts {
// Initialize pointers: just copies the address of the created struct value
Posts[i]= &Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts []*Post // "Just" a slice type (it's a descriptor)
}
// Create a page, only the Posts slice descriptor is copied
p := Page{"Index Page of My Super Blog", "A blog about everything", Posts}
tmpl := templates["index.html"]
// Only pass the address of p
// Although since Page.Posts is now just a slice, passing by value would also be OK
tmpl.ExecuteTemplate(w, "index.html", &p)
}
请测试此代码并报告您的结果。
PHP 没有同时响应 5000 个请求。这些请求被多路复用到少数进程以进行串行执行。这样可以更有效地利用 CPU 和内存。 5000 个并发连接对于消息代理或类似物可能有意义,对小块数据进行有限的处理,但对于任何进行真正 I/O 或处理的服务来说意义不大。如果您的 Go 应用程序不在某种类型的会限制并发请求数量的代理之后,您将希望自己这样做,也许在您的处理程序开始时,使用缓冲通道或等待组,la https://blakemesdag.com/blog/2014/11/12/limiting-go-concurrency/.
使用 html/template
的等效应用程序比 PHP 变体慢的主要原因有两个。
首先html/template
提供了比PHP更多的功能。主要区别在于 html/template
将使用正确的转义规则(HTML、JS、CSS 等)自动转义变量,具体取决于它们在结果 HTML 输出中的位置(其中我觉得很酷!)。
其次,html/template
渲染代码大量使用反射和参数数量可变的方法,它们的速度不如静态编译代码。
下面的模板
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
转换为
{{ .Title | html_template_htmlescaper }}
{{ .Subtitle | html_template_htmlescaper }}
{{ range .Posts }}
{{ .Title | html_template_htmlescaper }}
{{ .Content | html_template_htmlescaper }}
{{ end }}
在循环中使用反射调用 html_template_htmlescaper
会降低性能。
话虽如此,html/template
的这个微基准不应该被用来决定是否使用 Go。一旦您将使用数据库的代码添加到请求处理程序中,我怀疑模板呈现时间几乎不会引人注意。
此外,我很确定随着时间的推移,Go 反射和 html/template
包都会变得更快。
如果在实际应用程序中您会发现 html/template
是一个瓶颈,它仍然可以切换到 text/template
并为其提供已经转义的数据。
html/template
很慢,因为它使用 reflection,尚未针对速度进行优化。
尝试 quicktemplate 作为缓慢 html/template
的解决方法。根据其源代码的基准,目前 quicktemplate
比 html/template
快 20 倍以上。
您可以在 goTemplateBenchmark. Personally, I think Hero 查看模板基准,它是效率和可读性最好的结合。
Typed strings是你的朋友,如果你想加速html/template
。 pre-render 重复 HTML-fragments.
假设大部分时间都花在渲染那 100 个 Post 对象上,那么 pre-render 这些对象可能是有意义的。