Skip to content

Commit a1c823d

Browse files
committed
transform: elide allocations from byte slice equality tests
This change remove the allocation for Go idioms that temporarily convert a bytes slice to a string. For example `string(slice) == "some string"`. In particular, `bytes.Equal` no longer allocates two strings.
1 parent 3be7100 commit a1c823d

File tree

5 files changed

+194
-0
lines changed

5 files changed

+194
-0
lines changed

transform/optimizer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
6565

6666
// Run TinyGo-specific optimization passes.
6767
OptimizeStringToBytes(mod)
68+
OptimizeStringFromBytes(mod)
6869
OptimizeReflectImplements(mod)
6970
maxStackSize := config.MaxStackAlloc()
7071
OptimizeAllocs(mod, nil, maxStackSize, nil)
@@ -91,6 +92,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
9192
fmt.Fprintln(os.Stderr, pos.String()+": "+msg)
9293
})
9394
OptimizeStringToBytes(mod)
95+
OptimizeStringFromBytes(mod)
9496
OptimizeStringEqual(mod)
9597

9698
} else {

transform/rtcalls.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,108 @@ func OptimizeReflectImplements(mod llvm.Module) {
178178
call.EraseFromParentAsInstruction()
179179
}
180180
}
181+
182+
// OptimizeStringFromBytes removes allocations for byte slice equality
183+
// checks that use temporary strings. In particular, `bytes.Equal` allocates
184+
// two such strings:
185+
//
186+
// func Equal(a, b []byte) bool {
187+
// return string(a) == string(b)
188+
// }
189+
func OptimizeStringFromBytes(mod llvm.Module) {
190+
stringFromBytes := mod.NamedFunction("runtime.stringFromBytes")
191+
if stringFromBytes.IsNil() {
192+
return
193+
}
194+
stringEqual := mod.NamedFunction("runtime.stringEqual")
195+
if stringEqual.IsNil() {
196+
return
197+
}
198+
199+
uses:
200+
for _, call := range getUses(stringFromBytes) {
201+
sliceptr := call.Operand(0)
202+
slicelen := call.Operand(1)
203+
// Collect all uses of the slice pointer while replacing
204+
// uses of the string length.
205+
uses := make(map[llvm.Value]bool)
206+
if !collectStringFromBytesUses(uses, slicelen, call) {
207+
continue
208+
}
209+
inst := call
210+
found := 0
211+
// Scan instructions that follow the stringFromBytes call to
212+
// account for all uses. Bail if any instruction may mutate the
213+
// slice storage.
214+
for len(uses) > found {
215+
inst = llvm.NextInstruction(inst)
216+
if inst.IsNil() {
217+
// There are uses beyond this basic block.
218+
continue uses
219+
}
220+
switch {
221+
case !inst.IsACallInst().IsNil():
222+
if inst.CalledValue() != stringEqual {
223+
// The called function is not runtime.stringEqual
224+
// and may mutate the slice.
225+
continue uses
226+
}
227+
case !inst.IsAGetElementPtrInst().IsNil(),
228+
!inst.IsALoadInst().IsNil(),
229+
!inst.IsAExtractValueInst().IsNil():
230+
// Read-only instructions.
231+
default:
232+
// Instruction may perform a store on the slice.
233+
continue uses
234+
}
235+
if _, ok := uses[inst]; ok {
236+
found++
237+
}
238+
}
239+
// At this point, all instructions between the stringFromBytes call
240+
// and its uses are known not to mutate the slice storage. Replace
241+
// all string pointer uses with the slice pointer and get rid of
242+
// the call.
243+
for use, repl := range uses {
244+
if repl {
245+
use.ReplaceAllUsesWith(sliceptr)
246+
use.EraseFromParentAsInstruction()
247+
}
248+
}
249+
call.EraseFromParentAsInstruction()
250+
}
251+
}
252+
253+
// collectStringFromBytesUses collects the string pointer uses, while replacing string
254+
// length uses with the equivalent slice length.
255+
func collectStringFromBytesUses(uses map[llvm.Value]bool, slicelen, v llvm.Value) bool {
256+
if v.IsNil() {
257+
return true
258+
}
259+
for _, use := range getUses(v) {
260+
switch {
261+
case !use.IsAExtractValueInst().IsNil():
262+
switch use.Type().TypeKind() {
263+
case llvm.IntegerTypeKind:
264+
// String length can always safely be replaced with slice length.
265+
use.ReplaceAllUsesWith(slicelen)
266+
use.EraseFromParentAsInstruction()
267+
case llvm.PointerTypeKind:
268+
if !collectStringFromBytesUses(uses, slicelen, use) {
269+
return false
270+
}
271+
// Record the use as replaceable with the slice pointer.
272+
uses[use] = true
273+
default:
274+
return false
275+
}
276+
case !use.IsACallInst().IsNil():
277+
// Record the use, but don't replace it.
278+
uses[use] = false
279+
default:
280+
// Give up.
281+
return false
282+
}
283+
}
284+
return true
285+
}

transform/rtcalls_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ func TestOptimizeReflectImplements(t *testing.T) {
3030
transform.OptimizeReflectImplements(mod)
3131
})
3232
}
33+
34+
func TestOptimizeBytesFromString(t *testing.T) {
35+
t.Parallel()
36+
testTransform(t, "testdata/stringfrombytes", func(mod llvm.Module) {
37+
// Run optimization pass.
38+
transform.OptimizeStringFromBytes(mod)
39+
})
40+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
2+
target triple = "x86_64--linux"
3+
4+
@str = constant [6 x i8] c"foobar"
5+
6+
declare { ptr, i64, i64 } @runtime.stringToBytes(ptr, i64)
7+
8+
declare { ptr, i64 } @runtime.stringFromBytes(ptr, i64, i64)
9+
10+
declare i1 @runtime.stringEqual(ptr nocapture, i64, ptr nocapture, i64)
11+
12+
declare void @maybeSideEffect()
13+
14+
declare void @readString(ptr nocapture, i64)
15+
16+
define void @testReadOnly() {
17+
entry:
18+
; Build byte slice.
19+
%0 = call fastcc { ptr, i64, i64 } @runtime.stringToBytes(ptr @str, i64 6)
20+
%1 = extractvalue { ptr, i64, i64 } %0, 0
21+
%2 = extractvalue { ptr, i64, i64 } %0, 1
22+
%3 = extractvalue { ptr, i64, i64 } %0, 2
23+
24+
; Test that a side-effect free string equality check can optimize the stringFromBytes
25+
; call away.
26+
%4 = call fastcc { ptr, i64, i64 } @runtime.stringFromBytes(ptr %1, i64 %2, i64 %3)
27+
%5 = extractvalue { ptr, i64, i64 } %4, 0
28+
%6 = extractvalue { ptr, i64, i64 } %4, 1
29+
call fastcc i1 @runtime.stringEqual(ptr %5, i64 %6, ptr %5, i64 %6)
30+
31+
; Compare it again, but with an intermittent side-effect that blocks the optimization.
32+
%9 = call fastcc { ptr, i64, i64 } @runtime.stringFromBytes(ptr %1, i64 %2, i64 %3)
33+
%10 = extractvalue { ptr, i64, i64 } %9, 0
34+
%11 = extractvalue { ptr, i64, i64 } %9, 1
35+
; Function call may write to the slice storage.
36+
call fastcc void @maybeSideEffect()
37+
call fastcc i1 @runtime.stringEqual(ptr %10, i64 %11, ptr %10, i64 %11)
38+
39+
; Reading the string after comparing should also defeat the optimization.
40+
%13 = call fastcc { ptr, i64, i64 } @runtime.stringFromBytes(ptr %1, i64 %2, i64 %3)
41+
%14 = extractvalue { ptr, i64, i64 } %13, 0
42+
%15 = extractvalue { ptr, i64, i64 } %13, 1
43+
call fastcc i1 @runtime.stringEqual(ptr %14, i64 %15, ptr %14, i64 %15)
44+
call fastcc void @readString(ptr %14, i64 %15)
45+
ret void
46+
}
47+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
2+
target triple = "x86_64--linux"
3+
4+
@str = constant [6 x i8] c"foobar"
5+
6+
declare { ptr, i64, i64 } @runtime.stringToBytes(ptr, i64)
7+
8+
declare { ptr, i64 } @runtime.stringFromBytes(ptr, i64, i64)
9+
10+
declare i1 @runtime.stringEqual(ptr nocapture, i64, ptr nocapture, i64)
11+
12+
declare void @maybeSideEffect()
13+
14+
declare void @readString(ptr nocapture, i64)
15+
16+
define void @testReadOnly() {
17+
entry:
18+
%0 = call fastcc { ptr, i64, i64 } @runtime.stringToBytes(ptr @str, i64 6)
19+
%1 = extractvalue { ptr, i64, i64 } %0, 0
20+
%2 = extractvalue { ptr, i64, i64 } %0, 1
21+
%3 = extractvalue { ptr, i64, i64 } %0, 2
22+
%4 = call fastcc i1 @runtime.stringEqual(ptr %1, i64 %2, ptr %1, i64 %2)
23+
%5 = call fastcc { ptr, i64, i64 } @runtime.stringFromBytes(ptr %1, i64 %2, i64 %3)
24+
%6 = extractvalue { ptr, i64, i64 } %5, 0
25+
call fastcc void @maybeSideEffect()
26+
%7 = call fastcc i1 @runtime.stringEqual(ptr %6, i64 %2, ptr %6, i64 %2)
27+
%8 = call fastcc { ptr, i64, i64 } @runtime.stringFromBytes(ptr %1, i64 %2, i64 %3)
28+
%9 = extractvalue { ptr, i64, i64 } %8, 0
29+
%10 = call fastcc i1 @runtime.stringEqual(ptr %9, i64 %2, ptr %9, i64 %2)
30+
call fastcc void @readString(ptr %9, i64 %2)
31+
ret void
32+
}

0 commit comments

Comments
 (0)