runtime: precisely scan globals on all platforms

This commit is contained in:
Jake Bailey
2026-08-07 12:08:32 -07:00
committed by Ron Evans
parent ede501ae2b
commit 59fb104004
18 changed files with 136 additions and 324 deletions
+64
View File
@@ -58,7 +58,9 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
// LLVM 17 doesn't have the no-verify-fixpoint flag.
optPasses = "globaldce,globalopt,ipsccp,instcombine,adce,function-attrs"
}
blockGlobalAllocPromotion(mod)
err := mod.RunPasses(optPasses, llvm.TargetMachine{}, po)
removeGlobalAllocPromotionMarker(mod)
if err != nil {
return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
}
@@ -80,7 +82,9 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
// After interfaces are lowered, there are many more opportunities for
// interprocedural optimizations. To get them to work, function
// attributes have to be updated first.
blockGlobalAllocPromotion(mod)
err = mod.RunPasses(optPasses, llvm.TargetMachine{}, po)
removeGlobalAllocPromotionMarker(mod)
if err != nil {
return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
}
@@ -162,7 +166,9 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
po := llvm.NewPassBuilderOptions()
defer po.Dispose()
passes := fmt.Sprintf("thinlto-pre-link<%s>", optLevel)
blockGlobalAllocPromotion(mod)
err := mod.RunPasses(passes, llvm.TargetMachine{}, po)
removeGlobalAllocPromotionMarker(mod)
if err != nil {
return []error{fmt.Errorf("could not build pass pipeline: %w", err)}
}
@@ -177,6 +183,64 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
return nil
}
func blockGlobalAllocPromotion(mod llvm.Module) {
ctx := mod.Context()
ptrType := llvm.PointerType(ctx.Int8Type(), 0)
marker := llvm.AddFunction(mod, "tinygo.gc.alloc.marker", llvm.FunctionType(ctx.VoidType(), []llvm.Type{ptrType}, false))
builder := ctx.NewBuilder()
defer builder.Dispose()
var marked bool
for _, name := range []string{"runtime.alloc", "runtime.alloc_noheap"} {
alloc := mod.NamedFunction(name)
if alloc.IsNil() {
continue
}
for _, call := range getUses(alloc) {
if call.IsACallInst().IsNil() || call.CalledValue() != alloc {
continue
}
if isPointerFreeAllocation(call) {
continue
}
// GlobalOpt may otherwise turn this allocation into an untyped
// global, hiding its pointer fields from makeGCGlobalRoots.
next := llvm.NextInstruction(call)
if next.IsNil() {
continue
}
builder.SetInsertPointBefore(next)
builder.CreateCall(marker.GlobalValueType(), marker, []llvm.Value{call}, "")
marked = true
}
}
if !marked {
marker.EraseFromParentAsFunction()
}
}
func isPointerFreeAllocation(call llvm.Value) bool {
const noPointerLayout = 3
layout := call.Operand(1)
return !layout.IsAConstantExpr().IsNil() &&
layout.Opcode() == llvm.IntToPtr &&
!layout.Operand(0).IsAConstantInt().IsNil() &&
layout.Operand(0).ZExtValue() == noPointerLayout
}
func removeGlobalAllocPromotionMarker(mod llvm.Module) {
marker := mod.NamedFunction("tinygo.gc.alloc.marker")
if marker.IsNil() {
return
}
for _, call := range getUses(marker) {
call.EraseFromParentAsInstruction()
}
marker.EraseFromParentAsFunction()
}
// functionsUsedInTransform is a list of function symbols that may be used
// during TinyGo optimization passes so they have to be marked as external
// linkage until all TinyGo passes have finished.
+33
View File
@@ -0,0 +1,33 @@
package transform
import (
"testing"
"tinygo.org/x/go-llvm"
)
func TestBlockGlobalAllocPromotionUses(t *testing.T) {
ctx := llvm.NewContext()
defer ctx.Dispose()
buf, err := llvm.NewMemoryBufferFromFile("testdata/optimizer-alloc-uses.ll")
if err != nil {
t.Fatal(err)
}
mod, err := ctx.ParseIR(buf)
if err != nil {
t.Fatal(err)
}
defer mod.Dispose()
blockGlobalAllocPromotion(mod)
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
marker := mod.NamedFunction("tinygo.gc.alloc.marker")
if marker.IsNil() {
t.Fatal("allocation marker was not created")
}
if uses := getUses(marker); len(uses) != 1 {
t.Fatalf("got %d marker uses, want 1", len(uses))
}
}
+20
View File
@@ -0,0 +1,20 @@
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
target triple = "wasm32-unknown-unknown-wasm"
@allocFunction = constant ptr @runtime.alloc
declare ptr @runtime.alloc(i32, ptr)
declare void @use(ptr)
define void @passAllocator() {
entry:
call void @use(ptr @runtime.alloc)
ret void
}
define ptr @allocate() {
entry:
%allocation = call ptr @runtime.alloc(i32 4, ptr inttoptr (i32 5 to ptr))
ret ptr %allocation
}