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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cl/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/goplus/llgo/internal/build"
"github.com/goplus/llgo/internal/buildenv"
"github.com/goplus/llgo/internal/cabi"
"github.com/goplus/llgo/internal/filecheck"
"github.com/goplus/llgo/internal/llgen"
"github.com/goplus/llgo/internal/lto"
llvmenv "github.com/goplus/llgo/xtool/env/llvm"
Expand Down Expand Up @@ -390,6 +391,21 @@ func TestBuildAndCheckSymbolsFromTestltoLTOPluginAggregateABI(t *testing.T) {
}
}

func TestBuildAndCheckSymbolsFromTestltoLTOPluginSharedSRet(t *testing.T) {
conf := testltoLTOPluginConf(t, build.ModeGen)
checkFile := filepath.Join("..", "ltoplugin", "testdata", "shared_sret.ll")
opt := filepath.Join(llvmenv.New("").BinDir(), "opt")
cmd := exec.Command(opt, "-load-pass-plugin="+conf.LTOPlugin.Path,
"-passes=llgo-lto-pre-globaldce", "-S", "-o", "-", checkFile)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("run LTO plugin for shared sret: %v\n%s", err, out)
}
if err := filecheck.Match(checkFile, string(out)); err != nil {
t.Fatal(err)
}
}

func TestFilterEmulatorOutput(t *testing.T) {
tests := []struct {
name string
Expand Down
57 changes: 53 additions & 4 deletions ltoplugin/LLGOReflectMethodByNamePass.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "LLGOLTOPasses.h"

#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Analysis/ConstantFolding.h"
Expand Down Expand Up @@ -1219,6 +1220,11 @@ Value *getSRetArg(CallBase *CB) {
return nullptr;
}

Value *getSRetStorage(CallBase *CB) {
Value *SRet = getSRetArg(CB);
return SRet ? SRet->stripPointerCasts() : nullptr;
}

void addReflectMethodCheckedLoad(CallBase *CheckedLoad, StringRef GenericTypeID,
SmallPtrSetImpl<CallBase *> &SeenLoads,
SmallVectorImpl<CallBase *> &Loads) {
Expand Down Expand Up @@ -1288,7 +1294,20 @@ class LLGOLTOPreGlobalDCEPass : public PassInfoMixin<LLGOLTOPreGlobalDCEPass> {

bool Changed = false;
const DataLayout &DL = M.getDataLayout();
DenseMap<CallBase *, Value *> SRetStorageByCall;
DenseMap<Value *, SmallVector<CallBase *, 4>> CallsBySRetStorage;
for (CallBase *ReflectCall : Calls) {
Value *SRetStorage = getSRetStorage(ReflectCall);
SRetStorageByCall.insert({ReflectCall, SRetStorage});
if (SRetStorage)
CallsBySRetStorage[SRetStorage].push_back(ReflectCall);
}

SmallPtrSet<CallBase *, 16> ProcessedCalls;
for (CallBase *ReflectCall : Calls) {
if (!ProcessedCalls.insert(ReflectCall).second)
continue;

Attribute KindAttr = ReflectCall->getFnAttr(ReflectMethodByNameCallAttr);
if (!KindAttr.isStringAttribute() || ReflectCall->arg_empty())
continue;
Expand All @@ -1306,17 +1325,47 @@ class LLGOLTOPreGlobalDCEPass : public PassInfoMixin<LLGOLTOPreGlobalDCEPass> {
continue;
}

// Optimizations can leave several MethodByName calls sharing one sret
// slot. The checked-load walk starts from the slot and sees the loads for
// all of those calls. Refine them with the union of every provable name
// instead of letting the first call claim all of them. If any name is
// unknown, leave the generic markers intact so GlobalDCE remains
// conservative.
Value *SRetStorage = SRetStorageByCall.lookup(ReflectCall);
SmallVector<CallBase *, 4> SharedCalls{ReflectCall};
if (SRetStorage) {
for (CallBase *Candidate :
CallsBySRetStorage.find(SRetStorage)->second) {
if (Candidate == ReflectCall)
continue;
Attribute CandidateKind =
Candidate->getFnAttr(ReflectMethodByNameCallAttr);
if (!CandidateKind.isStringAttribute() ||
CandidateKind.getValueAsString() != Kind)
continue;
SharedCalls.push_back(Candidate);
ProcessedCalls.insert(Candidate);
}
}

SmallVector<std::string, 4> Names;
bool KnownNames = collectStringSetFromCallArgs(ReflectCall, DL, Names);
bool KnownNames = true;
// The bounded name budget applies to the combined group. Exceeding it
// leaves every generic marker intact.
for (CallBase *SharedCall : SharedCalls) {
if (!collectStringSetFromCallArgs(SharedCall, DL, Names)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior note: MaxReflectMethodNames (32) is now applied to the union of names across the whole shared group, not per call. A shared slot with several calls whose combined unique names exceed 32 will hit the cap in addName, set KnownNames=false, and refine none of them (falling back to the generic marker). This is safe/conservative, but the shared budget is a subtle change from the previous per-call limit — worth a one-line comment here so a future maintainer isn't surprised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment that the bounded name budget applies to the combined shared group and that exceeding it preserves the generic markers.

KnownNames = false;
break;
}
}
if (!KnownNames || Names.empty())
continue;

SmallVector<CallBase *, 2> GenericLoads;
SmallPtrSet<CallBase *, 4> SeenLoads;
SmallPtrSet<Value *, 16> SeenValues;
collectSRetReflectMethodCheckedLoads(getSRetArg(ReflectCall),
GenericTypeID, SeenValues, SeenLoads,
GenericLoads);
collectSRetReflectMethodCheckedLoads(SRetStorage, GenericTypeID,
SeenValues, SeenLoads, GenericLoads);
for (CallBase *GenericLoad : GenericLoads) {
insertMethodNameChecks(GenericLoad, Names, Prefix);
if (!eraseGenericCheckedLoad(GenericLoad))
Expand Down
104 changes: 104 additions & 0 deletions ltoplugin/testdata/shared_sret.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
; RUN: opt -load-pass-plugin=%plugin -passes=llgo-lto-pre-globaldce -S %s | FileCheck %s

target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"

%reflect.Value = type { ptr }

@query = private unnamed_addr constant [5 x i8] c"Query"
@mutation = private unnamed_addr constant [8 x i8] c"Mutation"
@subscription = private unnamed_addr constant [12 x i8] c"Subscription"

declare void @reflect.Value.MethodByName(ptr sret(%reflect.Value), ptr, i64)
declare { ptr, i1 } @llvm.type.checked.load(ptr, i32, metadata)
declare void @llvm.assume(i1)

define void @shared_sret(ptr %receiver) {
entry:
%ret = alloca %reflect.Value
call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @query, i64 5) #0
%query.fn = load ptr, ptr %ret
%query.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %query.fn, i32 0, metadata !"go.method.value.reflect")
%query.ok = extractvalue { ptr, i1 } %query.checked, 1
call void @llvm.assume(i1 %query.ok)

call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @mutation, i64 8) #0
%mutation.fn = load ptr, ptr %ret
%mutation.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %mutation.fn, i32 0, metadata !"go.method.value.reflect")
%mutation.ok = extractvalue { ptr, i1 } %mutation.checked, 1
call void @llvm.assume(i1 %mutation.ok)

call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @subscription, i64 12) #0
%subscription.fn = load ptr, ptr %ret
%subscription.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %subscription.fn, i32 0, metadata !"go.method.value.reflect")
%subscription.ok = extractvalue { ptr, i1 } %subscription.checked, 1
call void @llvm.assume(i1 %subscription.ok)
ret void
}

