• go 打造世界最快的go模板引擎gorazor 2.0


    打造世界最快的go模板引擎gorazor 2.0

    自2014年与 @于康

     等小伙伴发布gorazor后,我其实没有想过还会再给它做更新,因为近些年,网站的开发基本朝前后端分离的方向发展,一个供后端使用的模板引擎其实使用场景不多。

    gorazor应该是go语言的第一个支持将模板编译成为go代码的“预编译式”模板引擎。

    采用预编译一个显而易见的好处当然是渲染速度;没想一晃五年过去,go的后端模板引擎居然层出不穷,而相比起后来出现的这些模板,gorazor的渲染速度,赫然是最慢的一个

    QuickTemplate也有在其FAQ中说gorazor相比起QuickTemplate少了性能优化。

    QuickTemplate的作者也是fasthttp的作者Aliaksandr Valialkin,后者是go语言中非常有名http服务器实现,比go官方内置的net/http库快了近10倍。

    在研究过QuickTemplate之后,我对其作者valyala采用的优化手段叹为观止,试列举几项如下:

    • 使用bytebufferpool做字符串输出的缓存池
    • 模板渲染过程中实现了zero alloc 零内存分配
    • 使用unsafe包来实现对string / bytes的相互转换
    • 独立实现了writer库以优化对不同类型数据(int / bytes / float64等等)的转换与写入

    这些优化我感觉其实已经超乎了其模板引擎本身,而是借鉴在其它很多go项目中。

    感叹之余,我也不禁在想,gorazor能否借鉴这些来做优化呢?

    gorazor渲染慢的主要原因,是因为我提供的全部都是基于string的输入、输出接口;那么,在压测(当然还有实际运行)的时候,必然会有大量的字符串对象被创建、销毁;内存申请多,性能肯定也就慢。

    增加StringWriter接口的话,应该可以显著提高性能:

    // string 输入/输出接口
    func Msg(u *models.User) string {
    ...
    }
    
    // StringWriter接口
    func RenderMsg(_buffer io.StringWriter, u *models.User) {
    ...
    }
    
    // 后者相对于前者,无需创建并返回 string 对象;而是将数据写入 _buffer 中
    // 写入 _buffer的话就可能利用缓存池来避免内存分配

    当然,整体接口的改动也意味着需要对gorazor内部的代码生成引擎做大幅修改,特别是goraozr支持layout / helper的嵌套调用;所有内部谢谢嵌套的调用逻辑都需要改。

    但,那就改吧~既然是整体接口的大幅修改,那么版本号是可以升级到2.0的。

    经过几个月断断续续的修改,2.0版终于完成。因为支持了Writer接口,我实际上也可以直接使用QuickTemplate的bytebufferpool等方式来进行优化性能测试结果,这只需要几行代码

    type quickStringWriter struct {
        bb *quicktemplate.ByteBuffer
    }
    func (q *quickStringWriter) WriteString(s string) (i int, e error) {
        return q.bb.Write(unsafeStrToBytes(s))
    }

    便可直接将quickStringWriter传递给gorazor模板。

    这样一来,gorazor的性能实际上已经不逊于QuickTemplate了,毕竟,压测中性能的主要损耗是在字符串对象的转换以及缓存池的复用。

    但gorazor依旧做不到内存的零分配,唯一的一次内存分配是发生在这里

    // HTMLEscape wraps template.HTMLEscapeString
    func HTMLEscape(m interface{}) string {
        switch v := m.(type) {
        case int:
            return strconv.Itoa(v)
        case string:
            return template.HTMLEscapeString(v)
        }
    s := fmt.Sprint(m)
        return template.HTMLEscapeString(s)
    }

    当在模板中写入变量时,变量的类型很可能是不同的;最常见的当然是有int以及string;不同的类型也需要做不同的处理,可就是这么一句switch v := m.(type) { 类型判断会产生新变量以造成内存分配。

    咋看这几乎无解,因为我不可能在模板中限制需要渲染的数据类型;"类型转换"是必不可免的。

    回头去看QuickTemplate,它对此问题的解决方法,让我不禁莞尔:

    <li>ID={%d row.ID %}, Message={%s row.Message %}</li>

    QuickTemplate要求使用者在编写模板的时候,就直接指定插入变量的类型,如果是int,那么就使用 <%d %>的标签,如果是字符串,就使用<%s %>

    这需要引入新的语法不说,我认为也是会对模板使用者造成一定的心智负担,那么,有没有办法解决呢?

    同样的模板代码,在gorazor中是这么表示:

    <li>ID=@row.ID, Message=@row.Message</li>

    而生成出来的代码是:

    _buffer.WriteString("<li>ID=")
    _buffer.WriteString(gorazor.HTMLEscape(row.ID))
    _buffer.WriteString(", Message=")
    _buffer.WriteString(gorazor.HTMLEscape(row.Message))
    _buffer.WriteString("</li>")

    经过一番尝试,我发现可以使用go/types库,对生成出来的代码做类型分析,当通过编译器知道row.ID以及row.Message的具体类型后,我自然可以将调用函数自动替换成为具体类型的版本,也就是说,重新生成以下的代码:

    _buffer.WriteString("<li>ID=")
    _buffer.WriteString(gorazor.HTMLEscInt(row.ID))
    _buffer.WriteString(", Message=")
    _buffer.WriteString(gorazor.HTMLEscStr(row.Message))
    _buffer.WriteString("</li>")

    这么一来,gorazor模板,也就能够做到零内存分配了!

    压测的结果非常让人鼓舞:

    • 同样使用QuickTemplate自身的缓存池以及unsafe转换下,gorazor可以比QuickTemplate快80%
      • 因为razor从语法上就是要去除标签间的一些空格;我实际上是手动修改了gorazor生成出来的代码,增加不必要的空格输出,以确保压测时输出的跟QuickTemplate是严格一致的数据;如果不做这些额外的空格输出,gorazor还会更快。
    • 即便不使用缓存池,也不会比QuickTemplate慢太多

    若按比QuickTemplate快80%的结果看,gorazor 2.0应该是目前世界最快的go模板引擎了。

    我其实觉得这样的性能测试意义并不大;因为很容易为压测跑分做优化,实际使用中,模板的方便性应该会更加重要;我会特别希望在gorazor 3.0中,增加Language Server Protocol的支持,这样就可以在VS Code等编辑器中,对模板也实现完整的智能补全。

    就不知道3.0版本是否需要再等个五年了 :)

  • 相关阅读:
    触达项目涉及到的功能点
    NodeJS编程基础
    C#Socket通讯
    HTML转义字符大全
    C# 二进制,十进制,十六进制 互转
    浏览器的分类
    Prometheus设置systemctl管理
    第十五讲:Pagerduty的联用
    第十四讲:Prometheus 企业级实际使⽤二
    第十三讲:Prometheus 企业级实际使⽤
  • 原文地址:https://www.cnblogs.com/jackey2015/p/11149124.html
Copyright © 2020-2023  润新知