Skip to content

Commit e1052f9

Browse files
aykevldeadprogram
authored andcommitted
compiler: define atomic intrinsic functions directly
This changes the compiler from treating calls to sync/atomic.* functions as special calls (emitted directly at the call site) to actually defining their declarations when there is no Go SSA implementation. And rely on the inliner to inline these very small functions. This works a bit better in practice. For example, this makes it possible to use these functions in deferred function calls. This commit is a bit large because it also needs to refactor a few things to make it possible to define such intrinsic functions.
1 parent 6dff85c commit e1052f9

File tree

5 files changed

+87
-38
lines changed

5 files changed

+87
-38
lines changed

compiler/atomic.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import (
44
"fmt"
55
"strings"
66

7-
"golang.org/x/tools/go/ssa"
87
"tinygo.org/x/go-llvm"
98
)
109

11-
// createAtomicOp lowers an atomic library call by lowering it as an LLVM atomic
12-
// operation. It returns the result of the operation and true if the call could
13-
// be lowered inline, and false otherwise.
14-
func (b *builder) createAtomicOp(call *ssa.CallCommon) (llvm.Value, bool) {
15-
name := call.Value.(*ssa.Function).Name()
10+
// createAtomicOp lowers a sync/atomic function by lowering it as an LLVM atomic
11+
// operation. It returns the result of the operation, or a zero llvm.Value if
12+
// the result is void.
13+
func (b *builder) createAtomicOp(name string) llvm.Value {
1614
switch name {
1715
case "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr":
18-
ptr := b.getValue(call.Args[0])
19-
val := b.getValue(call.Args[1])
16+
ptr := b.getValue(b.fn.Params[0])
17+
val := b.getValue(b.fn.Params[1])
2018
if strings.HasPrefix(b.Triple, "avr") {
2119
// AtomicRMW does not work on AVR as intended:
2220
// - There are some register allocation issues (fixed by https://reviews.llvm.org/D97127 which is not yet in a usable LLVM release)
@@ -29,41 +27,42 @@ func (b *builder) createAtomicOp(call *ssa.CallCommon) (llvm.Value, bool) {
2927
}
3028
oldVal := b.createCall(fn, []llvm.Value{ptr, val}, "")
3129
// Return the new value, not the original value returned.
32-
return b.CreateAdd(oldVal, val, ""), true
30+
return b.CreateAdd(oldVal, val, "")
3331
}
3432
oldVal := b.CreateAtomicRMW(llvm.AtomicRMWBinOpAdd, ptr, val, llvm.AtomicOrderingSequentiallyConsistent, true)
3533
// Return the new value, not the original value returned by atomicrmw.
36-
return b.CreateAdd(oldVal, val, ""), true
34+
return b.CreateAdd(oldVal, val, "")
3735
case "SwapInt32", "SwapInt64", "SwapUint32", "SwapUint64", "SwapUintptr", "SwapPointer":
38-
ptr := b.getValue(call.Args[0])
39-
val := b.getValue(call.Args[1])
36+
ptr := b.getValue(b.fn.Params[0])
37+
val := b.getValue(b.fn.Params[1])
4038
isPointer := val.Type().TypeKind() == llvm.PointerTypeKind
4139
if isPointer {
4240
// atomicrmw only supports integers, so cast to an integer.
41+
// TODO: this is fixed in LLVM 15.
4342
val = b.CreatePtrToInt(val, b.uintptrType, "")
4443
ptr = b.CreateBitCast(ptr, llvm.PointerType(val.Type(), 0), "")
4544
}
4645
oldVal := b.CreateAtomicRMW(llvm.AtomicRMWBinOpXchg, ptr, val, llvm.AtomicOrderingSequentiallyConsistent, true)
4746
if isPointer {
4847
oldVal = b.CreateIntToPtr(oldVal, b.i8ptrType, "")
4948
}
50-
return oldVal, true
49+
return oldVal
5150
case "CompareAndSwapInt32", "CompareAndSwapInt64", "CompareAndSwapUint32", "CompareAndSwapUint64", "CompareAndSwapUintptr", "CompareAndSwapPointer":
52-
ptr := b.getValue(call.Args[0])
53-
old := b.getValue(call.Args[1])
54-
newVal := b.getValue(call.Args[2])
51+
ptr := b.getValue(b.fn.Params[0])
52+
old := b.getValue(b.fn.Params[1])
53+
newVal := b.getValue(b.fn.Params[2])
5554
tuple := b.CreateAtomicCmpXchg(ptr, old, newVal, llvm.AtomicOrderingSequentiallyConsistent, llvm.AtomicOrderingSequentiallyConsistent, true)
5655
swapped := b.CreateExtractValue(tuple, 1, "")
57-
return swapped, true
56+
return swapped
5857
case "LoadInt32", "LoadInt64", "LoadUint32", "LoadUint64", "LoadUintptr", "LoadPointer":
59-
ptr := b.getValue(call.Args[0])
58+
ptr := b.getValue(b.fn.Params[0])
6059
val := b.CreateLoad(ptr, "")
6160
val.SetOrdering(llvm.AtomicOrderingSequentiallyConsistent)
6261
val.SetAlignment(b.targetData.PrefTypeAlignment(val.Type())) // required
63-
return val, true
62+
return val
6463
case "StoreInt32", "StoreInt64", "StoreUint32", "StoreUint64", "StoreUintptr", "StorePointer":
65-
ptr := b.getValue(call.Args[0])
66-
val := b.getValue(call.Args[1])
64+
ptr := b.getValue(b.fn.Params[0])
65+
val := b.getValue(b.fn.Params[1])
6766
if strings.HasPrefix(b.Triple, "avr") {
6867
// SelectionDAGBuilder is currently missing the "are unaligned atomics allowed" check for stores.
6968
vType := val.Type()
@@ -79,13 +78,15 @@ func (b *builder) createAtomicOp(call *ssa.CallCommon) (llvm.Value, bool) {
7978
if fn.IsNil() {
8079
fn = llvm.AddFunction(b.mod, name, llvm.FunctionType(vType, []llvm.Type{ptr.Type(), vType, b.uintptrType}, false))
8180
}
82-
return b.createCall(fn, []llvm.Value{ptr, val, llvm.ConstInt(b.uintptrType, 5, false)}, ""), true
81+
b.createCall(fn, []llvm.Value{ptr, val, llvm.ConstInt(b.uintptrType, 5, false)}, "")
82+
return llvm.Value{}
8383
}
8484
store := b.CreateStore(val, ptr)
8585
store.SetOrdering(llvm.AtomicOrderingSequentiallyConsistent)
8686
store.SetAlignment(b.targetData.PrefTypeAlignment(val.Type())) // required
87-
return store, true
87+
return llvm.Value{}
8888
default:
89-
return llvm.Value{}, false
89+
b.addError(b.fn.Pos(), "unknown atomic operation: "+b.fn.Name())
90+
return llvm.Value{}
9091
}
9192
}

compiler/compiler.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,12 @@ func (c *compilerContext) createPackage(irbuilder llvm.Builder, pkg *ssa.Package
785785
// Create the function definition.
786786
b := newBuilder(c, irbuilder, member)
787787
if member.Blocks == nil {
788-
continue // external function
788+
// Try to define this as an intrinsic function.
789+
b.defineIntrinsicFunction()
790+
// It might not be an intrinsic function but simply an external
791+
// function (defined via //go:linkname). Leave it undefined in
792+
// that case.
793+
continue
789794
}
790795
b.createFunction()
791796
case *ssa.Type:
@@ -1009,10 +1014,11 @@ func (c *compilerContext) getEmbedFileString(file *loader.EmbedFile) llvm.Value
10091014
return llvm.ConstNamedStruct(c.getLLVMRuntimeType("_string"), []llvm.Value{strPtr, strLen})
10101015
}
10111016

1012-
// createFunction builds the LLVM IR implementation for this function. The
1013-
// function must not yet be defined, otherwise this function will create a
1014-
// diagnostic.
1015-
func (b *builder) createFunction() {
1017+
// Start defining a function so that it can be filled with instructions: load
1018+
// parameters, create basic blocks, and set up debug information.
1019+
// This is separated out from createFunction() so that it is also usable to
1020+
// define compiler intrinsics like the atomic operations in sync/atomic.
1021+
func (b *builder) createFunctionStart() {
10161022
if b.DumpSSA {
10171023
fmt.Printf("\nfunc %s:\n", b.fn)
10181024
}
@@ -1082,7 +1088,16 @@ func (b *builder) createFunction() {
10821088
b.blockEntries[block] = llvmBlock
10831089
b.blockExits[block] = llvmBlock
10841090
}
1085-
entryBlock := b.blockEntries[b.fn.Blocks[0]]
1091+
var entryBlock llvm.BasicBlock
1092+
if len(b.fn.Blocks) != 0 {
1093+
// Normal functions have an entry block.
1094+
entryBlock = b.blockEntries[b.fn.Blocks[0]]
1095+
} else {
1096+
// This function isn't defined in Go SSA. It is probably a compiler
1097+
// intrinsic (like an atomic operation). Create the entry block
1098+
// manually.
1099+
entryBlock = b.ctx.AddBasicBlock(b.llvmFn, "entry")
1100+
}
10861101
b.SetInsertPointAtEnd(entryBlock)
10871102

10881103
if b.fn.Synthetic == "package initializer" {
@@ -1157,6 +1172,13 @@ func (b *builder) createFunction() {
11571172
// them.
11581173
b.deferInitFunc()
11591174
}
1175+
}
1176+
1177+
// createFunction builds the LLVM IR implementation for this function. The
1178+
// function must not yet be defined, otherwise this function will create a
1179+
// diagnostic.
1180+
func (b *builder) createFunction() {
1181+
b.createFunctionStart()
11601182

11611183
// Fill blocks with instructions.
11621184
for _, block := range b.fn.DomPreorder() {
@@ -1630,14 +1652,6 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error)
16301652
supportsRecover = 1
16311653
}
16321654
return llvm.ConstInt(b.ctx.Int1Type(), supportsRecover, false), nil
1633-
case strings.HasPrefix(name, "sync/atomic."):
1634-
val, ok := b.createAtomicOp(instr)
1635-
if ok {
1636-
// This call could be lowered as an atomic operation.
1637-
return val, nil
1638-
}
1639-
// This call couldn't be lowered as an atomic operation, it's
1640-
// probably something else. Continue as usual.
16411655
case name == "runtime/interrupt.New":
16421656
return b.createInterruptGlobal(instr)
16431657
}

compiler/intrinsics.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,34 @@ package compiler
33
// This file contains helper functions to create calls to LLVM intrinsics.
44

55
import (
6+
"go/token"
67
"strconv"
8+
"strings"
79

810
"golang.org/x/tools/go/ssa"
911
"tinygo.org/x/go-llvm"
1012
)
1113

14+
// Define unimplemented intrinsic functions.
15+
//
16+
// Some functions are either normally implemented in Go assembly (like
17+
// sync/atomic functions) or intentionally left undefined to be implemented
18+
// directly in the compiler (like runtime/volatile functions). Either way, look
19+
// for these and implement them if this is the case.
20+
func (b *builder) defineIntrinsicFunction() {
21+
name := b.fn.RelString(nil)
22+
switch {
23+
case strings.HasPrefix(name, "sync/atomic.") && token.IsExported(b.fn.Name()):
24+
b.createFunctionStart()
25+
returnValue := b.createAtomicOp(b.fn.Name())
26+
if !returnValue.IsNil() {
27+
b.CreateRet(returnValue)
28+
} else {
29+
b.CreateRetVoid()
30+
}
31+
}
32+
}
33+
1234
// createMemoryCopyCall creates a call to a builtin LLVM memcpy or memmove
1335
// function, declaring this function if needed. These calls are treated
1436
// specially by optimization passes possibly resulting in better generated code,

testdata/atomic.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ func main() {
8181
// test atomic.Value load/store operations
8282
testValue(int(3), int(-2))
8383
testValue("", "foobar", "baz")
84+
85+
// Test atomic operations as deferred values.
86+
testDefer()
8487
}
8588

8689
func testValue(values ...interface{}) {
@@ -93,3 +96,11 @@ func testValue(values ...interface{}) {
9396
}
9497
}
9598
}
99+
100+
func testDefer() {
101+
n1 := int32(5)
102+
defer func() {
103+
println("deferred atomic add:", n1)
104+
}()
105+
defer atomic.AddInt32(&n1, 3)
106+
}

testdata/atomic.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ StoreUint32: 20
3333
StoreUint64: 20
3434
StoreUintptr: 20
3535
StorePointer: true
36+
deferred atomic add: 8

0 commit comments

Comments
 (0)