From 3a8a90892cdb276ea6612774a33772b3b5778462 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:12:56 -0700 Subject: [PATCH 1/6] transform: add byte slice string equality tests --- transform/rtcalls_test.go | 7 ++ .../testdata/stringfrombytes-stringequal.ll | 107 +++++++++++++++++ .../stringfrombytes-stringequal.out.ll | 110 ++++++++++++++++++ 3 files changed, 224 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..3fc259de00 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringequal.ll @@ -0,0 +1,107 @@ +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) + +declare void @llvm.lifetime.end.p0(ptr captures(none)) + +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 +} + +define i32 @main.equalAndLen(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 + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %len = extractvalue %runtime._string %0, 1 + %equal.ext = zext i1 %equal to i32 + %result = add i32 %len, %equal.ext + ret i32 %result +} + +define i1 @main.equalBeforeOtherUse(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 + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + call void @useString(ptr %1, i32 %2) + ret i1 %equal +} + +define i1 @main.twoComparisons(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 + %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %result = and i1 %equal1, %equal2 + ret i1 %result +} + +define i1 @main.keepComparisonAfterMutation(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 + %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + store i8 1, ptr %a.data, align 1 + %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %result = and i1 %equal1, %equal2 + ret i1 %result +} + +define i1 @main.keepAfterLifetimeEnd(ptr %s.data, i32 %s.len, ptr %context) { +entry: + %a.data = alloca [4 x i8], align 1 + %0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 4, i32 4, ptr undef) + %1 = extractvalue %runtime._string %0, 0 + %2 = extractvalue %runtime._string %0, 1 + call void @llvm.lifetime.end.p0(ptr %a.data) + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %equal +} diff --git a/transform/testdata/stringfrombytes-stringequal.out.ll b/transform/testdata/stringfrombytes-stringequal.out.ll new file mode 100644 index 0000000000..a04d6374c8 --- /dev/null +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -0,0 +1,110 @@ +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) + +; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) +declare void @llvm.lifetime.end.p0(ptr captures(none)) #0 + +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 +} + +define i32 @main.equalAndLen(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 + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %len = extractvalue %runtime._string %0, 1 + %equal.ext = zext i1 %equal to i32 + %result = add i32 %len, %equal.ext + ret i32 %result +} + +define i1 @main.equalBeforeOtherUse(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 + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + call void @useString(ptr %1, i32 %2) + ret i1 %equal +} + +define i1 @main.twoComparisons(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 + %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %result = and i1 %equal1, %equal2 + ret i1 %result +} + +define i1 @main.keepComparisonAfterMutation(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 + %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + store i8 1, ptr %a.data, align 1 + %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %result = and i1 %equal1, %equal2 + ret i1 %result +} + +define i1 @main.keepAfterLifetimeEnd(ptr %s.data, i32 %s.len, ptr %context) { +entry: + %a.data = alloca [4 x i8], align 1 + %0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 4, i32 4, ptr undef) + %1 = extractvalue %runtime._string %0, 0 + %2 = extractvalue %runtime._string %0, 1 + call void @llvm.lifetime.end.p0(ptr %a.data) + %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + ret i1 %equal +} + +attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } From 67e06c2c3af6ebef40894883e655ea21230bd51b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:13:25 -0700 Subject: [PATCH 2/6] transform: avoid byte slice string equality allocations Rewrite string equality comparisons when either operand comes from a temporary []byte-to-string conversion to compare the original slice data directly. Each comparison is optimized independently only when it is in the same basic block and every intervening instruction is known not to mutate the slice. The copied string remains for any other uses. Fixes issue 4045 --- transform/optimizer.go | 6 +- transform/rtcalls.go | 156 +++++++++++++++++- transform/rtcalls_test.go | 2 +- .../stringfrombytes-stringequal.out.ll | 39 ++--- 4 files changed, 170 insertions(+), 33 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..a1edd69da9 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -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: @@ -71,6 +69,158 @@ 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 + } + + stringEqual := mod.NamedFunction("runtime.stringEqual") + if stringEqual.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 _, call := range getUses(stringEqual) { + safeCalls[call] = struct{}{} + } + + // Rewrite each supported use independently, and remove the conversion only + // when no unconverted uses remain. + for _, call := range getUses(stringFromBytes) { + for _, extract := range getUses(call) { + if extract.IsAExtractValueInst().IsNil() { + continue + } + indices := extract.Indices() + if len(indices) != 1 || indices[0] != 0 { + continue + } + for _, use := range getUses(extract) { + if _, ok := safeCalls[use]; !ok { + continue + } + if !isSafeStringFromBytesUse(call, stringFromBytes, use, safeCalls) { + continue + } + replaceStringFromBytesCompareUse(use, extract, call, stringFromBytes) + } + } + 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. 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 a04d6374c8..65a9b5aa19 100644 --- a/transform/testdata/stringfrombytes-stringequal.out.ll +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -17,25 +17,14 @@ declare void @llvm.lifetime.end.p0(ptr captures(none)) #0 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) { @@ -44,11 +33,8 @@ 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 } define i32 @main.equalAndLen(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s.data, i32 %s.len, ptr %context) { @@ -56,7 +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 %2 = extractvalue %runtime._string %0, 1 - %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) %len = extractvalue %runtime._string %0, 1 %equal.ext = zext i1 %equal to i32 %result = add i32 %len, %equal.ext @@ -68,18 +54,15 @@ 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 - %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) call void @useString(ptr %1, i32 %2) ret i1 %equal } define i1 @main.twoComparisons(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 - %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) - %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal1 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) + %equal2 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) %result = and i1 %equal1, %equal2 ret i1 %result } @@ -89,7 +72,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 %2 = extractvalue %runtime._string %0, 1 - %equal1 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal1 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) store i8 1, ptr %a.data, align 1 %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) %result = and i1 %equal1, %equal2 From a64b956d53a54a77cd49430260590bacccc884ab Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:13:43 -0700 Subject: [PATCH 3/6] transform: add byte slice string ordering tests --- 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 84c633ec5fcf6d57fa43d680a73862b0e66153c7 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:14:15 -0700 Subject: [PATCH 4/6] transform: avoid byte slice string ordering allocations Reuse the temporary []byte-to-string comparison rewrite for runtime.stringLess. The same-block safety check preserves conversions when intervening instructions could mutate the source slice. --- transform/rtcalls.go | 16 ++++----- transform/rtcalls_test.go | 2 +- .../stringfrombytes-stringless.out.ll | 33 +++++-------------- 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index a1edd69da9..e8de5f061f 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -79,17 +79,17 @@ func OptimizeStringFromBytes(mod llvm.Module) { return } - stringEqual := mod.NamedFunction("runtime.stringEqual") - if stringEqual.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 _, call := range getUses(stringEqual) { - safeCalls[call] = 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, and remove the conversion only 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 e0a0742cd9f9b4191d9504df38cf443ac62ab181 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:14:45 -0700 Subject: [PATCH 5/6] transform: add byte slice string length tests --- 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 8a5c6e4edc74909019e3940698ea446e6a217a81 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:15:13 -0700 Subject: [PATCH 6/6] transform: avoid byte slice string length allocations Rewrite len(string(b)) to use the original slice length. This is always valid even when the copied string must remain for other uses. --- transform/rtcalls.go | 31 +++++++++++++------ transform/rtcalls_test.go | 2 +- transform/testdata/stringfrombytes-len.out.ll | 13 ++------ .../stringfrombytes-stringequal.out.ll | 22 +++++-------- .../stringfrombytes-stringless.out.ll | 7 ++--- 5 files changed, 35 insertions(+), 40 deletions(-) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index e8de5f061f..b8424c76c3 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -92,27 +92,38 @@ func OptimizeStringFromBytes(mod llvm.Module) { } } - // Rewrite each supported use independently, and remove the conversion only - // when no unconverted uses remain. + // 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 || indices[0] != 0 { + if len(indices) != 1 { continue } - for _, use := range getUses(extract) { - if _, ok := safeCalls[use]; !ok { - 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) } - 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) } } 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 65a9b5aa19..ec426da76c 100644 --- a/transform/testdata/stringfrombytes-stringequal.out.ll +++ b/transform/testdata/stringfrombytes-stringequal.out.ll @@ -31,21 +31,16 @@ 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 } define i32 @main.equalAndLen(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 %equal = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) - %len = extractvalue %runtime._string %0, 1 %equal.ext = zext i1 %equal to i32 - %result = add i32 %len, %equal.ext + %result = add i32 %a.len, %equal.ext ret i32 %result } @@ -53,9 +48,8 @@ define i1 @main.equalBeforeOtherUse(ptr %a.data, i32 %a.len, i32 %a.cap, ptr %s. 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 %equal = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) - call void @useString(ptr %1, i32 %2) + call void @useString(ptr %1, i32 %a.len) ret i1 %equal } @@ -71,10 +65,9 @@ define i1 @main.keepComparisonAfterMutation(ptr %a.data, i32 %a.len, i32 %a.cap, 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 %equal1 = call i1 @runtime.stringEqual(ptr %a.data, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) store i8 1, ptr %a.data, align 1 - %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal2 = call i1 @runtime.stringEqual(ptr %1, i32 %a.len, ptr %s.data, i32 %s.len, ptr undef) %result = and i1 %equal1, %equal2 ret i1 %result } @@ -84,9 +77,8 @@ entry: %a.data = alloca [4 x i8], align 1 %0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 4, i32 4, ptr undef) %1 = extractvalue %runtime._string %0, 0 - %2 = extractvalue %runtime._string %0, 1 call void @llvm.lifetime.end.p0(ptr %a.data) - %equal = call i1 @runtime.stringEqual(ptr %1, i32 %2, ptr %s.data, i32 %s.len, ptr undef) + %equal = call i1 @runtime.stringEqual(ptr %1, i32 4, ptr %s.data, i32 %s.len, ptr undef) ret i1 %equal } 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 }