From 7901bf5a47e69c2d8c37a701db8faf62fb8cd8bf Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:27:38 -0700 Subject: [PATCH 1/8] transform: add byte slice string equality test Add baseline coverage for string equality where both operands come from []byte-to-string conversions. The expected output records the current behavior, including the temporary runtime.stringFromBytes calls. --- transform/rtcalls_test.go | 7 +++ .../testdata/stringfrombytes-stringequal.ll | 49 +++++++++++++++++++ .../stringfrombytes-stringequal.out.ll | 49 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 transform/testdata/stringfrombytes-stringequal.ll create mode 100644 transform/testdata/stringfrombytes-stringequal.out.ll diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 2ae1603155..42a2e2038d 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -22,3 +22,10 @@ func TestOptimizeStringEqual(t *testing.T) { transform.OptimizeStringEqual(mod) }) } + +func TestOptimizeStringFromBytesStringEqual(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-stringequal", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversions away. + }) +} diff --git a/transform/testdata/stringfrombytes-stringequal.ll b/transform/testdata/stringfrombytes-stringequal.ll new file mode 100644 index 0000000000..3f3c4a17ae --- /dev/null +++ b/transform/testdata/stringfrombytes-stringequal.ll @@ -0,0 +1,49 @@ +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 i1 @runtime.stringEqual(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @useString(ptr, i32) + +define i1 @main.bytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} + +define i1 @main.stringAndBytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %3 +} + +define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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) + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} diff --git a/transform/testdata/stringfrombytes-stringequal.out.ll b/transform/testdata/stringfrombytes-stringequal.out.ll new file mode 100644 index 0000000000..3f3c4a17ae --- /dev/null +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -0,0 +1,49 @@ +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 i1 @runtime.stringEqual(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @useString(ptr, i32) + +define i1 @main.bytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} + +define i1 @main.stringAndBytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %3 +} + +define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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) + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} From bd7d9897048e9399623fc0ac78900dcba6880425 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:28:17 -0700 Subject: [PATCH 2/8] transform: avoid byte slice string equality allocations Rewrite string equality comparisons where either operand comes from a temporary []byte-to-string conversion to compare the original slice data directly. This avoids the temporary string allocations produced by bytes.Equal and by one-sided comparisons such as string(b) == s. The rewrite is limited to conversions that feed the comparison without intervening side-effecting instructions, preserving cases where the converted string must remain a copy. Fixes issue 4045 --- transform/optimizer.go | 6 +- transform/rtcalls.go | 170 ++++++++++++++++++ transform/rtcalls_test.go | 2 +- .../stringfrombytes-stringequal.out.ll | 26 +-- 4 files changed, 182 insertions(+), 22 deletions(-) diff --git a/transform/optimizer.go b/transform/optimizer.go index 150a9a77cb..cf6dd60969 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -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) @@ -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 @@ -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 { diff --git a/transform/rtcalls.go b/transform/rtcalls.go index 49b138bcc9..bf9831fd87 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -71,6 +71,176 @@ func OptimizeStringToBytes(mod llvm.Module) { } } +// OptimizeStringFromBytesStringEqual transforms string equality comparisons with +// temporary strings from []byte slices into direct comparisons of the slice +// data. This optimizes the following patterns: +// +// string(a) == string(b) +// string(a) == s +func OptimizeStringFromBytesStringEqual(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + stringEqual := mod.NamedFunction("runtime.stringEqual") + if stringEqual.IsNil() { + // nothing to optimize + return + } + + for _, call := range getUses(stringEqual) { + str1, ok1 := getTemporaryStringFromBytes(call.Operand(0), call.Operand(1), stringFromBytes, call) + str2, ok2 := getTemporaryStringFromBytes(call.Operand(2), call.Operand(3), stringFromBytes, call) + if !ok1 && !ok2 { + continue + } + + if ok1 { + call.SetOperand(0, str1.ptr) + call.SetOperand(1, str1.len) + } + if ok2 { + call.SetOperand(2, str2.ptr) + call.SetOperand(3, str2.len) + } + + if ok1 { + removeDeadStringFromBytes(str1) + } + if ok2 && (!ok1 || str1.call != str2.call) { + removeDeadStringFromBytes(str2) + } + } +} + +// OptimizeStringFromBytes transforms temporary strings created from []byte +// slices into direct uses of the slice data where the string cannot escape. +func OptimizeStringFromBytes(mod llvm.Module) { + OptimizeStringFromBytesStringEqual(mod) +} + +type stringFromBytesValue struct { + call llvm.Value + ptr llvm.Value + len llvm.Value +} + +func getTemporaryStringFromBytes(strptr, strlen, stringFromBytes, stringEqual llvm.Value) (stringFromBytesValue, bool) { + ptrExtract, ok := getStringFromBytesExtract(strptr, stringFromBytes, 0) + if !ok { + return stringFromBytesValue{}, false + } + lenExtract, ok := getStringFromBytesExtract(strlen, stringFromBytes, 1) + if !ok || ptrExtract.Operand(0) != lenExtract.Operand(0) { + return stringFromBytesValue{}, false + } + + call := ptrExtract.Operand(0) + if !isTemporaryStringFromBytes(call, stringFromBytes, stringEqual) { + return stringFromBytesValue{}, false + } + + return stringFromBytesValue{ + call: call, + ptr: call.Operand(0), + len: call.Operand(1), + }, true +} + +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 isTemporaryStringFromBytes(call, stringFromBytes, stringEqual llvm.Value) bool { + for _, use := range getUses(call) { + if use.IsAExtractValueInst().IsNil() { + return false + } + for _, extractUse := range getUses(use) { + if extractUse != stringEqual && !isTrackPointerCall(extractUse) { + return false + } + } + } + return onlySafeInstructionsBetween(call, stringFromBytes, stringEqual) +} + +func onlySafeInstructionsBetween(call, stringFromBytes, stringEqual llvm.Value) bool { + if call.InstructionParent() != stringEqual.InstructionParent() { + return false + } + for inst := llvm.NextInstruction(call); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if inst == stringEqual { + return true + } + switch { + case !inst.IsAExtractValueInst().IsNil(): + // Extracting pointer and length fields is safe. + case isTrackPointerCall(inst): + // GC tracking for the temporary string pointer is safe. + case isStringFromBytesCall(inst, stringFromBytes): + // Another temporary conversion for the same comparison is safe. + default: + return false + } + } + return false +} + +func isStringFromBytesCall(value, stringFromBytes llvm.Value) bool { + if value.IsACallInst().IsNil() { + return false + } + called := value.CalledValue() + return !called.IsNil() && called == stringFromBytes +} + +func removeDeadStringFromBytes(str stringFromBytesValue) { + for _, use := range getUses(str.call) { + if use.IsAExtractValueInst().IsNil() { + return + } + for _, extractUse := range getUses(use) { + if !isTrackPointerCall(extractUse) { + return + } + } + } + for _, use := range getUses(str.call) { + for _, extractUse := range getUses(use) { + extractUse.EraseFromParentAsInstruction() + } + use.EraseFromParentAsInstruction() + } + if !hasUses(str.call) { + str.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. diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 42a2e2038d..c34df3e429 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -26,6 +26,6 @@ func TestOptimizeStringEqual(t *testing.T) { func TestOptimizeStringFromBytesStringEqual(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-stringequal", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversions away. + transform.OptimizeStringFromBytes(mod) }) } diff --git a/transform/testdata/stringfrombytes-stringequal.out.ll b/transform/testdata/stringfrombytes-stringequal.out.ll index 3f3c4a17ae..f2d3c9ac5b 100644 --- a/transform/testdata/stringfrombytes-stringequal.out.ll +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -14,25 +14,14 @@ declare void @useString(ptr, i32) define i1 @main.bytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 - %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) - %4 = extractvalue %runtime._string %3, 0 - call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) - %5 = extractvalue %runtime._string %3, 1 - %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) - ret i1 %6 + %0 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %0 } define i1 @main.stringAndBytesEqual(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 - %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) - ret i1 %3 + %0 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %0 } define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.cap, ptr %context) { @@ -41,9 +30,6 @@ entry: %1 = extractvalue %runtime._string %0, 0 %2 = extractvalue %runtime._string %0, 1 call void @useString(ptr %1, i32 %2) - %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) - %4 = extractvalue %runtime._string %3, 0 - %5 = extractvalue %runtime._string %3, 1 - %6 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) - ret i1 %6 + %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %3 } From dcf3dac0df90e238c98986efb7da3c06fad98181 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:54:42 -0700 Subject: [PATCH 3/8] transform: add byte slice string ordering test Add baseline coverage for string ordering where operands come from []byte-to-string conversions. The expected output records the current behavior, including the temporary runtime.stringFromBytes calls. --- transform/rtcalls_test.go | 7 +++ .../testdata/stringfrombytes-stringless.ll | 58 +++++++++++++++++++ .../stringfrombytes-stringless.out.ll | 58 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 transform/testdata/stringfrombytes-stringless.ll create mode 100644 transform/testdata/stringfrombytes-stringless.out.ll diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index c34df3e429..b340dd06b9 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -29,3 +29,10 @@ func TestOptimizeStringFromBytesStringEqual(t *testing.T) { transform.OptimizeStringFromBytes(mod) }) } + +func TestOptimizeStringFromBytesStringLess(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-stringless", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversions away. + }) +} diff --git a/transform/testdata/stringfrombytes-stringless.ll b/transform/testdata/stringfrombytes-stringless.ll new file mode 100644 index 0000000000..d771a4448e --- /dev/null +++ b/transform/testdata/stringfrombytes-stringless.ll @@ -0,0 +1,58 @@ +target datalayout = "e-m:e-p:32:32-i64:64-v128:64-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 i1 @runtime.stringLess(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @useString(ptr, i32) + +define i1 @main.bytesLess(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} + +define i1 @main.bytesLessString(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %3 +} + +define i1 @main.stringLessBytes(ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringLess(ptr %s.data, i32 %s.len, ptr %1, i32 %2, ptr undef) + ret i1 %3 +} + +define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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) + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} diff --git a/transform/testdata/stringfrombytes-stringless.out.ll b/transform/testdata/stringfrombytes-stringless.out.ll new file mode 100644 index 0000000000..d771a4448e --- /dev/null +++ b/transform/testdata/stringfrombytes-stringless.out.ll @@ -0,0 +1,58 @@ +target datalayout = "e-m:e-p:32:32-i64:64-v128:64-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 i1 @runtime.stringLess(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @useString(ptr, i32) + +define i1 @main.bytesLess(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} + +define i1 @main.bytesLessString(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %3 +} + +define i1 @main.stringLessBytes(ptr %s.data, i32 %s.len, 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 + %3 = call i1 @runtime.stringLess(ptr %s.data, i32 %s.len, ptr %1, i32 %2, ptr undef) + ret i1 %3 +} + +define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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) + %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) + %4 = extractvalue %runtime._string %3, 0 + %5 = extractvalue %runtime._string %3, 1 + %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) + ret i1 %6 +} From cb0fdfb5092c8a877421e8ce6c63f7689ee6740b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:55:24 -0700 Subject: [PATCH 4/8] transform: avoid byte slice string ordering allocations Reuse the temporary []byte-to-string comparison rewrite for runtime.stringLess. This avoids allocations in ordering comparisons such as string(a) < string(b), string(a) < s, and s < string(a). The rewrite keeps the same safety checks as equality comparisons, so conversions that must remain copies are preserved. --- transform/rtcalls.go | 46 +++++++++++++++---- transform/rtcalls_test.go | 2 +- .../stringfrombytes-stringless.out.ll | 33 ++++--------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index bf9831fd87..01cb3e3346 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -89,7 +89,32 @@ func OptimizeStringFromBytesStringEqual(mod llvm.Module) { return } - for _, call := range getUses(stringEqual) { + optimizeStringFromBytesCompareUses(stringFromBytes, stringEqual) +} + +// OptimizeStringFromBytesStringLess transforms string ordering comparisons with +// temporary strings from []byte slices into direct comparisons of the slice +// data. This optimizes the following patterns: +// +// string(a) < string(b) +// string(a) < s +func OptimizeStringFromBytesStringLess(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + stringLess := mod.NamedFunction("runtime.stringLess") + if stringLess.IsNil() { + // nothing to optimize + return + } + + optimizeStringFromBytesCompareUses(stringFromBytes, stringLess) +} + +func optimizeStringFromBytesCompareUses(stringFromBytes, compare llvm.Value) { + for _, call := range getUses(compare) { str1, ok1 := getTemporaryStringFromBytes(call.Operand(0), call.Operand(1), stringFromBytes, call) str2, ok2 := getTemporaryStringFromBytes(call.Operand(2), call.Operand(3), stringFromBytes, call) if !ok1 && !ok2 { @@ -114,10 +139,13 @@ func OptimizeStringFromBytesStringEqual(mod llvm.Module) { } } +// OptimizeStringFromBytes transforms temporary strings created from []byte +// slices into direct uses of the slice data where the string cannot escape. // OptimizeStringFromBytes transforms temporary strings created from []byte // slices into direct uses of the slice data where the string cannot escape. func OptimizeStringFromBytes(mod llvm.Module) { OptimizeStringFromBytesStringEqual(mod) + OptimizeStringFromBytesStringLess(mod) } type stringFromBytesValue struct { @@ -126,7 +154,7 @@ type stringFromBytesValue struct { len llvm.Value } -func getTemporaryStringFromBytes(strptr, strlen, stringFromBytes, stringEqual llvm.Value) (stringFromBytesValue, bool) { +func getTemporaryStringFromBytes(strptr, strlen, stringFromBytes, compare llvm.Value) (stringFromBytesValue, bool) { ptrExtract, ok := getStringFromBytesExtract(strptr, stringFromBytes, 0) if !ok { return stringFromBytesValue{}, false @@ -137,7 +165,7 @@ func getTemporaryStringFromBytes(strptr, strlen, stringFromBytes, stringEqual ll } call := ptrExtract.Operand(0) - if !isTemporaryStringFromBytes(call, stringFromBytes, stringEqual) { + if !isTemporaryStringFromBytes(call, stringFromBytes, compare) { return stringFromBytesValue{}, false } @@ -167,26 +195,26 @@ func getStringFromBytesExtract(value, stringFromBytes llvm.Value, index uint64) return value, true } -func isTemporaryStringFromBytes(call, stringFromBytes, stringEqual llvm.Value) bool { +func isTemporaryStringFromBytes(call, stringFromBytes, compare llvm.Value) bool { for _, use := range getUses(call) { if use.IsAExtractValueInst().IsNil() { return false } for _, extractUse := range getUses(use) { - if extractUse != stringEqual && !isTrackPointerCall(extractUse) { + if extractUse != compare && !isTrackPointerCall(extractUse) { return false } } } - return onlySafeInstructionsBetween(call, stringFromBytes, stringEqual) + return onlySafeInstructionsBetween(call, stringFromBytes, compare) } -func onlySafeInstructionsBetween(call, stringFromBytes, stringEqual llvm.Value) bool { - if call.InstructionParent() != stringEqual.InstructionParent() { +func onlySafeInstructionsBetween(call, stringFromBytes, compare llvm.Value) bool { + if call.InstructionParent() != compare.InstructionParent() { return false } for inst := llvm.NextInstruction(call); !inst.IsNil(); inst = llvm.NextInstruction(inst) { - if inst == stringEqual { + if inst == compare { return true } switch { diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index b340dd06b9..56ba3ca5ae 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -33,6 +33,6 @@ func TestOptimizeStringFromBytesStringEqual(t *testing.T) { func TestOptimizeStringFromBytesStringLess(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-stringless", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversions away. + transform.OptimizeStringFromBytes(mod) }) } diff --git a/transform/testdata/stringfrombytes-stringless.out.ll b/transform/testdata/stringfrombytes-stringless.out.ll index d771a4448e..c1f8e28436 100644 --- a/transform/testdata/stringfrombytes-stringless.out.ll +++ b/transform/testdata/stringfrombytes-stringless.out.ll @@ -14,34 +14,20 @@ declare void @useString(ptr, i32) define i1 @main.bytesLess(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.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 - %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) - %4 = extractvalue %runtime._string %3, 0 - call void @runtime.trackPointer(ptr %4, ptr %stackalloc, ptr undef) - %5 = extractvalue %runtime._string %3, 1 - %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) - ret i1 %6 + %0 = call i1 @runtime.stringLess(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %0 } define i1 @main.bytesLessString(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, 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 - %3 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) - ret i1 %3 + %0 = call i1 @runtime.stringLess(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %0 } define i1 @main.stringLessBytes(ptr %s.data, i32 %s.len, 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 - %3 = call i1 @runtime.stringLess(ptr %s.data, i32 %s.len, ptr %1, i32 %2, ptr undef) - ret i1 %3 + %0 = call i1 @runtime.stringLess(ptr %s.data, i32 %s.len, ptr %a.data, i32 %a.len, ptr undef) + ret i1 %0 } define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b.data, i32 %b.len, i32 %b.cap, ptr %context) { @@ -50,9 +36,6 @@ entry: %1 = extractvalue %runtime._string %0, 0 %2 = extractvalue %runtime._string %0, 1 call void @useString(ptr %1, i32 %2) - %3 = call %runtime._string @runtime.stringFromBytes(ptr %b.data, i32 %b.len, i32 %b.cap, ptr undef) - %4 = extractvalue %runtime._string %3, 0 - %5 = extractvalue %runtime._string %3, 1 - %6 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %4, i32 %5, ptr undef) - ret i1 %6 + %3 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %3 } From a70e3f2c8ee361d39d5a09c9b2aa68c64eb7a98f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:55:48 -0700 Subject: [PATCH 5/8] transform: add byte slice string length test Add baseline coverage for len(string(b)) where b is a byte slice. The expected output records the current behavior, including the temporary runtime.stringFromBytes call. --- transform/rtcalls_test.go | 7 +++++ transform/testdata/stringfrombytes-len.ll | 31 +++++++++++++++++++ transform/testdata/stringfrombytes-len.out.ll | 31 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 transform/testdata/stringfrombytes-len.ll create mode 100644 transform/testdata/stringfrombytes-len.out.ll diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 56ba3ca5ae..3efac1dbaf 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -36,3 +36,10 @@ func TestOptimizeStringFromBytesStringLess(t *testing.T) { transform.OptimizeStringFromBytes(mod) }) } + +func TestOptimizeStringFromBytesLen(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-len", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversion away. + }) +} diff --git a/transform/testdata/stringfrombytes-len.ll b/transform/testdata/stringfrombytes-len.ll new file mode 100644 index 0000000000..8a7c0ec378 --- /dev/null +++ b/transform/testdata/stringfrombytes-len.ll @@ -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 +} diff --git a/transform/testdata/stringfrombytes-len.out.ll b/transform/testdata/stringfrombytes-len.out.ll new file mode 100644 index 0000000000..8a7c0ec378 --- /dev/null +++ b/transform/testdata/stringfrombytes-len.out.ll @@ -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 +} From 2d2a743c6163331946cd5f2c8e82651a3000e1d2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:56:19 -0700 Subject: [PATCH 6/8] transform: avoid byte slice string length allocations Rewrite len(string(b)) where b is a byte slice to use the original slice length. This removes the temporary runtime.stringFromBytes allocation when the string is only needed for its length, and still reuses the slice length when the string conversion has to stay for other uses. --- transform/rtcalls.go | 28 +++++++++++++++++++ transform/rtcalls_test.go | 2 +- transform/testdata/stringfrombytes-len.out.ll | 13 ++------- .../stringfrombytes-stringequal.out.ll | 7 ++--- .../stringfrombytes-stringless.out.ll | 7 ++--- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index 01cb3e3346..ae540f229a 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -113,6 +113,31 @@ func OptimizeStringFromBytesStringLess(mod llvm.Module) { optimizeStringFromBytesCompareUses(stringFromBytes, stringLess) } +// OptimizeStringFromBytesLen transforms lengths of strings freshly created +// from []byte slices into direct uses of the slice length. +func OptimizeStringFromBytesLen(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + + for _, call := range getUses(stringFromBytes) { + for _, use := range getUses(call) { + if use.IsAExtractValueInst().IsNil() { + continue + } + indices := use.Indices() + if len(indices) != 1 || indices[0] != 1 { + continue + } + use.ReplaceAllUsesWith(call.Operand(1)) + use.EraseFromParentAsInstruction() + } + removeDeadStringFromBytes(stringFromBytesValue{call: call}) + } +} + func optimizeStringFromBytesCompareUses(stringFromBytes, compare llvm.Value) { for _, call := range getUses(compare) { str1, ok1 := getTemporaryStringFromBytes(call.Operand(0), call.Operand(1), stringFromBytes, call) @@ -143,9 +168,12 @@ func optimizeStringFromBytesCompareUses(stringFromBytes, compare llvm.Value) { // slices into direct uses of the slice data where the string cannot escape. // OptimizeStringFromBytes transforms temporary strings created from []byte // slices into direct uses of the slice data where the string cannot escape. +// OptimizeStringFromBytes transforms temporary strings created from []byte +// slices into direct uses of the slice data where the string cannot escape. func OptimizeStringFromBytes(mod llvm.Module) { OptimizeStringFromBytesStringEqual(mod) OptimizeStringFromBytesStringLess(mod) + OptimizeStringFromBytesLen(mod) } type stringFromBytesValue struct { diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 3efac1dbaf..5d2b6f5f68 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -40,6 +40,6 @@ func TestOptimizeStringFromBytesStringLess(t *testing.T) { func TestOptimizeStringFromBytesLen(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-len", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversion away. + transform.OptimizeStringFromBytes(mod) }) } diff --git a/transform/testdata/stringfrombytes-len.out.ll b/transform/testdata/stringfrombytes-len.out.ll index 8a7c0ec378..e144720eaf 100644 --- a/transform/testdata/stringfrombytes-len.out.ll +++ b/transform/testdata/stringfrombytes-len.out.ll @@ -12,20 +12,13 @@ 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 + 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 - %2 = extractvalue %runtime._string %0, 1 - call void @useString(ptr %1, i32 %2) - %len = extractvalue %runtime._string %0, 1 - ret i32 %len + call void @useString(ptr %1, i32 %a.len) + ret i32 %a.len } diff --git a/transform/testdata/stringfrombytes-stringequal.out.ll b/transform/testdata/stringfrombytes-stringequal.out.ll index f2d3c9ac5b..0ec8277c2e 100644 --- a/transform/testdata/stringfrombytes-stringequal.out.ll +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -28,8 +28,7 @@ define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b 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) - %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %b.data, i32 %b.len, ptr undef) - ret i1 %3 + call void @useString(ptr %1, i32 %a.len) + %2 = call i1 @runtime.stringEqual(ptr %1, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %2 } diff --git a/transform/testdata/stringfrombytes-stringless.out.ll b/transform/testdata/stringfrombytes-stringless.out.ll index c1f8e28436..4e3e0f7832 100644 --- a/transform/testdata/stringfrombytes-stringless.out.ll +++ b/transform/testdata/stringfrombytes-stringless.out.ll @@ -34,8 +34,7 @@ define i1 @main.keepStringConversion(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %b 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) - %3 = call i1 @runtime.stringLess(ptr %1, i32 %2, ptr %b.data, i32 %b.len, ptr undef) - ret i1 %3 + call void @useString(ptr %1, i32 %a.len) + %2 = call i1 @runtime.stringLess(ptr %1, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) + ret i1 %2 } From 58a357c5cebabeee8f338c3248982360f0859e47 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:09:53 -0700 Subject: [PATCH 7/8] transform: add more byte slice string tests Add baseline coverage for temporary string conversions in map lookup and delete, string switch comparisons, and string concatenation. The expected output records the current runtime.stringFromBytes calls before adding the optimizations. --- transform/rtcalls_test.go | 21 ++++++ transform/testdata/stringfrombytes-mapkey.ll | 75 +++++++++++++++++++ .../testdata/stringfrombytes-mapkey.out.ll | 75 +++++++++++++++++++ .../testdata/stringfrombytes-stringconcat.ll | 47 ++++++++++++ .../stringfrombytes-stringconcat.out.ll | 47 ++++++++++++ .../testdata/stringfrombytes-stringswitch.ll | 59 +++++++++++++++ .../stringfrombytes-stringswitch.out.ll | 59 +++++++++++++++ 7 files changed, 383 insertions(+) create mode 100644 transform/testdata/stringfrombytes-mapkey.ll create mode 100644 transform/testdata/stringfrombytes-mapkey.out.ll create mode 100644 transform/testdata/stringfrombytes-stringconcat.ll create mode 100644 transform/testdata/stringfrombytes-stringconcat.out.ll create mode 100644 transform/testdata/stringfrombytes-stringswitch.ll create mode 100644 transform/testdata/stringfrombytes-stringswitch.out.ll diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 5d2b6f5f68..6b70b2f11b 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -43,3 +43,24 @@ func TestOptimizeStringFromBytesLen(t *testing.T) { transform.OptimizeStringFromBytes(mod) }) } + +func TestOptimizeStringFromBytesMapKey(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-mapkey", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversion away. + }) +} + +func TestOptimizeStringFromBytesStringSwitch(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-stringswitch", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversion away. + }) +} + +func TestOptimizeStringFromBytesStringConcat(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/stringfrombytes-stringconcat", func(mod llvm.Module) { + // TODO: optimize the []byte-to-string conversion away. + }) +} diff --git a/transform/testdata/stringfrombytes-mapkey.ll b/transform/testdata/stringfrombytes-mapkey.ll new file mode 100644 index 0000000000..6ab6f1f59f --- /dev/null +++ b/transform/testdata/stringfrombytes-mapkey.ll @@ -0,0 +1,75 @@ +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 @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #0 + +declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #0 + +declare i1 @runtime.hashmapStringGet(ptr, ptr, i32, ptr, i32, ptr) + +declare void @runtime.hashmapStringDelete(ptr, ptr, i32, ptr) + +declare void @runtime.hashmapStringSet(ptr, ptr, i32, ptr, ptr) + +define i1 @main.lookup(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) { +entry: + %value = alloca i32, align 4 + %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 + call void @llvm.lifetime.start.p0(i64 4, ptr %value) + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + call void @llvm.lifetime.end.p0(i64 4, ptr %value) + ret i1 %ok +} + +define void @main.delete(ptr %m, 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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + call void @runtime.hashmapStringDelete(ptr %m, ptr %3, i32 %4, ptr undef) + ret void +} + +define void @main.keepMapSet(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %value, 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 + call void @runtime.hashmapStringSet(ptr %m, ptr %1, i32 %2, ptr %value, ptr undef) + ret void +} + +define i1 @main.keepMapLookupAfterMutation(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) { +entry: + %value = alloca i32, align 4 + %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 + store i8 1, ptr %a.data, align 1 + call void @llvm.lifetime.start.p0(i64 4, ptr %value) + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + call void @llvm.lifetime.end.p0(i64 4, ptr %value) + ret i1 %ok +} + +attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } diff --git a/transform/testdata/stringfrombytes-mapkey.out.ll b/transform/testdata/stringfrombytes-mapkey.out.ll new file mode 100644 index 0000000000..6ab6f1f59f --- /dev/null +++ b/transform/testdata/stringfrombytes-mapkey.out.ll @@ -0,0 +1,75 @@ +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 @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #0 + +declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #0 + +declare i1 @runtime.hashmapStringGet(ptr, ptr, i32, ptr, i32, ptr) + +declare void @runtime.hashmapStringDelete(ptr, ptr, i32, ptr) + +declare void @runtime.hashmapStringSet(ptr, ptr, i32, ptr, ptr) + +define i1 @main.lookup(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) { +entry: + %value = alloca i32, align 4 + %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 + call void @llvm.lifetime.start.p0(i64 4, ptr %value) + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + call void @llvm.lifetime.end.p0(i64 4, ptr %value) + ret i1 %ok +} + +define void @main.delete(ptr %m, 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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + call void @runtime.hashmapStringDelete(ptr %m, ptr %3, i32 %4, ptr undef) + ret void +} + +define void @main.keepMapSet(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %value, 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 + call void @runtime.hashmapStringSet(ptr %m, ptr %1, i32 %2, ptr %value, ptr undef) + ret void +} + +define i1 @main.keepMapLookupAfterMutation(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context) { +entry: + %value = alloca i32, align 4 + %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 + store i8 1, ptr %a.data, align 1 + call void @llvm.lifetime.start.p0(i64 4, ptr %value) + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + call void @llvm.lifetime.end.p0(i64 4, ptr %value) + ret i1 %ok +} + +attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } diff --git a/transform/testdata/stringfrombytes-stringconcat.ll b/transform/testdata/stringfrombytes-stringconcat.ll new file mode 100644 index 0000000000..76b76860c7 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringconcat.ll @@ -0,0 +1,47 @@ +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 } + +@x = internal constant [1 x i8] c"x" + +declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr) + +declare %runtime._string @runtime.stringConcat(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +define %runtime._string @main.concatLeft(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call %runtime._string @runtime.stringConcat(ptr @x, i32 1, ptr %3, i32 %4, ptr undef) + ret %runtime._string %5 +} + +define %runtime._string @main.concatRight(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call %runtime._string @runtime.stringConcat(ptr %3, i32 %4, ptr @x, i32 1, ptr undef) + ret %runtime._string %5 +} + +define %runtime._string @main.keepEmptyConcat(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 + %3 = call %runtime._string @runtime.stringConcat(ptr @x, i32 0, ptr %1, i32 %2, ptr undef) + ret %runtime._string %3 +} diff --git a/transform/testdata/stringfrombytes-stringconcat.out.ll b/transform/testdata/stringfrombytes-stringconcat.out.ll new file mode 100644 index 0000000000..76b76860c7 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringconcat.out.ll @@ -0,0 +1,47 @@ +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 } + +@x = internal constant [1 x i8] c"x" + +declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr) + +declare %runtime._string @runtime.stringConcat(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +define %runtime._string @main.concatLeft(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call %runtime._string @runtime.stringConcat(ptr @x, i32 1, ptr %3, i32 %4, ptr undef) + ret %runtime._string %5 +} + +define %runtime._string @main.concatRight(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call %runtime._string @runtime.stringConcat(ptr %3, i32 %4, ptr @x, i32 1, ptr undef) + ret %runtime._string %5 +} + +define %runtime._string @main.keepEmptyConcat(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 + %3 = call %runtime._string @runtime.stringConcat(ptr @x, i32 0, ptr %1, i32 %2, ptr undef) + ret %runtime._string %3 +} diff --git a/transform/testdata/stringfrombytes-stringswitch.ll b/transform/testdata/stringfrombytes-stringswitch.ll new file mode 100644 index 0000000000..18565f24c2 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringswitch.ll @@ -0,0 +1,59 @@ +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 } + +@hello = internal constant [5 x i8] c"hello" +@bye = internal constant [3 x i8] c"bye" + +declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr) + +declare i1 @runtime.stringEqual(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @sideEffect(ptr) + +define i32 @main.stringSwitch(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call i1 @runtime.stringEqual(ptr %3, i32 %4, ptr @hello, i32 5, ptr undef) + br i1 %5, label %switch.body, label %switch.next + +switch.body: + ret i32 1 + +switch.next: + %6 = extractvalue %runtime._string %0, 0 + %7 = extractvalue %runtime._string %0, 1 + %8 = call i1 @runtime.stringEqual(ptr %6, i32 %7, ptr @bye, i32 3, ptr undef) + br i1 %8, label %switch.body1, label %switch.next2 + +switch.body1: + ret i32 2 + +switch.next2: + ret i32 0 +} + +define i32 @main.keepSideEffect(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 @sideEffect(ptr %a.data) + %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr @hello, i32 5, ptr undef) + br i1 %3, label %switch.body, label %switch.next + +switch.body: + ret i32 1 + +switch.next: + ret i32 0 +} diff --git a/transform/testdata/stringfrombytes-stringswitch.out.ll b/transform/testdata/stringfrombytes-stringswitch.out.ll new file mode 100644 index 0000000000..18565f24c2 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringswitch.out.ll @@ -0,0 +1,59 @@ +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 } + +@hello = internal constant [5 x i8] c"hello" +@bye = internal constant [3 x i8] c"bye" + +declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr) + +declare i1 @runtime.stringEqual(ptr, i32, ptr, i32, ptr) + +declare void @runtime.trackPointer(ptr, ptr, ptr) + +declare void @sideEffect(ptr) + +define i32 @main.stringSwitch(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 + %3 = extractvalue %runtime._string %0, 0 + %4 = extractvalue %runtime._string %0, 1 + %5 = call i1 @runtime.stringEqual(ptr %3, i32 %4, ptr @hello, i32 5, ptr undef) + br i1 %5, label %switch.body, label %switch.next + +switch.body: + ret i32 1 + +switch.next: + %6 = extractvalue %runtime._string %0, 0 + %7 = extractvalue %runtime._string %0, 1 + %8 = call i1 @runtime.stringEqual(ptr %6, i32 %7, ptr @bye, i32 3, ptr undef) + br i1 %8, label %switch.body1, label %switch.next2 + +switch.body1: + ret i32 2 + +switch.next2: + ret i32 0 +} + +define i32 @main.keepSideEffect(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 @sideEffect(ptr %a.data) + %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr @hello, i32 5, ptr undef) + br i1 %3, label %switch.body, label %switch.next + +switch.body: + ret i32 1 + +switch.next: + ret i32 0 +} From 15f6845a93d3bfb319c472d3fe182fe757e28e9a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:14:34 -0700 Subject: [PATCH 8/8] transform: avoid more byte slice string allocations Avoid temporary []byte-to-string allocations for map lookup and delete, string switch comparisons, and string concatenation with a statically non-empty string on the other side. These match additional temporary-string cases that cmd/compile handles without copying the byte slice. Map assignment is intentionally left alone because the map must keep an immutable key after the byte slice can be mutated. --- transform/rtcalls.go | 283 +++++++++++++++++- transform/rtcalls_test.go | 6 +- .../testdata/stringfrombytes-mapkey.out.ll | 27 +- .../stringfrombytes-stringconcat.out.ll | 25 +- .../stringfrombytes-stringswitch.out.ll | 35 +-- 5 files changed, 300 insertions(+), 76 deletions(-) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index ae540f229a..ed72e79313 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -4,6 +4,8 @@ package transform // calls. import ( + "strings" + "tinygo.org/x/go-llvm" ) @@ -83,6 +85,11 @@ func OptimizeStringFromBytesStringEqual(mod llvm.Module) { // nothing to optimize return } + + optimizeStringFromBytesStringEqual(mod, stringFromBytes) +} + +func optimizeStringFromBytesStringEqual(mod llvm.Module, stringFromBytes llvm.Value) { stringEqual := mod.NamedFunction("runtime.stringEqual") if stringEqual.IsNil() { // nothing to optimize @@ -104,6 +111,11 @@ func OptimizeStringFromBytesStringLess(mod llvm.Module) { // nothing to optimize return } + + optimizeStringFromBytesStringLess(mod, stringFromBytes) +} + +func optimizeStringFromBytesStringLess(mod llvm.Module, stringFromBytes llvm.Value) { stringLess := mod.NamedFunction("runtime.stringLess") if stringLess.IsNil() { // nothing to optimize @@ -122,6 +134,10 @@ func OptimizeStringFromBytesLen(mod llvm.Module) { return } + optimizeStringFromBytesLen(stringFromBytes) +} + +func optimizeStringFromBytesLen(stringFromBytes llvm.Value) { for _, call := range getUses(stringFromBytes) { for _, use := range getUses(call) { if use.IsAExtractValueInst().IsNil() { @@ -138,6 +154,132 @@ func OptimizeStringFromBytesLen(mod llvm.Module) { } } +// OptimizeStringFromBytesMapKey transforms map lookups and deletes with +// temporary strings from []byte slices to use the slice data directly. +func OptimizeStringFromBytesMapKey(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + + optimizeStringFromBytesMapKey(mod, stringFromBytes) +} + +func optimizeStringFromBytesMapKey(mod llvm.Module, stringFromBytes llvm.Value) { + optimizeStringFromBytesMapKeyUses(mod.NamedFunction("runtime.hashmapStringGet"), stringFromBytes) + optimizeStringFromBytesMapKeyUses(mod.NamedFunction("runtime.hashmapStringDelete"), stringFromBytes) +} + +func optimizeStringFromBytesMapKeyUses(mapFn, stringFromBytes llvm.Value) { + if mapFn.IsNil() { + return + } + for _, call := range getUses(mapFn) { + str, ok := getTemporaryStringFromBytes(call.Operand(1), call.Operand(2), stringFromBytes, call) + if !ok { + continue + } + call.SetOperand(1, str.ptr) + call.SetOperand(2, str.len) + removeDeadStringFromBytes(str) + } +} + +// OptimizeStringFromBytesStringSwitch transforms repeated string equality +// comparisons against the same temporary string from a []byte slice to use the +// slice data directly. +func OptimizeStringFromBytesStringSwitch(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + + optimizeStringFromBytesStringSwitch(mod, stringFromBytes) +} + +func optimizeStringFromBytesStringSwitch(mod llvm.Module, stringFromBytes llvm.Value) { + stringEqual := mod.NamedFunction("runtime.stringEqual") + if stringEqual.IsNil() { + // nothing to optimize + return + } + + for _, call := range getUses(stringFromBytes) { + consumers, ok := getStringFromBytesStringEqualConsumers(call, stringFromBytes, stringEqual) + if !ok || len(consumers) < 2 { + continue + } + str := stringFromBytesValue{ + call: call, + ptr: call.Operand(0), + len: call.Operand(1), + } + for _, consumer := range consumers { + replaceStringFromBytesCompareOperands(consumer, call, stringFromBytes, str) + } + removeDeadStringFromBytes(str) + } +} + +// OptimizeStringFromBytesStringConcat transforms string concatenations with a +// temporary string from a []byte slice to use the slice data directly when the +// other side is a statically non-empty string. +func OptimizeStringFromBytesStringConcat(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + + optimizeStringFromBytesStringConcat(mod, stringFromBytes) +} + +func optimizeStringFromBytesStringConcat(mod llvm.Module, stringFromBytes llvm.Value) { + stringConcat := mod.NamedFunction("runtime.stringConcat") + if stringConcat.IsNil() { + // nothing to optimize + return + } + + for _, call := range getUses(stringConcat) { + if isNonZeroConstInt(call.Operand(3)) { + str, ok := getTemporaryStringFromBytes(call.Operand(0), call.Operand(1), stringFromBytes, call) + if ok { + call.SetOperand(0, str.ptr) + call.SetOperand(1, str.len) + removeDeadStringFromBytes(str) + } + } + if isNonZeroConstInt(call.Operand(1)) { + str, ok := getTemporaryStringFromBytes(call.Operand(2), call.Operand(3), stringFromBytes, call) + if ok { + call.SetOperand(2, str.ptr) + call.SetOperand(3, str.len) + removeDeadStringFromBytes(str) + } + } + } +} + +// OptimizeStringFromBytes transforms temporary strings created from []byte +// slices into direct uses of the slice data where the string cannot escape. +func OptimizeStringFromBytes(mod llvm.Module) { + stringFromBytes := mod.NamedFunction("runtime.stringFromBytes") + if stringFromBytes.IsNil() { + // nothing to optimize + return + } + + optimizeStringFromBytesStringEqual(mod, stringFromBytes) + optimizeStringFromBytesStringLess(mod, stringFromBytes) + optimizeStringFromBytesMapKey(mod, stringFromBytes) + optimizeStringFromBytesStringSwitch(mod, stringFromBytes) + optimizeStringFromBytesStringConcat(mod, stringFromBytes) + optimizeStringFromBytesLen(stringFromBytes) +} + func optimizeStringFromBytesCompareUses(stringFromBytes, compare llvm.Value) { for _, call := range getUses(compare) { str1, ok1 := getTemporaryStringFromBytes(call.Operand(0), call.Operand(1), stringFromBytes, call) @@ -164,18 +306,6 @@ func optimizeStringFromBytesCompareUses(stringFromBytes, compare llvm.Value) { } } -// OptimizeStringFromBytes transforms temporary strings created from []byte -// slices into direct uses of the slice data where the string cannot escape. -// OptimizeStringFromBytes transforms temporary strings created from []byte -// slices into direct uses of the slice data where the string cannot escape. -// OptimizeStringFromBytes transforms temporary strings created from []byte -// slices into direct uses of the slice data where the string cannot escape. -func OptimizeStringFromBytes(mod llvm.Module) { - OptimizeStringFromBytesStringEqual(mod) - OptimizeStringFromBytesStringLess(mod) - OptimizeStringFromBytesLen(mod) -} - type stringFromBytesValue struct { call llvm.Value ptr llvm.Value @@ -252,6 +382,7 @@ func onlySafeInstructionsBetween(call, stringFromBytes, compare llvm.Value) bool // GC tracking for the temporary string pointer is safe. case isStringFromBytesCall(inst, stringFromBytes): // Another temporary conversion for the same comparison is safe. + case isLifetimeCall(inst): default: return false } @@ -267,6 +398,114 @@ func isStringFromBytesCall(value, stringFromBytes llvm.Value) bool { return !called.IsNil() && called == stringFromBytes } +func getStringFromBytesStringEqualConsumers(call, stringFromBytes, stringEqual llvm.Value) ([]llvm.Value, bool) { + var consumers []llvm.Value + consumerSet := map[llvm.Value]struct{}{} + for _, use := range getUses(call) { + if use.IsAExtractValueInst().IsNil() { + return nil, false + } + for _, extractUse := range getUses(use) { + switch { + case isTrackPointerCall(extractUse): + case isCallTo(extractUse, stringEqual) && stringEqualUsesStringFromBytes(extractUse, call, stringFromBytes): + if _, ok := consumerSet[extractUse]; !ok { + consumerSet[extractUse] = struct{}{} + consumers = append(consumers, extractUse) + } + default: + return nil, false + } + } + } + for _, consumer := range consumers { + if !onlySafeInstructionsBetweenStringFromBytesUse(call, stringFromBytes, consumer, consumerSet) { + return nil, false + } + } + return consumers, true +} + +func stringEqualUsesStringFromBytes(compare, call, stringFromBytes llvm.Value) bool { + for _, pair := range [][2]int{{0, 1}, {2, 3}} { + ptrExtract, ok := getStringFromBytesExtract(compare.Operand(pair[0]), stringFromBytes, 0) + if !ok || ptrExtract.Operand(0) != call { + continue + } + lenExtract, ok := getStringFromBytesExtract(compare.Operand(pair[1]), stringFromBytes, 1) + if ok && lenExtract.Operand(0) == call { + return true + } + } + return false +} + +func replaceStringFromBytesCompareOperands(compare, call, stringFromBytes llvm.Value, str stringFromBytesValue) { + if ptrExtract, ok := getStringFromBytesExtract(compare.Operand(0), stringFromBytes, 0); ok && ptrExtract.Operand(0) == call { + if lenExtract, ok := getStringFromBytesExtract(compare.Operand(1), stringFromBytes, 1); ok && lenExtract.Operand(0) == call { + compare.SetOperand(0, str.ptr) + compare.SetOperand(1, str.len) + } + } + if ptrExtract, ok := getStringFromBytesExtract(compare.Operand(2), stringFromBytes, 0); ok && ptrExtract.Operand(0) == call { + if lenExtract, ok := getStringFromBytesExtract(compare.Operand(3), stringFromBytes, 1); ok && lenExtract.Operand(0) == call { + compare.SetOperand(2, str.ptr) + compare.SetOperand(3, str.len) + } + } +} + +func onlySafeInstructionsBetweenStringFromBytesUse(call, stringFromBytes, use llvm.Value, allowedCalls map[llvm.Value]struct{}) bool { + if call.InstructionParent().Parent() != use.InstructionParent().Parent() { + return false + } + if call.InstructionParent() == use.InstructionParent() { + for inst := llvm.NextInstruction(call); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if inst == use { + return true + } + if !isSafeBetweenStringFromBytesUse(inst, stringFromBytes, allowedCalls) { + return false + } + } + return false + } + for inst := llvm.NextInstruction(call); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if !isSafeBetweenStringFromBytesUse(inst, stringFromBytes, allowedCalls) { + return false + } + } + for inst := use.InstructionParent().FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if inst == use { + return true + } + if !isSafeBetweenStringFromBytesUse(inst, stringFromBytes, allowedCalls) { + return false + } + } + return false +} + +func isSafeBetweenStringFromBytesUse(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 + case isLifetimeCall(inst): + return true + case !inst.IsABranchInst().IsNil(): + return true + default: + return false + } +} + func removeDeadStringFromBytes(str stringFromBytesValue) { for _, use := range getUses(str.call) { if use.IsAExtractValueInst().IsNil() { @@ -297,6 +536,26 @@ func isTrackPointerCall(value llvm.Value) bool { return !called.IsNil() && called.Name() == "runtime.trackPointer" } +func isLifetimeCall(value llvm.Value) bool { + if value.IsACallInst().IsNil() { + return false + } + called := value.CalledValue() + return !called.IsNil() && strings.HasPrefix(called.Name(), "llvm.lifetime.") +} + +func isCallTo(value, fn llvm.Value) bool { + if value.IsACallInst().IsNil() { + return false + } + called := value.CalledValue() + return !called.IsNil() && called == fn +} + +func isNonZeroConstInt(value llvm.Value) bool { + return !value.IsAConstantInt().IsNil() && value.ZExtValue() != 0 +} + // 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. diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 6b70b2f11b..57fb6ce0e8 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -47,20 +47,20 @@ func TestOptimizeStringFromBytesLen(t *testing.T) { func TestOptimizeStringFromBytesMapKey(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-mapkey", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversion away. + transform.OptimizeStringFromBytes(mod) }) } func TestOptimizeStringFromBytesStringSwitch(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-stringswitch", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversion away. + transform.OptimizeStringFromBytes(mod) }) } func TestOptimizeStringFromBytesStringConcat(t *testing.T) { t.Parallel() testTransform(t, "testdata/stringfrombytes-stringconcat", func(mod llvm.Module) { - // TODO: optimize the []byte-to-string conversion away. + transform.OptimizeStringFromBytes(mod) }) } diff --git a/transform/testdata/stringfrombytes-mapkey.out.ll b/transform/testdata/stringfrombytes-mapkey.out.ll index 6ab6f1f59f..9c99b376ec 100644 --- a/transform/testdata/stringfrombytes-mapkey.out.ll +++ b/transform/testdata/stringfrombytes-mapkey.out.ll @@ -7,8 +7,10 @@ declare %runtime._string @runtime.stringFromBytes(ptr, i32, i32, ptr) declare void @runtime.trackPointer(ptr, ptr, ptr) +; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #0 +; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #0 declare i1 @runtime.hashmapStringGet(ptr, ptr, i32, ptr, i32, ptr) @@ -21,14 +23,8 @@ define i1 @main.lookup(ptr %m, ptr %a.data, i32 %a.len, i32 %a.cap, ptr %context entry: %value = alloca i32, align 4 %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 call void @llvm.lifetime.start.p0(i64 4, ptr %value) - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %a.data, i32 %a.len, ptr %value, i32 4, ptr undef) call void @llvm.lifetime.end.p0(i64 4, ptr %value) ret i1 %ok } @@ -36,13 +32,7 @@ entry: define void @main.delete(ptr %m, 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 - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - call void @runtime.hashmapStringDelete(ptr %m, ptr %3, i32 %4, ptr undef) + call void @runtime.hashmapStringDelete(ptr %m, ptr %a.data, i32 %a.len, ptr undef) ret void } @@ -52,8 +42,7 @@ 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 @runtime.trackPointer(ptr %1, ptr %stackalloc, ptr undef) - %2 = extractvalue %runtime._string %0, 1 - call void @runtime.hashmapStringSet(ptr %m, ptr %1, i32 %2, ptr %value, ptr undef) + call void @runtime.hashmapStringSet(ptr %m, ptr %1, i32 %a.len, ptr %value, ptr undef) ret void } @@ -62,12 +51,10 @@ entry: %value = alloca i32, align 4 %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 store i8 1, ptr %a.data, align 1 call void @llvm.lifetime.start.p0(i64 4, ptr %value) - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %3, i32 %4, ptr %value, i32 4, ptr undef) + %2 = extractvalue %runtime._string %0, 0 + %ok = call i1 @runtime.hashmapStringGet(ptr %m, ptr %2, i32 %a.len, ptr %value, i32 4, ptr undef) call void @llvm.lifetime.end.p0(i64 4, ptr %value) ret i1 %ok } diff --git a/transform/testdata/stringfrombytes-stringconcat.out.ll b/transform/testdata/stringfrombytes-stringconcat.out.ll index 76b76860c7..9d9af218cc 100644 --- a/transform/testdata/stringfrombytes-stringconcat.out.ll +++ b/transform/testdata/stringfrombytes-stringconcat.out.ll @@ -14,34 +14,21 @@ declare void @runtime.trackPointer(ptr, ptr, ptr) define %runtime._string @main.concatLeft(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 - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - %5 = call %runtime._string @runtime.stringConcat(ptr @x, i32 1, ptr %3, i32 %4, ptr undef) - ret %runtime._string %5 + %0 = call %runtime._string @runtime.stringConcat(ptr @x, i32 1, ptr %a.data, i32 %a.len, ptr undef) + ret %runtime._string %0 } define %runtime._string @main.concatRight(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 - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - %5 = call %runtime._string @runtime.stringConcat(ptr %3, i32 %4, ptr @x, i32 1, ptr undef) - ret %runtime._string %5 + %0 = call %runtime._string @runtime.stringConcat(ptr %a.data, i32 %a.len, ptr @x, i32 1, ptr undef) + ret %runtime._string %0 } define %runtime._string @main.keepEmptyConcat(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 - %3 = call %runtime._string @runtime.stringConcat(ptr @x, i32 0, ptr %1, i32 %2, ptr undef) - ret %runtime._string %3 + %2 = call %runtime._string @runtime.stringConcat(ptr @x, i32 0, ptr %1, i32 %a.len, ptr undef) + ret %runtime._string %2 } diff --git a/transform/testdata/stringfrombytes-stringswitch.out.ll b/transform/testdata/stringfrombytes-stringswitch.out.ll index 18565f24c2..f596ffbe12 100644 --- a/transform/testdata/stringfrombytes-stringswitch.out.ll +++ b/transform/testdata/stringfrombytes-stringswitch.out.ll @@ -17,28 +17,20 @@ declare void @sideEffect(ptr) define i32 @main.stringSwitch(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 - %3 = extractvalue %runtime._string %0, 0 - %4 = extractvalue %runtime._string %0, 1 - %5 = call i1 @runtime.stringEqual(ptr %3, i32 %4, ptr @hello, i32 5, ptr undef) - br i1 %5, label %switch.body, label %switch.next - -switch.body: + %0 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr @hello, i32 5, ptr undef) + br i1 %0, label %switch.body, label %switch.next + +switch.body: ; preds = %entry ret i32 1 -switch.next: - %6 = extractvalue %runtime._string %0, 0 - %7 = extractvalue %runtime._string %0, 1 - %8 = call i1 @runtime.stringEqual(ptr %6, i32 %7, ptr @bye, i32 3, ptr undef) - br i1 %8, label %switch.body1, label %switch.next2 +switch.next: ; preds = %entry + %1 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr @bye, i32 3, ptr undef) + br i1 %1, label %switch.body1, label %switch.next2 -switch.body1: +switch.body1: ; preds = %switch.next ret i32 2 -switch.next2: +switch.next2: ; preds = %switch.next ret i32 0 } @@ -46,14 +38,13 @@ define i32 @main.keepSideEffect(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %contex 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 @sideEffect(ptr %a.data) - %3 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr @hello, i32 5, ptr undef) - br i1 %3, label %switch.body, label %switch.next + %2 = call i1 @runtime.stringEqual(ptr %1, i32 %a.len, ptr @hello, i32 5, ptr undef) + br i1 %2, label %switch.body, label %switch.next -switch.body: +switch.body: ; preds = %entry ret i32 1 -switch.next: +switch.next: ; preds = %entry ret i32 0 }