Skip to content

Commit a411cf7

Browse files
committed
machine: make sure DMA buffers do not escape unnecessarily
Writing the pointer of a buffer to memory-mapped I/O will normally cause it to escape, which forces the compiler to heap-allocate the buffer. But we do know how long the value stays alive, so we can tell the compiler to keep it alive exactly until it is not needed anymore - and tell it to not treat the pointer-to-uintptr cast as escaping.
1 parent 073862e commit a411cf7

File tree

7 files changed

+127
-17
lines changed

7 files changed

+127
-17
lines changed

compiler/intrinsics.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func (b *builder) defineIntrinsicFunction() {
2727
b.createStackSaveImpl()
2828
case name == "runtime.KeepAlive":
2929
b.createKeepAliveImpl()
30+
case name == "machine.keepAliveNoEscape":
31+
b.createMachineKeepAliveImpl()
3032
case strings.HasPrefix(name, "runtime/volatile.Load"):
3133
b.createVolatileLoad()
3234
case strings.HasPrefix(name, "runtime/volatile.Store"):
@@ -144,6 +146,20 @@ func (b *builder) createAbiEscapeImpl() {
144146
b.CreateRet(result)
145147
}
146148

149+
// Implement machine.keepAliveNoEscape, which makes sure the compiler keeps the
150+
// pointer parameter alive until this point (for GC).
151+
func (b *builder) createMachineKeepAliveImpl() {
152+
b.createFunctionStart(true)
153+
pointerValue := b.getValue(b.fn.Params[0], getPos(b.fn))
154+
155+
// See createKeepAliveImpl for details.
156+
asmType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.dataPtrType}, false)
157+
asmFn := llvm.InlineAsm(asmType, "", "r", true, false, 0, false)
158+
b.createCall(asmType, asmFn, []llvm.Value{pointerValue}, "")
159+
160+
b.CreateRetVoid()
161+
}
162+
147163
var mathToLLVMMapping = map[string]string{
148164
"math.Ceil": "llvm.ceil.f64",
149165
"math.Exp": "llvm.exp.f64",

compiler/symbol.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value)
154154
llvmFn.AddFunctionAttr(c.ctx.CreateEnumAttribute(llvm.AttributeKindID("noreturn"), 0))
155155
case "internal/abi.NoEscape":
156156
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0))
157+
case "machine.keepAliveNoEscape", "machine.unsafeNoEscape":
158+
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0))
157159
case "runtime.alloc":
158160
// Tell the optimizer that runtime.alloc is an allocator, meaning that it
159161
// returns values that are never null and never alias to an existing value.

src/machine/machine.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package machine
22

3-
import "errors"
3+
import (
4+
"errors"
5+
"unsafe"
6+
)
47

58
var (
69
ErrTimeoutRNG = errors.New("machine: RNG Timeout")
@@ -62,3 +65,30 @@ func (p Pin) Low() {
6265
type ADC struct {
6366
Pin Pin
6467
}
68+
69+
// Convert the pointer to a uintptr, to be used for memory I/O (DMA for
70+
// example). It also means the pointer is "gone" as far as the compiler is
71+
// concerned, and a GC cycle might deallocate the object. To prevent this from
72+
// happening, also call keepAliveNoEscape at a point after the address isn't
73+
// accessed anymore by the hardware.
74+
// The only exception is if the pointer is accessed later in a volatile way
75+
// (volatile read/write), which also forces the value to stay alive until that
76+
// point.
77+
//
78+
// This function is treated specially by the compiler to mark the 'ptr'
79+
// parameter as not escaping.
80+
//
81+
// TODO: this function should eventually be replaced with the proposed ptrtoaddr
82+
// instruction in LLVM. See:
83+
// https://discourse.llvm.org/t/clarifiying-the-semantics-of-ptrtoint/83987/10
84+
// https://github.com/llvm/llvm-project/pull/139357
85+
func unsafeNoEscape(ptr unsafe.Pointer) uintptr {
86+
return uintptr(ptr)
87+
}
88+
89+
// Make sure the given pointer stays alive until this point. This is similar to
90+
// runtime.KeepAlive, with the difference that it won't let the pointer escape.
91+
// This is typically used together with unsafeNoEscape.
92+
//
93+
// This is a compiler intrinsic.
94+
func keepAliveNoEscape(ptr unsafe.Pointer)

src/machine/machine_nrf528xx.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) {
4949

5050
// Configure for a single shot to perform both write and read (as applicable)
5151
if len(w) != 0 {
52-
i2c.Bus.TXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&w[0]))))
52+
i2c.Bus.TXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&w[0]))))
5353
i2c.Bus.TXD.MAXCNT.Set(uint32(len(w)))
5454

