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版本是否需要再等個五年了 :)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM