Skip to content

Commit 40cd0a9

Browse files
[Feat] Enable LTO
Signed-off-by: ZhouGuangyuan <zhouguangyuan.xian@gmail.com>
1 parent c18be41 commit 40cd0a9

File tree

8 files changed

+177
-2
lines changed

8 files changed

+177
-2
lines changed

_demo/embed/targetsbuild/build.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ case "$test_dir" in
111111
"riscv32"
112112
"riscv64"
113113
"rp2040"
114+
"nintendoswitch" # undefined symbol under lto, should not work when no-lto
114115
)
115116
;;
116117
defer)

internal/build/build.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
897897
// linkInputs contains .a archives from all packages and .o files from main module
898898
var linkInputs []string
899899
var linkArgs []string
900+
var publicAPISymbols []string
900901
var rtLinkInputs []string
901902
var rtLinkArgs []string
902903
linkedPkgs := make(map[string]bool) // Track linked packages by ID to avoid duplicates
@@ -929,6 +930,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
929930
if aPkg.LPkg.NeedAbiInit {
930931
needAbiInit = true
931932
}
933+
publicAPISymbols = append(publicAPISymbols, exportedSymbols(aPkg)...)
932934

933935
linkArgs = append(linkArgs, aPkg.LinkArgs...)
934936
if aPkg.ArchiveFile != "" {
@@ -976,6 +978,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
976978
}
977979
}
978980
}
981+
linkArgs = append(linkArgs, ltoPublicAPIArgs(ctx, publicAPISymbols)...)
979982

980983
err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose)
981984
if err != nil {
@@ -985,6 +988,68 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
985988
return nil
986989
}
987990

991+
func exportedSymbols(aPkg *aPackage) []string {
992+
if aPkg == nil || aPkg.LPkg == nil {
993+
return nil
994+
}
995+
exports := aPkg.LPkg.ExportFuncs()
996+
if len(exports) == 0 {
997+
return nil
998+
}
999+
symbols := make([]string, 0, len(exports))
1000+
for _, symbol := range exports {
1001+
symbols = append(symbols, symbol)
1002+
}
1003+
return symbols
1004+
}
1005+
1006+
func ltoPublicAPIArgs(ctx *context, symbols []string) []string {
1007+
if !isLTOEnabled(ctx.crossCompile) {
1008+
return nil
1009+
}
1010+
symbols = normalizeSymbols(symbols)
1011+
if len(symbols) == 0 {
1012+
return nil
1013+
}
1014+
return []string{
1015+
"-mllvm",
1016+
"-internalize-public-api-list=" + strings.Join(symbols, ","),
1017+
}
1018+
}
1019+
1020+
func isLTOEnabled(cfg crosscompile.Export) bool {
1021+
return hasLTOFlag(cfg.CFLAGS) || hasLTOFlag(cfg.CCFLAGS) || hasLTOFlag(cfg.LDFLAGS)
1022+
}
1023+
1024+
func hasLTOFlag(flags []string) bool {
1025+
for _, flag := range flags {
1026+
if strings.Contains(flag, "lto") {
1027+
return true
1028+
}
1029+
}
1030+
return false
1031+
}
1032+
1033+
func normalizeSymbols(symbols []string) []string {
1034+
if len(symbols) == 0 {
1035+
return nil
1036+
}
1037+
seen := make(map[string]struct{}, len(symbols))
1038+
out := make([]string, 0, len(symbols))
1039+
for _, symbol := range symbols {
1040+
if symbol == "" {
1041+
continue
1042+
}
1043+
if _, ok := seen[symbol]; ok {
1044+
continue
1045+
}
1046+
seen[symbol] = struct{}{}
1047+
out = append(out, symbol)
1048+
}
1049+
slices.Sort(out)
1050+
return out
1051+
}
1052+
9881053
// isRuntimePkg reports whether the package path belongs to the llgo runtime tree.
9891054
func isRuntimePkg(pkgPath string) bool {
9901055
rtRoot := env.LLGoRuntimePkg

internal/build/lto_exports_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//go:build !llgo
2+
// +build !llgo
3+
4+
package build
5+
6+
import (
7+
"reflect"
8+
"testing"
9+
10+
"github.com/goplus/llgo/internal/crosscompile"
11+
)
12+
13+
func TestNormalizeSymbols(t *testing.T) {
14+
got := normalizeSymbols([]string{"beta", "", "alpha", "beta", "alpha", "gamma"})
15+
want := []string{"alpha", "beta", "gamma"}
16+
if !reflect.DeepEqual(got, want) {
17+
t.Fatalf("normalizeSymbols() = %v, want %v", got, want)
18+
}
19+
}
20+
21+
func TestLTOPublicAPIArgs(t *testing.T) {
22+
ctx := &context{
23+
crossCompile: crosscompile.Export{
24+
LDFLAGS: []string{"--lto-O0"},
25+
},
26+
}
27+
got := ltoPublicAPIArgs(ctx, []string{"beta", "alpha", "beta"})
28+
want := []string{
29+
"-mllvm",
30+
"-internalize-public-api-list=alpha,beta",
31+
}
32+
if !reflect.DeepEqual(got, want) {
33+
t.Fatalf("ltoPublicAPIArgs() = %v, want %v", got, want)
34+
}
35+
}
36+
37+
func TestLTOPublicAPIArgsDisabledWithoutLTO(t *testing.T) {
38+
ctx := &context{
39+
crossCompile: crosscompile.Export{
40+
LDFLAGS: []string{"-Wl,--icf=safe"},
41+
},
42+
}
43+
if got := ltoPublicAPIArgs(ctx, []string{"alpha"}); got != nil {
44+
t.Fatalf("ltoPublicAPIArgs() = %v, want nil", got)
45+
}
46+
}

internal/build/plan9asm.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ func filterPlan9AsmFuncs(pkgPath, goos, goarch string, funcs []plan9asm.Func, re
254254
if pkgPath == "syscall" && goos == "linux" && (goarch == "arm64" || goarch == "amd64") && strings.HasSuffix(resolved, "rawVforkSyscall") {
255255
continue
256256
}
257+
if pkgPath == "syscall" && goos == "darwin" && (goarch == "arm64" || goarch == "amd64") &&
258+
(strings.HasSuffix(resolved, "RawSyscall") || strings.HasSuffix(resolved, "RawSyscall6")) {
259+
continue
260+
}
257261
keep = append(keep, fn)
258262
}
259263
return keep

internal/crosscompile/crosscompile.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,9 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
225225
"-Wl,--error-limit=0",
226226
"-fuse-ld=lld",
227227
// Enable ICF (Identical Code Folding) to reduce binary size
228-
"-Xlinker",
229-
"--icf=safe",
228+
"-Wl,--icf=safe",
229+
// Enable ThinLTO, Using default lto kind(thinlto).
230+
"-Wl,--lto-O0",
230231
}
231232
if clangRoot != "" {
232233
clangLib := filepath.Join(clangRoot, "lib")
@@ -249,6 +250,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
249250
export.CCFLAGS = []string{
250251
"-Qunused-arguments",
251252
"-Wno-unused-command-line-argument",
253+
"-flto=thin",
252254
}
253255

254256
// Add sysroot for macOS only
@@ -499,6 +501,13 @@ func UseTarget(targetName string) (export Export, err error) {
499501
expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs)
500502
cflags = append(cflags, expandedCFlags...)
501503

504+
if config.Linker == "ld.lld" {
505+
// Enable ThinLTO, Using default lto kind(thinlto).
506+
ldflags = append(ldflags, "--lto-O0")
507+
cflags = append(cflags, "-flto=thin")
508+
ccflags = append(ccflags, "-flto=thin")
509+
}
510+
502511
// The following parameters are inspired by tinygo/builder/library.go
503512
// Handle CPU configuration
504513
if cpu != "" {
@@ -545,6 +554,8 @@ func UseTarget(targetName string) (export Export, err error) {
545554
ccflags = append(ccflags, "-fforce-enable-int128")
546555
case "riscv64":
547556
ccflags = append(ccflags, "-march=rv64gc")
557+
// codegen option should be added to ldflags for lto
558+
ldflags = append(ldflags, "-mllvm", "-march=rv64gc")
548559
case "mips":
549560
ccflags = append(ccflags, "-fno-pic")
550561
}
@@ -574,9 +585,13 @@ func UseTarget(targetName string) (export Export, err error) {
574585
// Handle code generation configuration
575586
if config.CodeModel != "" {
576587
ccflags = append(ccflags, "-mcmodel="+config.CodeModel)
588+
// codegen option should be added to ldflags for lto
589+
ldflags = append(ldflags, "-mllvm", "-code-model="+config.CodeModel)
577590
}
578591
if config.TargetABI != "" {
579592
ccflags = append(ccflags, "-mabi="+config.TargetABI)
593+
// codegen option should be added to ldflags for lto
594+
ldflags = append(ldflags, "-mllvm", "-target-abi="+config.TargetABI)
580595
}
581596
if config.RelocationModel != "" {
582597
switch config.RelocationModel {

ssa/datastruct.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) {
342342
if lowIsNil {
343343
low = prog.IntVal(0, prog.Int())
344344
}
345+
low = b.fitIntSize(low)
346+
if !high.IsNil() {
347+
high = b.fitIntSize(high)
348+
}
345349
switch t := x.raw.Type.Underlying().(type) {
346350
case *types.Basic:
347351
if t.Kind() != types.String {
@@ -382,6 +386,7 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) {
382386
if max.IsNil() {
383387
max = nCap
384388
}
389+
max = b.fitIntSize(max)
385390
ret.impl = b.InlineCall(b.Pkg.rtFunc("NewSlice3"), base, nEltSize, nCap, low, high, max).impl
386391
return
387392
}

ssa/decl.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ func (p Package) NewFuncEx(name string, sig *types.Signature, bg Background, has
211211
if instantiated {
212212
fn.SetLinkage(llvm.LinkOnceAnyLinkage)
213213
}
214+
if p.Prog.requiresFramePointer() {
215+
attr := p.mod.Context().CreateStringAttribute("frame-pointer", "non-leaf")
216+
fn.AddFunctionAttr(attr)
217+
}
214218
ret := newFunction(fn, t, p, p.Prog, hasFreeVars)
215219
p.fns[name] = ret
216220
return ret
@@ -342,6 +346,10 @@ func (p Function) Block(idx int) BasicBlock {
342346
return p.blks[idx]
343347
}
344348

349+
func (p Program) requiresFramePointer() bool {
350+
return p.target != nil && p.target.GOOS == "darwin" && p.target.GOARCH == "arm64"
351+
}
352+
345353
// SetRecover sets the recover block for the function.
346354
func (p Function) SetRecover(blk BasicBlock) {
347355
p.recov = blk

ssa/ssa_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,37 @@ func TestClosureCtx(t *testing.T) {
145145
f.closureCtx(nil)
146146
}
147147

148+
149+
func TestNewFuncExFramePointerAttrOnDarwinArm64(t *testing.T) {
150+
prog := NewProgram(&Target{GOOS: "darwin", GOARCH: "arm64"})
151+
pkg := prog.NewPackage("main", "main")
152+
sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
153+
154+
fn := pkg.NewFunc("Foo", sig, InGo)
155+
attr := fn.impl.GetStringAttributeAtIndex(-1, "frame-pointer")
156+
if attr.IsNil() {
157+
t.Fatal("missing frame-pointer function attribute")
158+
}
159+
if got := pkg.String(); !strings.Contains(got, `"frame-pointer"="non-leaf"`) {
160+
t.Fatalf("module missing frame-pointer attribute:\n%s", got)
161+
}
162+
}
163+
164+
func TestNewFuncExFramePointerAttrNotForcedOnOtherTargets(t *testing.T) {
165+
prog := NewProgram(&Target{GOOS: "linux", GOARCH: "amd64"})
166+
pkg := prog.NewPackage("main", "main")
167+
sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
168+
169+
fn := pkg.NewFunc("Foo", sig, InGo)
170+
attr := fn.impl.GetStringAttributeAtIndex(-1, "frame-pointer")
171+
if !attr.IsNil() {
172+
t.Fatal("unexpected frame-pointer function attribute")
173+
}
174+
if got := pkg.String(); strings.Contains(got, `"frame-pointer"="non-leaf"`) {
175+
t.Fatalf("unexpected frame-pointer attribute in module:\n%s", got)
176+
}
177+
}
178+
148179
func TestClosureNoCtxValue(t *testing.T) {
149180
prog := NewProgram(nil)
150181
pkg := prog.NewPackage("bar", "foo/bar")

0 commit comments

Comments
 (0)