]> Cypherpunks repositories - gostls13.git/commitdiff
go/printer: reduce allocations to improve performance
authorDaniel Martí <mvdan@mvdan.cc>
Fri, 19 Aug 2022 11:00:01 +0000 (12:00 +0100)
committerDaniel Martí <mvdan@mvdan.cc>
Fri, 9 Sep 2022 15:10:10 +0000 (15:10 +0000)
First, we know that Go source files almost always weigh at least a few
kilobytes, so we can kickstart the output buffer to be a reasonable size
and reduce the initial number of incremental allocations and copies when
appending bytes or strings to output.

Second, in nodeSize we use a nested printer, but we don't actually need
its printed bytes - we only need to know how many bytes it prints.
For that reason, use a throwaway buffer: the part of our output buffer
between length and capacity, as we haven't used it yet.

Third, use a sync.Pool to reuse allocated printers.
The current API doesn't allow reusing printers,
and some programs like gofmt will print many files in sequence.

Those changes combined result in a modest reduction in allocations and
CPU usage. The benchmark uses testdata/parser.go, which has just over
two thousand lines of code, which is pretty standard size-wise.

We also split the Print benchmark to cover both a medium-sized ast.File
as well as a pretty small ast.Decl node. The latter is a somewhat common
scenario in gopls, which has code actions which alter small bits of the
AST and print them back out to rewrite only a few lines in a file.

name          old time/op    new time/op     delta
PrintFile-16    5.43ms ± 1%     4.85ms ± 3%  -10.68%  (p=0.000 n=9+10)
PrintDecl-16    19.1µs ± 0%     18.5µs ± 1%   -3.04%  (p=0.000 n=10+10)

name          old speed      new speed       delta
PrintFile-16  9.56MB/s ± 1%  10.69MB/s ± 3%  +11.81%  (p=0.000 n=8+10)
PrintDecl-16  1.67MB/s ± 0%   1.73MB/s ± 1%   +3.05%  (p=0.000 n=10+10)

name          old alloc/op   new alloc/op    delta
PrintFile-16     332kB ± 0%      107kB ± 2%  -67.87%  (p=0.000 n=10+10)
PrintDecl-16    3.92kB ± 0%     3.28kB ± 0%  -16.38%  (p=0.000 n=10+10)

name          old allocs/op  new allocs/op   delta
PrintFile-16     3.45k ± 0%      2.42k ± 0%  -29.90%  (p=0.000 n=10+10)
PrintDecl-16      56.0 ± 0%       46.0 ± 0%  -17.86%  (p=0.000 n=10+10)

Change-Id: I475a3babca77532b2d51888f49710f74763d81d2
Reviewed-on: https://go-review.googlesource.com/c/go/+/424924
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
src/go/printer/performance_test.go
src/go/printer/printer.go

index ea6a98caa4a98498ff8452c2420f8081b52f5747..c58f6d470610a8861e5c6bbadfdbeae51e7e81f4 100644 (file)
@@ -11,6 +11,7 @@ import (
        "bytes"
        "go/ast"
        "go/parser"
+       "go/token"
        "io"
        "log"
        "os"
@@ -18,12 +19,15 @@ import (
 )
 
 var (
-       testfile *ast.File
-       testsize int64
+       fileNode *ast.File
+       fileSize int64
+
+       declNode ast.Decl
+       declSize int64
 )
 
-func testprint(out io.Writer, file *ast.File) {
-       if err := (&Config{TabIndent | UseSpaces | normalizeNumbers, 8, 0}).Fprint(out, fset, file); err != nil {
+func testprint(out io.Writer, node ast.Node) {
+       if err := (&Config{TabIndent | UseSpaces | normalizeNumbers, 8, 0}).Fprint(out, fset, node); err != nil {
                log.Fatalf("print error: %s", err)
        }
 }
@@ -48,17 +52,40 @@ func initialize() {
                log.Fatalf("print error: %s not idempotent", filename)
        }
 
-       testfile = file
-       testsize = int64(len(src))
+       fileNode = file
+       fileSize = int64(len(src))
+
+       for _, decl := range file.Decls {
+               // The first global variable, which is pretty short:
+               //
+               //      var unresolved = new(ast.Object)
+               if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.VAR {
+                       declNode = decl
+                       declSize = int64(fset.Position(decl.End()).Offset - fset.Position(decl.Pos()).Offset)
+                       break
+               }
+
+       }
+}
+
+func BenchmarkPrintFile(b *testing.B) {
+       if fileNode == nil {
+               initialize()
+       }
+       b.ReportAllocs()
+       b.SetBytes(fileSize)
+       for i := 0; i < b.N; i++ {
+               testprint(io.Discard, fileNode)
+       }
 }
 
-func BenchmarkPrint(b *testing.B) {
-       if testfile == nil {
+func BenchmarkPrintDecl(b *testing.B) {
+       if declNode == nil {
                initialize()
        }
        b.ReportAllocs()
-       b.SetBytes(testsize)
+       b.SetBytes(declSize)
        for i := 0; i < b.N; i++ {
-               testprint(io.Discard, testfile)
+               testprint(io.Discard, declNode)
        }
 }
index 2cb11939411ccc756bd5aa28d0f7ad58daed18c0..7f96c226dc352335eaa466d85d849fec53dd6070 100644 (file)
@@ -13,6 +13,7 @@ import (
        "io"
        "os"
        "strings"
+       "sync"
        "text/tabwriter"
        "unicode"
 )
@@ -94,16 +95,6 @@ type printer struct {
        cachedLine int // line corresponding to cachedPos
 }
 
-func (p *printer) init(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) {
-       p.Config = *cfg
-       p.fset = fset
-       p.pos = token.Position{Line: 1, Column: 1}
-       p.out = token.Position{Line: 1, Column: 1}
-       p.wsbuf = make([]whiteSpace, 0, 16) // whitespace sequences are short
-       p.nodeSizes = nodeSizes
-       p.cachedPos = -1
-}
-
 func (p *printer) internalError(msg ...any) {
        if debug {
                fmt.Print(p.pos.String() + ": ")
@@ -1324,11 +1315,47 @@ type Config struct {
        Indent   int  // default: 0 (all code is indented at least by this much)
 }
 
+var printerPool = sync.Pool{
+       New: func() any {
+               return &printer{
+                       // Whitespace sequences are short.
+                       wsbuf: make([]whiteSpace, 0, 16),
+                       // We start the printer with a 16K output buffer, which is currently
+                       // larger than about 80% of Go files in the standard library.
+                       output: make([]byte, 0, 16<<10),
+               }
+       },
+}
+
+func newPrinter(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) *printer {
+       p := printerPool.Get().(*printer)
+       *p = printer{
+               Config:    *cfg,
+               fset:      fset,
+               pos:       token.Position{Line: 1, Column: 1},
+               out:       token.Position{Line: 1, Column: 1},
+               wsbuf:     p.wsbuf[:0],
+               nodeSizes: nodeSizes,
+               cachedPos: -1,
+               output:    p.output[:0],
+       }
+       return p
+}
+
+func (p *printer) free() {
+       // Hard limit on buffer size; see https://golang.org/issue/23199.
+       if cap(p.output) > 64<<10 {
+               return
+       }
+
+       printerPool.Put(p)
+}
+
 // fprint implements Fprint and takes a nodesSizes map for setting up the printer state.
 func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeSizes map[ast.Node]int) (err error) {
        // print node
-       var p printer
-       p.init(cfg, fset, nodeSizes)
+       p := newPrinter(cfg, fset, nodeSizes)
+       defer p.free()
        if err = p.printNode(node); err != nil {
                return
        }