compiler: move OptimizeStringToBytes to transform package
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.
Этот коммит содержится в:
родитель
cea0d9f864
коммит
65beddafe8
6 изменённых файлов: 151 добавлений и 102 удалений
|
@ -50,7 +50,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
|
||||||
|
|
||||||
// Run Go-specific optimization passes.
|
// Run Go-specific optimization passes.
|
||||||
transform.OptimizeMaps(c.mod)
|
transform.OptimizeMaps(c.mod)
|
||||||
c.OptimizeStringToBytes()
|
transform.OptimizeStringToBytes(c.mod)
|
||||||
transform.OptimizeAllocs(c.mod)
|
transform.OptimizeAllocs(c.mod)
|
||||||
c.LowerInterfaces()
|
c.LowerInterfaces()
|
||||||
c.LowerFuncValues()
|
c.LowerFuncValues()
|
||||||
|
@ -62,7 +62,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
|
||||||
|
|
||||||
// Run TinyGo-specific interprocedural optimizations.
|
// Run TinyGo-specific interprocedural optimizations.
|
||||||
transform.OptimizeAllocs(c.mod)
|
transform.OptimizeAllocs(c.mod)
|
||||||
c.OptimizeStringToBytes()
|
transform.OptimizeStringToBytes(c.mod)
|
||||||
|
|
||||||
// Lower runtime.isnil calls to regular nil comparisons.
|
// Lower runtime.isnil calls to regular nil comparisons.
|
||||||
isnil := c.mod.NamedFunction("runtime.isnil")
|
isnil := c.mod.NamedFunction("runtime.isnil")
|
||||||
|
@ -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
transform/stringtobytes.go
Обычный файл
57
transform/stringtobytes.go
Обычный файл
|
@ -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
transform/stringtobytes_test.go
Обычный файл
15
transform/stringtobytes_test.go
Обычный файл
|
@ -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
transform/testdata/stringtobytes.ll
предоставленный
Обычный файл
32
transform/testdata/stringtobytes.ll
предоставленный
Обычный файл
|
@ -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
transform/testdata/stringtobytes.out.ll
предоставленный
Обычный файл
24
transform/testdata/stringtobytes.out.ll
предоставленный
Обычный файл
|
@ -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
|
||||||
|
}
|
|
@ -32,3 +32,24 @@ func hasFlag(call, param llvm.Value, kind string) bool {
|
||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче