Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion transform/optimizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {

// Run TinyGo-specific optimization passes.
OptimizeStringToBytes(mod)
OptimizeStringFromBytes(mod)
maxStackSize := config.MaxStackAlloc()
OptimizeAllocs(mod, nil, maxStackSize, nil)
err = LowerInterfaces(mod, config)
Expand All @@ -90,6 +91,8 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
// The go coverage tool expects this header before any blocks.
fmt.Fprintln(os.Stderr, "mode: set")
}
OptimizeStringToBytes(mod)
OptimizeStringFromBytes(mod)
OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize,
func(pos token.Position, reason string) {
var line string
Expand All @@ -103,10 +106,11 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
}
},
)
OptimizeStringToBytes(mod)
OptimizeStringEqual(mod)

} else {
OptimizeStringFromBytes(mod)

// Must be run at any optimization level.
err := LowerInterfaces(mod, config)
if err != nil {
Expand Down
167 changes: 164 additions & 3 deletions transform/rtcalls.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package transform
// This file implements several small optimizations of runtime and reflect
// calls.

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

// OptimizeStringToBytes transforms runtime.stringToBytes(...) calls into const
// []byte slices whenever possible. This optimizes the following pattern:
Expand Down Expand Up @@ -71,6 +69,169 @@ func OptimizeStringToBytes(mod llvm.Module) {
}
}

// OptimizeStringFromBytes transforms temporary strings created from []byte
// slices into direct uses of the slice data when no instruction between the
// conversion and use can mutate the slice.
func OptimizeStringFromBytes(mod llvm.Module) {
stringFromBytes := mod.NamedFunction("runtime.stringFromBytes")
if stringFromBytes.IsNil() {
// nothing to optimize
return
}

// String comparisons only read their operands, so they are safe between a
// conversion and another supported use of that conversion.
safeCalls := map[llvm.Value]struct{}{}
for _, name := range []string{"runtime.stringEqual", "runtime.stringLess"} {
compare := mod.NamedFunction(name)
if compare.IsNil() {
continue
}
for _, call := range getUses(compare) {
safeCalls[call] = struct{}{}
}
}

// Rewrite each supported use independently. Length extracts are delayed so
// comparison operands can still be matched as pointer/length pairs.
for _, call := range getUses(stringFromBytes) {
var lengthExtracts []llvm.Value
for _, extract := range getUses(call) {
if extract.IsAExtractValueInst().IsNil() {
continue
}
indices := extract.Indices()
if len(indices) != 1 {
continue
}
switch indices[0] {
case 0:
for _, use := range getUses(extract) {
if _, ok := safeCalls[use]; !ok {
continue
}
if !isSafeStringFromBytesUse(call, stringFromBytes, use, safeCalls) {
continue
}
replaceStringFromBytesCompareUse(use, extract, call, stringFromBytes)
}
case 1:
lengthExtracts = append(lengthExtracts, extract)
}
}

for _, extract := range lengthExtracts {
extract.ReplaceAllUsesWith(call.Operand(1))
extract.EraseFromParentAsInstruction()
}
removeDeadStringFromBytes(call)
}
}

func replaceStringFromBytesCompareUse(compare, ptrExtract, call, stringFromBytes llvm.Value) {
for _, pair := range [][2]int{{0, 1}, {2, 3}} {
if compare.Operand(pair[0]) != ptrExtract {
continue
}
lenExtract, ok := getStringFromBytesExtract(compare.Operand(pair[1]), stringFromBytes, 1)
if !ok || lenExtract.Operand(0) != call {
continue
}
compare.SetOperand(pair[0], call.Operand(0))
compare.SetOperand(pair[1], call.Operand(1))
}
}

func getStringFromBytesExtract(value, stringFromBytes llvm.Value, index uint64) (llvm.Value, bool) {
if value.IsAExtractValueInst().IsNil() {
return llvm.Value{}, false
}
indices := value.Indices()
if len(indices) != 1 || indices[0] != uint32(index) {
return llvm.Value{}, false
}
call := value.Operand(0)
if call.IsACallInst().IsNil() {
return llvm.Value{}, false
}
called := call.CalledValue()
if called.IsNil() || called != stringFromBytes {
return llvm.Value{}, false
}
return value, true
}

func isStringFromBytesCall(value, stringFromBytes llvm.Value) bool {
if value.IsACallInst().IsNil() {
return false
}
called := value.CalledValue()
return !called.IsNil() && called == stringFromBytes
}

// isSafeStringFromBytesUse reports whether replacing the copied string with the
// source slice preserves the bytes observed by use.
func isSafeStringFromBytesUse(call, stringFromBytes, use llvm.Value, allowedCalls map[llvm.Value]struct{}) bool {
if call.InstructionParent() != use.InstructionParent() {
return false
}
for inst := llvm.NextInstruction(call); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
if inst == use {
return true
}
if !isSafeStringFromBytesInterveningInstruction(inst, stringFromBytes, allowedCalls) {
return false
}
}
return false
}

func isSafeStringFromBytesInterveningInstruction(inst, stringFromBytes llvm.Value, allowedCalls map[llvm.Value]struct{}) bool {
if _, ok := allowedCalls[inst]; ok {
return true
}
switch {
case !inst.IsAExtractValueInst().IsNil():
return true
case isTrackPointerCall(inst):
return true
case isStringFromBytesCall(inst, stringFromBytes):
return true
default:
return false
}
}

func removeDeadStringFromBytes(call llvm.Value) {
for _, use := range getUses(call) {
if use.IsAExtractValueInst().IsNil() {
return
}
for _, extractUse := range getUses(use) {
if !isTrackPointerCall(extractUse) {
return
}
}
}
for _, use := range getUses(call) {
for _, extractUse := range getUses(use) {
extractUse.EraseFromParentAsInstruction()
}
use.EraseFromParentAsInstruction()
}
if !hasUses(call) {
call.EraseFromParentAsInstruction()
}
}

func isTrackPointerCall(value llvm.Value) bool {
if value.IsACallInst().IsNil() {
return false
}
called := value.CalledValue()
return !called.IsNil() && called.Name() == "runtime.trackPointer"
}

// OptimizeStringEqual transforms runtime.stringEqual(...) calls into simple
// integer comparisons if at least one of the sides of the comparison is zero.
// Ths converts str == "" into len(str) == 0 and "" == "" into false.
Expand Down
21 changes: 21 additions & 0 deletions transform/rtcalls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,24 @@ func TestOptimizeStringEqual(t *testing.T) {
transform.OptimizeStringEqual(mod)
})
}

func TestOptimizeStringFromBytesStringEqual(t *testing.T) {
t.Parallel()
testTransform(t, "testdata/stringfrombytes-stringequal", func(mod llvm.Module) {
transform.OptimizeStringFromBytes(mod)
})
}

func TestOptimizeStringFromBytesStringLess(t *testing.T) {
t.Parallel()
testTransform(t, "testdata/stringfrombytes-stringless", func(mod llvm.Module) {
transform.OptimizeStringFromBytes(mod)
})
}

func TestOptimizeStringFromBytesLen(t *testing.T) {
t.Parallel()
testTransform(t, "testdata/stringfrombytes-len", func(mod llvm.Module) {
transform.OptimizeStringFromBytes(mod)
})
}
31 changes: 31 additions & 0 deletions transform/testdata/stringfrombytes-len.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64"
target triple = "armv7m-none-eabi"

%runtime._string = type { ptr, i32 }

declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr)

declare void @runtime.trackPointer(ptr, ptr, ptr)

declare void @useString(ptr, i32)

define i32 @main.stringFromBytesLen(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) {
entry:
%stackalloc = alloca i8, align 1
%0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 %a.len, i32 %a.cap, ptr undef)
%1 = extractvalue %runtime._string %0, 0
call void @runtime.trackPointer(ptr %1, ptr %stackalloc, ptr undef)
%2 = extractvalue %runtime._string %0, 1
%len = extractvalue %runtime._string %0, 1
ret i32 %len
}

define i32 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) {
entry:
%0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 %a.len, i32 %a.cap, ptr undef)
%1 = extractvalue %runtime._string %0, 0
%2 = extractvalue %runtime._string %0, 1
call void @useString(ptr %1, i32 %2)
%len = extractvalue %runtime._string %0, 1
ret i32 %len
}
24 changes: 24 additions & 0 deletions transform/testdata/stringfrombytes-len.out.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64"
target triple = "armv7m-none-eabi"

%runtime._string = type { ptr, i32 }

declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr)

declare void @runtime.trackPointer(ptr, ptr, ptr)

declare void @useString(ptr, i32)

define i32 @main.stringFromBytesLen(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) {
entry:
%stackalloc = alloca i8, align 1
ret i32 %a.len
}

define i32 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) {
entry:
%0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 %a.len, i32 %a.cap, ptr undef)
%1 = extractvalue %runtime._string %0, 0
call void @useString(ptr %1, i32 %a.len)
ret i32 %a.len
}
Loading
Loading