
This commit optimizes string literals and globals by setting the appropriate alignment and using a nil pointer in zero-length strings. - Setting the alignment for string values has a surprisingly large effect, up to around 2% in binary size. I suspect that LLVM will pick some default alignment for larger byte arrays if no alignment has been specified and forcing an alignment of 1 will pack all strings closer together. - Using nil for zero-length strings also has a positive effect, but I'm not sure why. Perhaps it makes some optimizations more trivial. - Always setting the alignment on globals improves code size slightly, probably for the same reasons setting the alignment of string literals improves code size. The effect is much smaller, however. This commit might have an effect on performance, but if it does this should be tested separately and such a large win in binary size should definitely not be ignored for small embedded systems.
452 строки
15 КиБ
Go
452 строки
15 КиБ
Go
package compiler
|
|
|
|
// This file manages symbols, that is, functions and globals. It reads their
|
|
// pragmas, determines the link name, etc.
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tinygo-org/tinygo/loader"
|
|
"golang.org/x/tools/go/ssa"
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// functionInfo contains some information about a function or method. In
|
|
// particular, it contains information obtained from pragmas.
|
|
//
|
|
// The linkName value contains a valid link name, even if //go:linkname is not
|
|
// present.
|
|
type functionInfo struct {
|
|
module string // go:wasm-module
|
|
importName string // go:linkname, go:export - The name the developer assigns
|
|
linkName string // go:linkname, go:export - The name that we map for the particular module -> importName
|
|
exported bool // go:export, CGo
|
|
nobounds bool // go:nobounds
|
|
variadic bool // go:variadic (CGo only)
|
|
inline inlineType // go:inline
|
|
}
|
|
|
|
type inlineType int
|
|
|
|
// How much to inline.
|
|
const (
|
|
// Default behavior. The compiler decides for itself whether any given
|
|
// function will be inlined. Whether any function is inlined depends on the
|
|
// optimization level.
|
|
inlineDefault inlineType = iota
|
|
|
|
// Inline hint, just like the C inline keyword (signalled using
|
|
// //go:inline). The compiler will be more likely to inline this function,
|
|
// but it is not a guarantee.
|
|
inlineHint
|
|
|
|
// Don't inline, just like the GCC noinline attribute. Signalled using
|
|
// //go:noinline.
|
|
inlineNone
|
|
)
|
|
|
|
// getFunction returns the LLVM function for the given *ssa.Function, creating
|
|
// it if needed. It can later be filled with compilerContext.createFunction().
|
|
func (c *compilerContext) getFunction(fn *ssa.Function) llvm.Value {
|
|
info := c.getFunctionInfo(fn)
|
|
llvmFn := c.mod.NamedFunction(info.linkName)
|
|
if !llvmFn.IsNil() {
|
|
return llvmFn
|
|
}
|
|
|
|
var retType llvm.Type
|
|
if fn.Signature.Results() == nil {
|
|
retType = c.ctx.VoidType()
|
|
} else if fn.Signature.Results().Len() == 1 {
|
|
retType = c.getLLVMType(fn.Signature.Results().At(0).Type())
|
|
} else {
|
|
results := make([]llvm.Type, 0, fn.Signature.Results().Len())
|
|
for i := 0; i < fn.Signature.Results().Len(); i++ {
|
|
results = append(results, c.getLLVMType(fn.Signature.Results().At(i).Type()))
|
|
}
|
|
retType = c.ctx.StructType(results, false)
|
|
}
|
|
|
|
var paramInfos []paramInfo
|
|
for _, param := range getParams(fn.Signature) {
|
|
paramType := c.getLLVMType(param.Type())
|
|
paramFragmentInfos := c.expandFormalParamType(paramType, param.Name(), param.Type())
|
|
paramInfos = append(paramInfos, paramFragmentInfos...)
|
|
}
|
|
|
|
// Add an extra parameter as the function context. This context is used in
|
|
// closures and bound methods, but should be optimized away when not used.
|
|
if !info.exported {
|
|
paramInfos = append(paramInfos, paramInfo{llvmType: c.i8ptrType, name: "context", flags: 0})
|
|
paramInfos = append(paramInfos, paramInfo{llvmType: c.i8ptrType, name: "parentHandle", flags: 0})
|
|
}
|
|
|
|
var paramTypes []llvm.Type
|
|
for _, info := range paramInfos {
|
|
paramTypes = append(paramTypes, info.llvmType)
|
|
}
|
|
|
|
fnType := llvm.FunctionType(retType, paramTypes, info.variadic)
|
|
llvmFn = llvm.AddFunction(c.mod, info.linkName, fnType)
|
|
if strings.HasPrefix(c.Triple, "wasm") {
|
|
// C functions without prototypes like this:
|
|
// void foo();
|
|
// are actually variadic functions. However, it appears that it has been
|
|
// decided in WebAssembly that such prototype-less functions are not
|
|
// allowed in WebAssembly.
|
|
// In C, this can only happen when there are zero parameters, hence this
|
|
// check here. For more information:
|
|
// https://reviews.llvm.org/D48443
|
|
// https://github.com/WebAssembly/tool-conventions/issues/16
|
|
if info.variadic && len(fn.Params) == 0 {
|
|
attr := c.ctx.CreateStringAttribute("no-prototype", "")
|
|
llvmFn.AddFunctionAttr(attr)
|
|
}
|
|
}
|
|
|
|
dereferenceableOrNullKind := llvm.AttributeKindID("dereferenceable_or_null")
|
|
for i, info := range paramInfos {
|
|
if info.flags¶mIsDeferenceableOrNull == 0 {
|
|
continue
|
|
}
|
|
if info.llvmType.TypeKind() == llvm.PointerTypeKind {
|
|
el := info.llvmType.ElementType()
|
|
size := c.targetData.TypeAllocSize(el)
|
|
if size == 0 {
|
|
// dereferenceable_or_null(0) appears to be illegal in LLVM.
|
|
continue
|
|
}
|
|
dereferenceableOrNull := c.ctx.CreateEnumAttribute(dereferenceableOrNullKind, size)
|
|
llvmFn.AddAttributeAtIndex(i+1, dereferenceableOrNull)
|
|
}
|
|
}
|
|
|
|
// Set a number of function or parameter attributes, depending on the
|
|
// function. These functions are runtime functions that are known to have
|
|
// certain attributes that might not be inferred by the compiler.
|
|
switch info.linkName {
|
|
case "abort":
|
|
// On *nix systems, the "abort" functuion in libc is used to handle fatal panics.
|
|
// Mark it as noreturn so LLVM can optimize away code.
|
|
llvmFn.AddFunctionAttr(c.ctx.CreateEnumAttribute(llvm.AttributeKindID("noreturn"), 0))
|
|
case "runtime.alloc":
|
|
// Tell the optimizer that runtime.alloc is an allocator, meaning that it
|
|
// returns values that are never null and never alias to an existing value.
|
|
for _, attrName := range []string{"noalias", "nonnull"} {
|
|
llvmFn.AddAttributeAtIndex(0, c.ctx.CreateEnumAttribute(llvm.AttributeKindID(attrName), 0))
|
|
}
|
|
case "runtime.trackPointer":
|
|
// This function is necessary for tracking pointers on the stack in a
|
|
// portable way (see gc_stack_portable.go). Indicate to the optimizer
|
|
// that the only thing we'll do is read the pointer.
|
|
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0))
|
|
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("readonly"), 0))
|
|
}
|
|
|
|
// External/exported functions may not retain pointer values.
|
|
// https://golang.org/cmd/cgo/#hdr-Passing_pointers
|
|
if info.exported {
|
|
// Set the wasm-import-module attribute if the function's module is set.
|
|
if info.module != "" {
|
|
|
|
// We need to add the wasm-import-module and the wasm-import-name
|
|
wasmImportModuleAttr := c.ctx.CreateStringAttribute("wasm-import-module", info.module)
|
|
llvmFn.AddFunctionAttr(wasmImportModuleAttr)
|
|
|
|
// Add the Wasm Import Name, if we are a named wasm import
|
|
if info.importName != "" {
|
|
wasmImportNameAttr := c.ctx.CreateStringAttribute("wasm-import-name", info.importName)
|
|
llvmFn.AddFunctionAttr(wasmImportNameAttr)
|
|
}
|
|
}
|
|
nocaptureKind := llvm.AttributeKindID("nocapture")
|
|
nocapture := c.ctx.CreateEnumAttribute(nocaptureKind, 0)
|
|
for i, typ := range paramTypes {
|
|
if typ.TypeKind() == llvm.PointerTypeKind {
|
|
llvmFn.AddAttributeAtIndex(i+1, nocapture)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Synthetic functions are functions that do not appear in the source code,
|
|
// they are artificially constructed. Usually they are wrapper functions
|
|
// that are not referenced anywhere except in a SSA call instruction so
|
|
// should be created right away.
|
|
// The exception is the package initializer, which does appear in the
|
|
// *ssa.Package members and so shouldn't be created here.
|
|
if fn.Synthetic != "" && fn.Synthetic != "package initializer" {
|
|
irbuilder := c.ctx.NewBuilder()
|
|
b := newBuilder(c, irbuilder, fn)
|
|
b.createFunction()
|
|
irbuilder.Dispose()
|
|
llvmFn.SetLinkage(llvm.LinkOnceODRLinkage)
|
|
llvmFn.SetUnnamedAddr(true)
|
|
}
|
|
|
|
return llvmFn
|
|
}
|
|
|
|
// getFunctionInfo returns information about a function that is not directly
|
|
// present in *ssa.Function, such as the link name and whether it should be
|
|
// exported.
|
|
func (c *compilerContext) getFunctionInfo(f *ssa.Function) functionInfo {
|
|
info := functionInfo{}
|
|
if strings.HasPrefix(f.Name(), "C.") {
|
|
// Created by CGo: such a name cannot be created by regular C code.
|
|
info.linkName = f.Name()[2:]
|
|
info.exported = true
|
|
} else {
|
|
// Pick the default linkName.
|
|
info.linkName = f.RelString(nil)
|
|
}
|
|
// Check for //go: pragmas, which may change the link name (among others).
|
|
info.parsePragmas(f)
|
|
return info
|
|
}
|
|
|
|
// parsePragmas is used by getFunctionInfo to parse function pragmas such as
|
|
// //export or //go:noinline.
|
|
func (info *functionInfo) parsePragmas(f *ssa.Function) {
|
|
if f.Syntax() == nil {
|
|
return
|
|
}
|
|
if decl, ok := f.Syntax().(*ast.FuncDecl); ok && decl.Doc != nil {
|
|
|
|
// Our importName for a wasm module (if we are compiling to wasm), or llvm link name
|
|
var importName string
|
|
|
|
for _, comment := range decl.Doc.List {
|
|
text := comment.Text
|
|
if strings.HasPrefix(text, "//export ") {
|
|
// Rewrite '//export' to '//go:export' for compatibility with
|
|
// gc.
|
|
text = "//go:" + text[2:]
|
|
}
|
|
if !strings.HasPrefix(text, "//go:") {
|
|
continue
|
|
}
|
|
parts := strings.Fields(text)
|
|
switch parts[0] {
|
|
case "//go:export":
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
importName = parts[1]
|
|
info.exported = true
|
|
case "//go:wasm-module":
|
|
// Alternative comment for setting the import module.
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
info.module = parts[1]
|
|
case "//go:inline":
|
|
info.inline = inlineHint
|
|
case "//go:noinline":
|
|
info.inline = inlineNone
|
|
case "//go:linkname":
|
|
if len(parts) != 3 || parts[1] != f.Name() {
|
|
continue
|
|
}
|
|
// Only enable go:linkname when the package imports "unsafe".
|
|
// This is a slightly looser requirement than what gc uses: gc
|
|
// requires the file to import "unsafe", not the package as a
|
|
// whole.
|
|
if hasUnsafeImport(f.Pkg.Pkg) {
|
|
info.linkName = parts[2]
|
|
}
|
|
case "//go:nobounds":
|
|
// Skip bounds checking in this function. Useful for some
|
|
// runtime functions.
|
|
// This is somewhat dangerous and thus only imported in packages
|
|
// that import unsafe.
|
|
if hasUnsafeImport(f.Pkg.Pkg) {
|
|
info.nobounds = true
|
|
}
|
|
case "//go:variadic":
|
|
// The //go:variadic pragma is emitted by the CGo preprocessing
|
|
// pass for C variadic functions. This includes both explicit
|
|
// (with ...) and implicit (no parameters in signature)
|
|
// functions.
|
|
if strings.HasPrefix(f.Name(), "C.") {
|
|
// This prefix cannot naturally be created, it must have
|
|
// been created as a result of CGo preprocessing.
|
|
info.variadic = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the importName for our exported function if we have one
|
|
if importName != "" {
|
|
if info.module == "" {
|
|
info.linkName = importName
|
|
} else {
|
|
// WebAssembly import
|
|
info.importName = importName
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// getParams returns the function parameters, including the receiver at the
|
|
// start. This is an alternative to the Params member of *ssa.Function, which is
|
|
// not yet populated when the package has not yet been built.
|
|
func getParams(sig *types.Signature) []*types.Var {
|
|
params := []*types.Var{}
|
|
if sig.Recv() != nil {
|
|
params = append(params, sig.Recv())
|
|
}
|
|
for i := 0; i < sig.Params().Len(); i++ {
|
|
params = append(params, sig.Params().At(i))
|
|
}
|
|
return params
|
|
}
|
|
|
|
// globalInfo contains some information about a specific global. By default,
|
|
// linkName is equal to .RelString(nil) on a global and extern is false, but for
|
|
// some symbols this is different (due to //go:extern for example).
|
|
type globalInfo struct {
|
|
linkName string // go:extern
|
|
extern bool // go:extern
|
|
align int // go:align
|
|
}
|
|
|
|
// loadASTComments loads comments on globals from the AST, for use later in the
|
|
// program. In particular, they are required for //go:extern pragmas on globals.
|
|
func (c *compilerContext) loadASTComments(pkg *loader.Package) {
|
|
for _, file := range pkg.Files {
|
|
for _, decl := range file.Decls {
|
|
switch decl := decl.(type) {
|
|
case *ast.GenDecl:
|
|
switch decl.Tok {
|
|
case token.VAR:
|
|
if len(decl.Specs) != 1 {
|
|
continue
|
|
}
|
|
for _, spec := range decl.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.ValueSpec: // decl.Tok == token.VAR
|
|
for _, name := range spec.Names {
|
|
id := pkg.Pkg.Path() + "." + name.Name
|
|
c.astComments[id] = decl.Doc
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// getGlobal returns a LLVM IR global value for a Go SSA global. It is added to
|
|
// the LLVM IR if it has not been added already.
|
|
func (c *compilerContext) getGlobal(g *ssa.Global) llvm.Value {
|
|
info := c.getGlobalInfo(g)
|
|
llvmGlobal := c.mod.NamedGlobal(info.linkName)
|
|
if llvmGlobal.IsNil() {
|
|
typ := g.Type().(*types.Pointer).Elem()
|
|
llvmType := c.getLLVMType(typ)
|
|
llvmGlobal = llvm.AddGlobal(c.mod, llvmType, info.linkName)
|
|
|
|
// Set alignment from the //go:align comment.
|
|
var alignInBits uint32
|
|
alignment := c.targetData.ABITypeAlignment(llvmType)
|
|
if info.align > alignment {
|
|
alignment = info.align
|
|
}
|
|
if alignment <= 0 || alignment&(alignment-1) != 0 {
|
|
// Check for power-of-two (or 0).
|
|
// See: https://stackoverflow.com/a/108360
|
|
c.addError(g.Pos(), "global variable alignment must be a positive power of two")
|
|
} else {
|
|
// Set the alignment only when it is a power of two.
|
|
alignInBits = uint32(alignment) ^ uint32(alignment-1)
|
|
llvmGlobal.SetAlignment(alignment)
|
|
}
|
|
|
|
if c.Debug && !info.extern {
|
|
// Add debug info.
|
|
pos := c.program.Fset.Position(g.Pos())
|
|
diglobal := c.dibuilder.CreateGlobalVariableExpression(c.difiles[pos.Filename], llvm.DIGlobalVariableExpression{
|
|
Name: g.RelString(nil),
|
|
LinkageName: info.linkName,
|
|
File: c.getDIFile(pos.Filename),
|
|
Line: pos.Line,
|
|
Type: c.getDIType(typ),
|
|
LocalToUnit: false,
|
|
Expr: c.dibuilder.CreateExpression(nil),
|
|
AlignInBits: alignInBits,
|
|
})
|
|
llvmGlobal.AddMetadata(0, diglobal)
|
|
}
|
|
}
|
|
return llvmGlobal
|
|
}
|
|
|
|
// getGlobalInfo returns some information about a specific global.
|
|
func (c *compilerContext) getGlobalInfo(g *ssa.Global) globalInfo {
|
|
info := globalInfo{}
|
|
if strings.HasPrefix(g.Name(), "C.") {
|
|
// Created by CGo: such a name cannot be created by regular C code.
|
|
info.linkName = g.Name()[2:]
|
|
info.extern = true
|
|
} else {
|
|
// Pick the default linkName.
|
|
info.linkName = g.RelString(nil)
|
|
// Check for //go: pragmas, which may change the link name (among
|
|
// others).
|
|
doc := c.astComments[info.linkName]
|
|
if doc != nil {
|
|
info.parsePragmas(doc)
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
// Parse //go: pragma comments from the source. In particular, it parses the
|
|
// //go:extern pragma on globals.
|
|
func (info *globalInfo) parsePragmas(doc *ast.CommentGroup) {
|
|
for _, comment := range doc.List {
|
|
if !strings.HasPrefix(comment.Text, "//go:") {
|
|
continue
|
|
}
|
|
parts := strings.Fields(comment.Text)
|
|
switch parts[0] {
|
|
case "//go:extern":
|
|
info.extern = true
|
|
if len(parts) == 2 {
|
|
info.linkName = parts[1]
|
|
}
|
|
case "//go:align":
|
|
align, err := strconv.Atoi(parts[1])
|
|
if err == nil {
|
|
info.align = align
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get all methods of a type.
|
|
func getAllMethods(prog *ssa.Program, typ types.Type) []*types.Selection {
|
|
ms := prog.MethodSets.MethodSet(typ)
|
|
methods := make([]*types.Selection, ms.Len())
|
|
for i := 0; i < ms.Len(); i++ {
|
|
methods[i] = ms.At(i)
|
|
}
|
|
return methods
|
|
}
|
|
|
|
// Return true if this package imports "unsafe", false otherwise.
|
|
func hasUnsafeImport(pkg *types.Package) bool {
|
|
for _, imp := range pkg.Imports() {
|
|
if imp == types.Unsafe {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|