Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compiler: Add go:noescape pragma for variables #3809

Open
soypat opened this issue Jun 24, 2023 · 6 comments
Open

compiler: Add go:noescape pragma for variables #3809

soypat opened this issue Jun 24, 2023 · 6 comments
Labels
core enhancement New feature or request

Comments

@soypat
Copy link
Contributor

soypat commented Jun 24, 2023

I'd like to be able to mark variables as non-escaping for cases where I'm using well defined interfaces which guarantee their arguments don't escape like the drivers.SPI and drivers.I2C interfaces.

Usage

In the below case we are sure that SPI implementation will not capture the buf slice. We tell the compiler that it's perfectly OK if buf is on the stack.

// Write32S writes register and swaps big-endian 16bit word length. Used only at initialization.
func (d *Device) Write32S(fn Function, addr, val uint32) error {
	cmd := make_cmd(true, true, fn, addr, 4)
        //go:noescape
	var buf [4]byte
	d.csLow()
	if sharedDATA {
		d.sharedSD.Configure(machine.PinConfig{Mode: machine.PinOutput})
	}
	binary.BigEndian.PutUint32(buf[:], swap32(cmd))
	return d.spi.Tx(buf[:], nil)
}

Incorrect usage

Example of where NOT to use go:escape. buf should certainly not stay on stack since it may be immediately be reused by the next function to be called after Uitoa. This is just for demonstration, I don't expect the compiler to realize this is bad usage.

// Uitoa converts val to a decimal string.
func Uitoa(val uint) []byte {
	if val == 0 { // avoid string allocation
		return "0"
	}
        //go:noescape
	var buf [20]byte
	i := len(buf) - 1
	for val >= 10 {
		q := val / 10
		buf[i] = byte('0' + val - q*10)
		i--
		val = q
	}
	// val < 10
	buf[i] = byte('0' + val)
	return buf[i:]
}
@soypat soypat added enhancement New feature or request core labels Jun 24, 2023
@dgryski
Copy link
Member

dgryski commented Jun 24, 2023

Seems like the real solution here is to be able to tag noescape to the methods in the interface, and then pass that along to the callers of the interface.

@aykevl
Copy link
Member

aykevl commented Jun 24, 2023

I'd like to be able to mark variables as non-escaping for cases where I'm using well defined interfaces which guarantee their arguments don't escape like the drivers.SPI and drivers.I2C interfaces.

Unfortunately, that's not really safe. Some other code could (in theory) capture the value in the parameter.

Instead, what is needed is to add this pragma to the implementation of that method call. So for example, in the machine package on the func (spi *SPI) Tx method. That will typically trickle down through the interface method call (at least, with the current design of method calls).

The LLVM attribute that corresponds to //go:noescape is nocapture.

@soypat
Copy link
Contributor Author

soypat commented Jun 24, 2023

tag noescape to the methods in the interface,

That would be ideal.

func (spi *SPI) Tx method. That will typically trickle down through the interface method call (at least, with the current design of method calls).

So does the compiler have knowledge of the underlying SPI/I2C type in a drivers/xxxx.Device type's field? In that case this issue is technically already solved.

Unrelated: Testing out //go:noescape and went down a rabbit hole resulting in #3810.

@aykevl
Copy link
Member

aykevl commented Jul 3, 2023

So does the compiler have knowledge of the underlying SPI/I2C type in a drivers/xxxx.Device type's field? In that case this issue is technically already solved.

No. Basically, the compiler currently converts a method call to a type switch and direct call, like this pseudocode:

func callTxMethod(itf any, w, r []byte) error {
    switch itf := itf.(type) {
    case *machine.SPI:
        return machine.SPI.Tx(itf, w, r)
    // other cases here
    default:
        runtime.nilPanic()
    }
}

If machine.SPI.Tx doesn't let the parameters escape, this information will then be propagated through the pseudo callTxMethod function so that the optimization pass that converts heap allocations to stack allocations will know that the interface method call doesn't let the parameters escape.
Of course, when machine.SPI.Tx does let the parameters escape or there is another type with the same signature, then the heap to stack transform will still fail.

@soypat
Copy link
Contributor Author

soypat commented Sep 5, 2023

Linking a related PR for future reference: #3887

@dgryski
Copy link
Member

dgryski commented Oct 21, 2024

Having interface methods and function pointers being able to be tagged as noescape would help with things escaping when passed as keys to hashmap get/set since those go through function pointers (and their pointers don't escape...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants