From 72c92672b79a35bca025a6d8835ad82da7f00743 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sat, 15 Dec 2018 14:39:50 +1000 Subject: [PATCH] Use unsafe to reduce the number of string to slice copies in templates. Use sliced arrays to reduce the amount of null padding in template fragments. Revert a failed optimisation in templates. Remove a few more redundant branches in variant templates. Added the unsafe function StringToBytes. Added BenchmarkTopicGuestRouteParallelWithRouterAlt. --- common/files.go | 20 +++++++++++ common/template_init.go | 27 +++++++++----- common/templates/templates.go | 68 ++++++++++++++++++++++++++--------- general_test.go | 6 ++++ tmpl_client/stub.go | 20 +++++++++++ tmpl_stub.go | 20 +++++++++++ 6 files changed, 137 insertions(+), 24 deletions(-) diff --git a/common/files.go b/common/files.go index 60cfa3a7..a348a2fa 100644 --- a/common/files.go +++ b/common/files.go @@ -154,6 +154,24 @@ func (list SFileList) JSTmplInit() error { data[braceAt] = ' ' // Blank it } }) + each("w.Write(StringToBytes(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') + // TODO: Make sure we don't go onto the next line in case someone misplaced a brace + if hasEndBrace { + data[braceAt] = ' ' // Blank it + } + braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')') + if hasEndBrace { + data[braceAt] = ' ' // Blank this one too + } + }) + each(" = StringToBytes(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') + // TODO: Make sure we don't go onto the next line in case someone misplaced a brace + if hasEndBrace { + data[braceAt] = ' ' // Blank it + } + }) each("w.Write(", func(index int) { braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') // TODO: Make sure we don't go onto the next line in case someone misplaced a brace @@ -183,6 +201,7 @@ func (list SFileList) JSTmplInit() error { }) data = replace(data, "for _, item := range ", "for(item of ") data = replace(data, "w.Write([]byte(", "out += ") + data = replace(data, "w.Write(StringToBytes(", "out += ") data = replace(data, "w.Write(", "out += ") data = replace(data, "strconv.Itoa(", "") data = replace(data, "strconv.FormatInt(", "") @@ -194,6 +213,7 @@ func (list SFileList) JSTmplInit() error { //data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);") data = replace(data, "var cached_var_", "let cached_var_") data = replace(data, " = []byte(", " = ") + data = replace(data, " = StringToBytes(", " = ") data = replace(data, "if ", "if(") data = replace(data, "return nil", "return out") data = replace(data, " )", ")") diff --git a/common/template_init.go b/common/template_init.go index f71db65f..7c48b3f6 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -509,18 +509,29 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) } getterstr += "}\nreturn nil\n}\n" out += "\n// nolint\nfunc init() {\n" - var bodyMap = make(map[string]string) //map[body]fragmentPrefix + //var bodyMap = make(map[string]string) //map[body]fragmentPrefix + var tmpCount = 0 for _, frag := range c.FragOut { - var fragmentPrefix string front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]" - fp, ok := bodyMap[frag.Body] + /*fp, ok := bodyMap[frag.Body] if !ok { - fragmentPrefix = front + " = []byte(`" + frag.Body + "`)\n" - bodyMap[frag.Body] = front - } else { - fragmentPrefix = front + " = " + fp + "\n" + bodyMap[frag.Body] = front*/ + var bits string + for _, char := range []byte(frag.Body) { + if char == '\'' { + bits += "'\\" + string(char) + "'," + } else { + bits += "'" + string(char) + "'," + } } - out += fragmentPrefix + tmpStr := strconv.Itoa(tmpCount) + out += "arr_" + tmpStr + " := [...]byte{" + bits + "}\n" + out += front + " = arr_" + tmpStr + "[:]\n" + tmpCount++ + //out += front + " = []byte(`" + frag.Body + "`)\n" + /*} else { + out += front + " = " + fp + "\n" + }*/ } out += "\n" + getterstr + "}\n" err := writeFile(prefix+"template_list.go", out) diff --git a/common/templates/templates.go b/common/templates/templates.go index 5ecde011..a23504f9 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -425,6 +425,15 @@ func (c *CTemplateSet) rootIterate(tree *parse.Tree, con CContext) { c.retCall("rootIterate") } +var inSlice = func(haystack []string, expr string) bool { + for _, needle := range haystack { + if needle == expr { + return true + } + } + return false +} + func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) { c.dumpCall("compileSwitch", con, node) defer c.retCall("compileSwitch") @@ -451,14 +460,6 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) { c.detail("Expression:", expr) // Simple member / guest optimisation for now // TODO: Expand upon this - var inSlice = func(haystack []string, expr string) bool { - for _, needle := range haystack { - if needle == expr { - return true - } - } - return false - } var userExprs = []string{ con.RootHolder + ".CurrentUser.Loggedin", con.RootHolder + ".CurrentUser.IsSuperMod", @@ -861,7 +862,7 @@ func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) c.dumpCall("compileIdentSwitch", con, node) var litString = func(inner string, bytes bool) { if !bytes { - inner = "[]byte(" + inner + ")" + inner = "StringToBytes(" + inner + ")" } out = "w.Write(" + inner + ")\n" literal = true @@ -1219,7 +1220,7 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V // Is this a literal string? if len(varname) != 0 && varname[0] == '"' { - con.Push("lvarsub", onEnd(assLines+"w.Write([]byte("+varname+"))\n")) + con.Push("lvarsub", onEnd(assLines+"w.Write(StringToBytes("+varname+"))\n")) return } for _, varItem := range c.varList { @@ -1250,9 +1251,44 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V switch val.Kind() { case reflect.Int: c.importMap["strconv"] = "strconv" - base = "[]byte(strconv.Itoa(" + varname + "))" + base = "StringToBytes(strconv.Itoa(" + varname + "))" case reflect.Bool: - // TODO: Take c.guestOnly / c.memberOnly into account + // TODO: Take c.memberOnly into account + // TODO: Make this a template fragment so more optimisations can be applied to this + // TODO: De-duplicate this logic + var userExprs = []string{ + con.RootHolder + ".CurrentUser.Loggedin", + con.RootHolder + ".CurrentUser.IsSuperMod", + con.RootHolder + ".CurrentUser.IsAdmin", + } + var negUserExprs = []string{ + "!" + con.RootHolder + ".CurrentUser.Loggedin", + "!" + con.RootHolder + ".CurrentUser.IsSuperMod", + "!" + con.RootHolder + ".CurrentUser.IsAdmin", + } + if c.guestOnly { + c.detail("optimising away member branch") + if inSlice(userExprs, varname) { + c.detail("positive conditional:", varname) + con.Push("varsub", "[]byte(\"false\")") + return + } else if inSlice(negUserExprs, varname) { + c.detail("negative conditional:", varname) + con.Push("varsub", "[]byte(\"true\")") + return + } + } else if c.memberOnly { + c.detail("optimising away guest branch") + if (con.RootHolder + ".CurrentUser.Loggedin") == varname { + c.detail("positive conditional:", varname) + con.Push("varsub", "[]byte(\"true\")") + return + } else if ("!" + con.RootHolder + ".CurrentUser.Loggedin") == varname { + c.detail("negative conditional:", varname) + con.Push("varsub", "[]byte(\"false\")") + return + } + } con.Push("startif", "if "+varname+" {\n") con.Push("varsub", "[]byte(\"true\")") con.Push("endif", "} ") @@ -1264,19 +1300,19 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") { varname = "string(" + varname + ")" } - base = "[]byte(" + varname + ")" + base = "StringToBytes(" + varname + ")" // We don't to waste time on this conversion / w.Write call when guests don't have sessions // TODO: Implement this properly - if c.guestOnly && base == "[]byte("+con.RootHolder+".CurrentUser.Session))" { + if c.guestOnly && base == "StringToBytes("+con.RootHolder+".CurrentUser.Session))" { return } case reflect.Int64: c.importMap["strconv"] = "strconv" - base = "[]byte(strconv.FormatInt(" + varname + ", 10))" + base = "StringToBytes(strconv.FormatInt(" + varname + ", 10))" case reflect.Struct: // TODO: Avoid clashing with other packages which have structs named Time if val.Type().Name() == "Time" { - base = "[]byte(" + varname + ".String())" + base = "StringToBytes(" + varname + ".String())" } else { if !val.IsValid() { panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") diff --git a/general_test.go b/general_test.go index 5bacbb10..992b52ad 100644 --- a/general_test.go +++ b/general_test.go @@ -288,10 +288,16 @@ func BenchmarkTopicGuestRouteParallelWithRouter(b *testing.B) { obRoute(b, "/topic/hm."+benchTid) } +func BenchmarkTopicGuestRouteParallelWithRouterAlt(b *testing.B) { + obRoute(b, "/topic/hm."+benchTid) +} + func BenchmarkBadRouteGuestRouteParallelWithRouter(b *testing.B) { obRoute(b, "/garble/haa") } +// TODO: Alternate between member and guest to bust some CPU caches? + func binit(b *testing.B) { b.ReportAllocs() err := gloinit() diff --git a/tmpl_client/stub.go b/tmpl_client/stub.go index 7c0dc0ed..041d1e71 100644 --- a/tmpl_client/stub.go +++ b/tmpl_client/stub.go @@ -1,5 +1,25 @@ package tmpl +import ( + "reflect" + "runtime" + "unsafe" +) + var GetFrag = func(name string) [][]byte { return nil } + +type WriteString interface { + WriteString(s string) (n int, err error) +} + +func StringToBytes(s string) (bytes []byte) { + str := (*reflect.StringHeader)(unsafe.Pointer(&s)) + slice := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) + slice.Data = str.Data + slice.Len = str.Len + slice.Cap = str.Len + runtime.KeepAlive(&s) + return bytes +} diff --git a/tmpl_stub.go b/tmpl_stub.go index 449cea1c..cc94bfce 100644 --- a/tmpl_stub.go +++ b/tmpl_stub.go @@ -1,5 +1,25 @@ package main +import ( + "reflect" + "runtime" + "unsafe" +) + var GetFrag = func(name string) [][]byte { return nil } + +type WriteString interface { + WriteString(s string) (n int, err error) +} + +func StringToBytes(s string) (bytes []byte) { + str := (*reflect.StringHeader)(unsafe.Pointer(&s)) + slice := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) + slice.Data = str.Data + slice.Len = str.Len + slice.Cap = str.Len + runtime.KeepAlive(&s) + return bytes +}