define void @shared_sret_unknown(ptr %receiver, ptr %dynamic.name, i64 %dynamic.len) {
entry:
%ret = alloca %reflect.Value
call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @query, i64 5) #0
%query.fn = load ptr, ptr %ret
%query.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %query.fn, i32 0, metadata !"go.method.value.reflect")
%query.ok = extractvalue { ptr, i1 } %query.checked, 1
call void @llvm.assume(i1 %query.ok)

call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" %dynamic.name, i64 %dynamic.len) #0
%dynamic.fn = load ptr, ptr %ret
%dynamic.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %dynamic.fn, i32 0, metadata !"go.method.value.reflect")
%dynamic.ok = extractvalue { ptr, i1 } %dynamic.checked, 1
call void @llvm.assume(i1 %dynamic.ok)
ret void
}

define void @shared_sret_duplicate(ptr %receiver) {
entry:
%ret = alloca %reflect.Value
call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @query, i64 5) #0
%first.fn = load ptr, ptr %ret
%first.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %first.fn, i32 0, metadata !"go.method.value.reflect")
%first.ok = extractvalue { ptr, i1 } %first.checked, 1
call void @llvm.assume(i1 %first.ok)

call void @reflect.Value.MethodByName(ptr sret(%reflect.Value) %ret, ptr "llgo.reflect.methodbyname.name"="1" @query, i64 5) #0
%second.fn = load ptr, ptr %ret
%second.checked = call { ptr, i1 } @llvm.type.checked.load(ptr %second.fn, i32 0, metadata !"go.method.value.reflect")
%second.ok = extractvalue { ptr, i1 } %second.checked, 1
call void @llvm.assume(i1 %second.ok)
ret void
}

; CHECK-LABEL: define void @shared_sret(
; CHECK: call void @reflect.Value.MethodByName({{.*}}@query
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Query")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Mutation")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Subscription")
; CHECK: call void @reflect.Value.MethodByName({{.*}}@mutation
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Query")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Mutation")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Subscription")
; CHECK: call void @reflect.Value.MethodByName({{.*}}@subscription
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Query")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Mutation")
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Subscription")
; CHECK-NOT: !"go.method.value.reflect"

; CHECK-LABEL: define void @shared_sret_unknown(
; CHECK-NOT: !"go.method.value.reflect.Query"
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect")
; CHECK-NOT: !"go.method.value.reflect.Query"
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect")
; CHECK-NOT: !"go.method.value.reflect.Query"
; CHECK: ret void

; CHECK-LABEL: define void @shared_sret_duplicate(
; CHECK: call void @reflect.Value.MethodByName({{.*}}@query
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Query")
; CHECK-NOT: !"go.method.value.reflect.Query"
; CHECK: call void @reflect.Value.MethodByName({{.*}}@query
; CHECK: @llvm.type.checked.load({{.*}}!"go.method.value.reflect.Query")
; CHECK-NOT: !"go.method.value.reflect.Query"
; CHECK: ret void

attributes #0 = { "llgo.reflect.methodbyname"="value" }
Loading