12

I'm stress testing (with loader.io) this type of code in Go to create an array of 100 items along with some other basic variables and parse them all in the template:

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)
}

My test with Loader is using 5k concurrent connections/s through 1 minute. The problem is, just after a few seconds after starting the test, I get a high average latency (almost 10s) and as a result 5k successful responses and the test stops because it reaches the Error Rate of 50% (timeouts).

On the same machine, PHP gives 50k+.

I understand that it's not Go performance issue, but probably something related to html/template. Go can easily manage hard enough calculations a lot faster than anything like PHP of course, but when it comes to parsing the data to the template, why is it so awful?

Any workarounds, or probably I'm just doing it wrong (I'm new to Go)?

P.S. Actually even with 1 item it's exactly the same... 5-6k and stopping after huge amount of timeouts. But that's probably because the array with posts is staying of the same length.

My template code (index.html):

{{ .Title }}
{{ .Subtitle }}

{{ range .Posts }}
        {{ .Title }}
        {{ .Content }}
{{ end }}

Here's the profiling result of 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

These are profiling results after refining the code (as suggested in the answer by 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)
4

5 回答 5

11

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并提供已经转义的数据。

于 2015-07-14T06:25:59.937 回答
10

您正在使用数组和结构,它们都是非指针类型,也不是描述符(如切片、映射或通道)。因此传递它们总是会创建值的副本,将数组值分配给变量会复制所有元素。这很慢,并且给 GC 带来了大量的工作。


此外,您仅使用 1 个 CPU 内核。要使用更多,请将其添加到您的main()函数中:

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8888", nil))
}

编辑: 这只是 Go 1.5 之前的情况。因为 Go 1.5runtime.NumCPU()是默认的。


你的代码

var Posts [100]Post

Post分配了一个空间为 100 秒的数组。

Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}

您使用复合文字创建一个Post值,然后将该值复制到i数组中的第 th 个元素中。(多余的)

var p Page

这将创建一个类型为 的变量Page。它是 a struct,因此分配了它的内存,其中还包含一个字段,因此分配了另一个元素Posts [100]Post数组。100

p.Posts = Posts

这会复制100元素(一百个结构)!

tmpl.ExecuteTemplate(w, "index.html", p)

p这会创建一个(类型为)的副本,因此会创建另一个帖子Page数组并复制其中的元素,然后将其传递给.100pExecuteTemplate()

而且由于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)
}

请测试此代码并报告您的结果。

于 2015-07-12T11:12:23.113 回答
1

PHP 没有同时响应 5000 个请求。请求被多路复用到少数进程以进行串行执行。这样可以更有效地利用 CPU 和内存。5000 个并发连接对于消息代理或类似的,对小块数据进行有限处理可能是有意义的,但对于任何进行真正 I/O 或处理的服务来说意义不大。如果您的 Go 应用程序没有使用会限制并发请求数量的某种类型的代理,您可能希望自己这样做,也许在处理程序的开头,使用缓冲通道或等待组,例如https: //blakemesdag.com/blog/2014/11/12/limiting-go-concurrency/

于 2015-07-13T23:52:35.113 回答
1

html/template速度很慢,因为它使用了尚未针对速度进行优化的反射。

尝试quicktemplate作为 slow 的解决方法html/template。目前quicktemplatehtml/template根据其源代码的基准测试快 20 倍以上。

于 2016-03-16T14:16:42.187 回答
0

您可以在goTemplateBenchmark上查看模板基准。就个人而言,我认为Hero是最能兼顾效率和可读性的一款。

于 2020-01-23T18:15:10.870 回答