Skip to content

Commit

Permalink
compiler: move OptimizeStringToBytes to transform package
Browse files Browse the repository at this point in the history
Unfortunately, while doing this I found that it doesn't actually apply
in any real-world programs (tested with `make smoketest`), apparently
because nil pointer checking messes with the functionattrs pass. I hope
to fix that after moving to LLVM 9, which has an optimization that makes
nil pointer checking easier to implement.
  • Loading branch information
aykevl authored and deadprogram committed Sep 22, 2019
1 parent cea0d9f commit 65bedda
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 102 deletions.
104 changes: 2 additions & 102 deletions compiler/optimizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro

// Run Go-specific optimization passes.
transform.OptimizeMaps(c.mod)
c.OptimizeStringToBytes()
transform.OptimizeStringToBytes(c.mod)
transform.OptimizeAllocs(c.mod)
c.LowerInterfaces()
c.LowerFuncValues()
Expand All @@ -62,7 +62,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro

// Run TinyGo-specific interprocedural optimizations.
transform.OptimizeAllocs(c.mod)
c.OptimizeStringToBytes()
transform.OptimizeStringToBytes(c.mod)

// Lower runtime.isnil calls to regular nil comparisons.
isnil := c.mod.NamedFunction("runtime.isnil")
Expand Down Expand Up @@ -158,103 +158,3 @@ func (c *Compiler) replacePanicsWithTrap() {
}
}
}

// 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()
}
}
}

// 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
}
57 changes: 57 additions & 0 deletions transform/stringtobytes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package transform

import (
"tinygo.org/x/go-llvm"
)

// OptimizeStringToBytes transforms 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 OptimizeStringToBytes(mod llvm.Module) {
stringToBytes := 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) {
if use.IsAExtractValueInst().IsNil() {
// Expected an extractvalue, but this is something else.
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 !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()
}
}
}
15 changes: 15 additions & 0 deletions transform/stringtobytes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package transform

import (
"testing"

"tinygo.org/x/go-llvm"
)

func TestOptimizeStringToBytes(t *testing.T) {
t.Parallel()
testTransform(t, "testdata/stringtobytes", func(mod llvm.Module) {
// Run optimization pass.
OptimizeStringToBytes(mod)
})
}
32 changes: 32 additions & 0 deletions transform/testdata/stringtobytes.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64--linux"

@str = constant [6 x i8] c"foobar"

declare { i8*, i64, i64 } @runtime.stringToBytes(i8*, i64)

declare void @printSlice(i8* nocapture readonly, i64, i64)

declare void @writeToSlice(i8* nocapture, i64, i64)

; Test that runtime.stringToBytes can be fully optimized away.
define void @testReadOnly() {
entry:
%0 = call fastcc { i8*, i64, i64 } @runtime.stringToBytes(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @str, i32 0, i32 0), i64 6)
%1 = extractvalue { i8*, i64, i64 } %0, 0
%2 = extractvalue { i8*, i64, i64 } %0, 1
%3 = extractvalue { i8*, i64, i64 } %0, 2
call fastcc void @printSlice(i8* %1, i64 %2, i64 %3)
ret void
}

; Test that even though the slice is written to, some values can be propagated.
define void @testReadWrite() {
entry:
%0 = call fastcc { i8*, i64, i64 } @runtime.stringToBytes(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @str, i32 0, i32 0), i64 6)
%1 = extractvalue { i8*, i64, i64 } %0, 0
%2 = extractvalue { i8*, i64, i64 } %0, 1
%3 = extractvalue { i8*, i64, i64 } %0, 2
call fastcc void @writeToSlice(i8* %1, i64 %2, i64 %3)
ret void
}
24 changes: 24 additions & 0 deletions transform/testdata/stringtobytes.out.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64--linux"

@str = constant [6 x i8] c"foobar"

declare { i8*, i64, i64 } @runtime.stringToBytes(i8*, i64)

declare void @printSlice(i8* nocapture readonly, i64, i64)

declare void @writeToSlice(i8* nocapture, i64, i64)

define void @testReadOnly() {
entry:
call fastcc void @printSlice(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @str, i32 0, i32 0), i64 6, i64 6)
ret void
}

define void @testReadWrite() {
entry:
%0 = call fastcc { i8*, i64, i64 } @runtime.stringToBytes(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @str, i32 0, i32 0), i64 6)
%1 = extractvalue { i8*, i64, i64 } %0, 0
call fastcc void @writeToSlice(i8* %1, i64 6, i64 6)
ret void
}
21 changes: 21 additions & 0 deletions transform/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,24 @@ func hasFlag(call, param llvm.Value, kind string) bool {
}
return true
}

// isReadOnly returns true if the given value (which must be of pointer type) is
// never stored to, and false if this cannot be proven.
func isReadOnly(value llvm.Value) bool {
uses := getUses(value)
for _, use := range uses {
if !use.IsAGetElementPtrInst().IsNil() {
if !isReadOnly(use) {
return false
}
} else if !use.IsACallInst().IsNil() {
if !hasFlag(use, value, "readonly") {
return false
}
} else {
// Unknown instruction, might not be readonly.
return false
}
}
return true
}

0 comments on commit 65bedda

Please sign in to comment.