From ec73bd6a26d3ef667ace6c9f7dfac97638074866 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 6 Oct 2018 21:26:34 +0200 Subject: [PATCH] compiler: optimize runtime.stringToBytes calls This optimization makes sure the following pattern doesn't do a heap allocation (assuming Write doesn't modify the slice): var w *machine.UART = ... w.Write([]byte("foo")) As long as Write doesn't modify the slice and LLVM can detect this, a call to runtime.stringToBytes with the necessary allocation + copy is avoided. --- compiler/optimizer.go | 122 +++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/compiler/optimizer.go b/compiler/optimizer.go index 27c107cd..6716a222 100644 --- a/compiler/optimizer.go +++ b/compiler/optimizer.go @@ -37,6 +37,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) { goPasses.Run(c.mod) // Run Go-specific optimization passes. + c.OptimizeStringToBytes() c.OptimizeAllocs() c.Verify() } @@ -48,6 +49,56 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) { modPasses.Run(c.mod) } +// Transform runtime.stringToBytes(...) calls into const []byte slices whenever +// possible. This optimizes the following pattern: +// w.Write([]byte("foo")) +// where Write does not store to the slice. +func (c *Compiler) OptimizeStringToBytes() { + stringToBytes := c.mod.NamedFunction("runtime.stringToBytes") + if stringToBytes.IsNil() { + // nothing to optimize + return + } + + for _, call := range getUses(stringToBytes) { + strptr := call.Operand(0) + strlen := call.Operand(1) + + // strptr is always constant because strings are always constant. + + convertedAllUses := true + for _, use := range getUses(call) { + nilValue := llvm.Value{} + if use.IsAExtractValueInst() == nilValue { + convertedAllUses = false + continue + } + switch use.Type().TypeKind() { + case llvm.IntegerTypeKind: + // A length (len or cap). Propagate the length value. + use.ReplaceAllUsesWith(strlen) + use.EraseFromParentAsInstruction() + case llvm.PointerTypeKind: + // The string pointer itself. + if !c.isReadOnly(use) { + convertedAllUses = false + continue + } + use.ReplaceAllUsesWith(strptr) + use.EraseFromParentAsInstruction() + default: + // should not happen + panic("unknown return type of runtime.stringToBytes: " + use.Type().String()) + } + } + if convertedAllUses { + // Call to runtime.stringToBytes can be eliminated: both the input + // and the output is constant. + call.EraseFromParentAsInstruction() + } + } +} + // Basic escape analysis: translate runtime.alloc calls into alloca // instructions. func (c *Compiler) OptimizeAllocs() { @@ -122,25 +173,8 @@ func (c *Compiler) doesEscape(value llvm.Value) bool { // Call only escapes when the (pointer) parameter is not marked // "nocapture". This flag means that the parameter does not escape // the give function. - fn := use.CalledValue() - if fn.IsAFunction() == nilValue { - // This is not a function but something else, like a function - // pointer. - return false - } - nocaptureKind := llvm.AttributeKindID("nocapture") - for i := 0; i < fn.ParamsCount(); i++ { - if use.Operand(i) != value { - // This is not the parameter we're checking. - continue - } - index := i + 1 // param attributes start at 1 - nocapture := fn.GetEnumAttributeAtIndex(index, nocaptureKind) - nilAttribute := llvm.Attribute{} - if nocapture == nilAttribute { - // Parameter doesn't have the nocapture flag, so may escape. - return true - } + if !c.hasFlag(use, value, "nocapture") { + return true } } else { // Unknown instruction, might escape. @@ -152,6 +186,56 @@ func (c *Compiler) doesEscape(value llvm.Value) bool { return false } +// Check whether the given value (which is of pointer type) is never stored to. +func (c *Compiler) isReadOnly(value llvm.Value) bool { + uses := getUses(value) + for _, use := range uses { + nilValue := llvm.Value{} + if use.IsAGetElementPtrInst() != nilValue { + if !c.isReadOnly(use) { + return false + } + } else if use.IsACallInst() != nilValue { + if !c.hasFlag(use, value, "readonly") { + return false + } + } else { + // Unknown instruction, might not be readonly. + return false + } + } + return true +} + +// Check whether all uses of this param as parameter to the call have the given +// flag. In most cases, there will only be one use but a function could take the +// same parameter twice, in which case both must have the flag. +// A flag can be any enum flag, like "readonly". +func (c *Compiler) hasFlag(call, param llvm.Value, kind string) bool { + fn := call.CalledValue() + nilValue := llvm.Value{} + if fn.IsAFunction() == nilValue { + // This is not a function but something else, like a function pointer. + return false + } + kindID := llvm.AttributeKindID(kind) + for i := 0; i < fn.ParamsCount(); i++ { + if call.Operand(i) != param { + // This is not the parameter we're checking. + continue + } + index := i + 1 // param attributes start at 1 + attr := fn.GetEnumAttributeAtIndex(index, kindID) + nilAttribute := llvm.Attribute{} + if attr == nilAttribute { + // At least one parameter doesn't have the flag (there may be + // multiple). + return false + } + } + return true +} + // Return a list of values (actually, instructions) where this value is used as // an operand. func getUses(value llvm.Value) []llvm.Value {