5555
// If no read, immediately signal stop after TX
@@ -58,7 +58,7 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) {
5858
}
5959
}
6060
if len(r) != 0 {
61-
i2c.Bus.RXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&r[0]))))
61+
i2c.Bus.RXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&r[0]))))
6262
i2c.Bus.RXD.MAXCNT.Set(uint32(len(r)))
6363

6464
// Auto-start Rx after Tx and Stop after Rx
@@ -89,6 +89,15 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) {
8989
}
9090
}
9191

92+
// Make sure the w and r buffers stay alive until this point, so they won't
93+
// be garbage collected while the buffers are used by the hardware.
94+
if len(w) > 0 {
95+
keepAliveNoEscape(unsafe.Pointer(&w[0]))
96+
}
97+
if len(r) > 0 {
98+
keepAliveNoEscape(unsafe.Pointer(&r[0]))
99+
}
100+
92101
return
93102
}
94103

@@ -117,7 +126,7 @@ func (i2c *I2C) Listen(addr uint8) error {
117126
//
118127
// For request events, the caller MUST call `Reply` to avoid hanging the i2c bus indefinitely.
119128
func (i2c *I2C) WaitForEvent(buf []byte) (evt I2CTargetEvent, count int, err error) {
120-
i2c.BusT.RXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&buf[0]))))
129+
i2c.BusT.RXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&buf[0]))))
121130
i2c.BusT.RXD.MAXCNT.Set(uint32(len(buf)))
122131

123132
i2c.BusT.TASKS_PREPARERX.Set(nrf.TWIS_TASKS_PREPARERX_TASKS_PREPARERX_Trigger)
@@ -134,6 +143,10 @@ func (i2c *I2C) WaitForEvent(buf []byte) (evt I2CTargetEvent, count int, err err
134143
}
135144
}
136145

146+
// Make sure buf stays alive until this point, so it won't be garbage
147+
// collected while it is used by the hardware.
148+
keepAliveNoEscape(unsafe.Pointer(&buf[0]))
149+
137150
count = 0
138151
evt = I2CFinish
139152
err = nil
@@ -163,7 +176,7 @@ func (i2c *I2C) WaitForEvent(buf []byte) (evt I2CTargetEvent, count int, err err
163176

164177
// Reply supplies the response data the controller.
165178
func (i2c *I2C) Reply(buf []byte) error {
166-
i2c.BusT.TXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&buf[0]))))
179+
i2c.BusT.TXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&buf[0]))))
167180
i2c.BusT.TXD.MAXCNT.Set(uint32(len(buf)))
168181

169182
i2c.BusT.EVENTS_STOPPED.Set(0)
@@ -180,6 +193,10 @@ func (i2c *I2C) Reply(buf []byte) error {
180193
}
181194
}
182195

196+
// Make sure the buffer stays alive until this point, so it won't be garbage
197+
// collected while it is used by the hardware.
198+
keepAliveNoEscape(unsafe.Pointer(&buf[0]))
199+
183200
i2c.BusT.EVENTS_STOPPED.Set(0)
184201

185202
return nil

src/machine/machine_nrf52xxx.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ func (a *ADC) Get() uint16 {
145145
nrf.SAADC.CH[0].PSELP.Set(pwmPin)
146146

147147
// Destination for sample result.
148-
nrf.SAADC.RESULT.PTR.Set(uint32(uintptr(unsafe.Pointer(&rawValue))))
148+
// Note: rawValue doesn't need to be kept alive for the GC, since the
149+
// volatile read later will force it to stay alive.
150+
nrf.SAADC.RESULT.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&rawValue))))
149151
nrf.SAADC.RESULT.MAXCNT.Set(1) // One sample
150152

151153
// Start tasks.
@@ -314,7 +316,7 @@ func (spi *SPI) Tx(w, r []byte) error {
314316
if nr > 255 {
315317
nr = 255
316318
}
317-
spi.Bus.RXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&r[0]))))
319+
spi.Bus.RXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&r[0]))))
318320
r = r[nr:]
319321
}
320322
spi.Bus.RXD.MAXCNT.Set(nr)
@@ -325,7 +327,7 @@ func (spi *SPI) Tx(w, r []byte) error {
325327
if nw > 255 {
326328
nw = 255
327329
}
328-
spi.Bus.TXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&w[0]))))
330+
spi.Bus.TXD.PTR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&w[0]))))
329331
w = w[nw:]
330332
}
331333
spi.Bus.TXD.MAXCNT.Set(nw)
@@ -339,6 +341,15 @@ func (spi *SPI) Tx(w, r []byte) error {
339341
spi.Bus.EVENTS_END.Set(0)
340342
}
341343

344+
// Make sure the w and r buffers stay alive for the GC until this point,
345+
// since they are used by the hardware but not otherwise visible.
346+
if len(r) != 0 {
347+
keepAliveNoEscape(unsafe.Pointer(&r[0]))
348+
}
349+
if len(w) != 0 {
350+
keepAliveNoEscape(unsafe.Pointer(&w[0]))
351+
}
352+
342353
return nil
343354
}
344355

src/machine/machine_rp2_spi.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (spi *SPI) tx(tx []byte) error {
309309
// - set data size to single bytes
310310
// - set the DREQ so that the DMA will fill the SPI FIFO as needed
311311
// - start the transfer
312-
ch.READ_ADDR.Set(uint32(uintptr(unsafe.Pointer(&tx[0]))))
312+
ch.READ_ADDR.Set(uint32(unsafeNoEscape(unsafe.Pointer(&tx[0]))))
313313
ch.WRITE_ADDR.Set(uint32(uintptr(unsafe.Pointer(&spi.Bus.SSPDR))))
314314
ch.TRANS_COUNT.Set(uint32(len(tx)))
315315
ch.CTRL_TRIG.Set(rp.DMA_CH0_CTRL_TRIG_INCR_READ |
@@ -328,6 +328,11 @@ func (spi *SPI) tx(tx []byte) error {
328328
for ch.CTRL_TRIG.Get()&rp.DMA_CH0_CTRL_TRIG_BUSY != 0 {
329329
}
330330

331+
// Make sure the read buffer stays alive until this point (in the unlikely
332+
// case the tx slice wasn't read after this function returns and a GC cycle
333+
// happened inbetween).
334+
keepAliveNoEscape(unsafe.Pointer(&tx[0]))
335+
331336
// We didn't read any result values, which means the RX FIFO has likely
332337
// overflown. We have to clean up this mess now.
333338

transform/testdata/allocs2.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package main
22

3+
import (
4+
"runtime/volatile"
5+
"unsafe"
6+
_ "unsafe"
7+
)
8+
39
func main() {
410
n1 := 5
511
derefInt(&n1)
612

713
// This should eventually be modified to not escape.
8-
n2 := 6 // OUT: object allocated on the heap: escapes at line 9
14+
n2 := 6 // OUT: object allocated on the heap: escapes at line 15
915
returnIntPtr(&n2)
1016

1117
s1 := make([]int, 3)
@@ -15,36 +21,36 @@ func main() {
1521
readIntSlice(s2[:])
1622

1723
// This should also be modified to not escape.
18-
s3 := make([]int, 3) // OUT: object allocated on the heap: escapes at line 19
24+
s3 := make([]int, 3) // OUT: object allocated on the heap: escapes at line 25
1925
returnIntSlice(s3)
2026

2127
useSlice(make([]int, getUnknownNumber())) // OUT: object allocated on the heap: size is not constant
2228

2329
s4 := make([]byte, 300) // OUT: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256
2430
readByteSlice(s4)
2531

26-
s5 := make([]int, 4) // OUT: object allocated on the heap: escapes at line 27
32+
s5 := make([]int, 4) // OUT: object allocated on the heap: escapes at line 33
2733
_ = append(s5, 5)
2834

2935
s6 := make([]int, 3)
3036
s7 := []int{1, 2, 3}
3137
copySlice(s6, s7)
3238

33-
c1 := getComplex128() // OUT: object allocated on the heap: escapes at line 34
39+
c1 := getComplex128() // OUT: object allocated on the heap: escapes at line 40
3440
useInterface(c1)
3541

3642
n3 := 5
3743
func() int {
3844
return n3
3945
}()
4046

41-
callVariadic(3, 5, 8) // OUT: object allocated on the heap: escapes at line 41
47+
callVariadic(3, 5, 8) // OUT: object allocated on the heap: escapes at line 47
4248

43-
s8 := []int{3, 5, 8} // OUT: object allocated on the heap: escapes at line 44
49+
s8 := []int{3, 5, 8} // OUT: object allocated on the heap: escapes at line 50
4450
callVariadic(s8...)
4551

46-
n4 := 3 // OUT: object allocated on the heap: escapes at line 48
47-
n5 := 7 // OUT: object allocated on the heap: escapes at line 48
52+
n4 := 3 // OUT: object allocated on the heap: escapes at line 54
53+
n5 := 7 // OUT: object allocated on the heap: escapes at line 54
4854
func() {
4955
n4 = n5
5056
}()
@@ -58,6 +64,19 @@ func main() {
5864
var rbuf [5]rune
5965
s = string(rbuf[:])
6066
println(s)
67+
68+
// Unsafe usage of DMA buffers: the compiler thinks this buffer won't be
69+
// used anymore after the volatile store.
70+
var dmaBuf1 [4]byte
71+
pseudoVolatile.Set(uint32(unsafeNoEscape(unsafe.Pointer(&dmaBuf1[0]))))
72+
73+
// Safe usage of DMA buffers: keep the buffer alive until it is no longer
74+
// needed, but don't mark it as needing to be heap allocated. The compiler
75+
// will keep the buffer stack allocated if possible.
76+
var dmaBuf2 [4]byte
77+
pseudoVolatile.Set(uint32(unsafeNoEscape(unsafe.Pointer(&dmaBuf2[0]))))
78+
// ...use the buffer in the DMA peripheral
79+
keepAliveNoEscape(unsafe.Pointer(&dmaBuf2[0]))
6180
}
6281

6382
func derefInt(x *int) int {
@@ -93,3 +112,13 @@ func useInterface(interface{})
93112
func callVariadic(...int)
94113

95114
func useSlice([]int)
115+
116+
// See the function with the same name in the machine package.
117+
//
118+
//go:linkname unsafeNoEscape machine.unsafeNoEscape
119+
func unsafeNoEscape(ptr unsafe.Pointer) uintptr
120+
121+
//go:linkname keepAliveNoEscape machine.keepAliveNoEscape
122+
func keepAliveNoEscape(ptr unsafe.Pointer)
123+
124+
var pseudoVolatile volatile.Register32

0 commit comments

Comments
 (0)