net: UDPConn.WriteTo and UDPConn.ReadFromUDP both allocate #43451
Comments
I have a vague impression of pointing this out to @FiloSottile 2 years ago but I don't remember the conclusion of our conversation. CCing in case he has a better recollection. |
I found some old notes. The conclusion from last I looked into this was that the API made it unavoidable. As a result I wound up making direct syscalls on Linux but didn't port that to all platforms. I wonder if this warrants adding a new API. |
Change https://golang.org/cl/280934 mentions this issue: |
IIUC, most sockaddrs will be re-used and they are never mutated. Given that, we could intern them. E.g. we could use a sync.Pool of maps to opportunistically re-use them. (This is the technique that https://github.com/josharian/intern uses for strings; it is fast and pretty good, but not perfect. There are other options, with their own trade-offs, like go4.org/intern.) I threw together CL 280934 to illustrate using the sync.Pool of maps approach. |
Ugh. Nope, that is not safe. We end up exposing the sockaddr memory to the caller in this line (udpsock_posix.go:50): addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port} Avoiding that would require making a copy of the We might still be able to intern the sockaddrs that are destined for the kernel at least. If we (a) switched to inet.af/netaddr's IP type and (b) returned a We could do both by letting people provide their own |
Evil idea: if the |
Hyrum's Law says no. (Plus the Go standard library generally doesn't go for such evil tricks, entertaining though they be.) |
Maybe a variant of that might be acceptable: Right now people pass in a buffer of the maximum size of data they want: data := make([]byte, 1472)
n, addr, err := conn.ReadFromUDP(data)
data = data[:n] My initial idea was to place the sockaddr allocations in the region of It occurred to me that other Go APIs sometimes work by taking slice to append to and then return a new slice. The reasoning goes that the caller can preallocate by allocating a slice with a large capacity but a zero length, and then the appending operation is free. What if we use a related trick here: data := make([]byte, 1472, 2000)
n, addr, err := conn.ReadFromUDP(data)
data = data[:n] In this instance, rather than placing addr at |
Mmm, looks like that can still lead to unexpected problems: package main
import (
"fmt"
)
func doTheAliasingTrick(slice []byte) *byte {
for i := range slice {
slice[i] = 41
}
return &append(slice, 42)[len(slice)]
}
func main() {
data := make([]byte, 1472, 2000)
x := doTheAliasingTrick(data)
fmt.Printf("*x = %d\n", *x) // Prints 42
_ = append(data, 43)
fmt.Printf("*x = %d\n", *x) // Prints 43
} |
I think you can outline that. It will require some care on the caller side, but if someone needs it they can make sure they don't get in the way of the inliner. |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes.
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
I'd like to be able to write a program that uses UDPConn.WriteTo and UDPConn.ReadFromUDP without allocating per-packet.
This benchmark indicates one alloc per WriteTo and two allocs per ReadFromUDP.
Two of the allocs come from constructing syscall.Sockaddrs. Maybe this is fixable, but I don't see an easy way.
The last alloc is from constructing a
*UDPAddr
to return fromReadFromUDP
. I fear the API may make this one unavoidable.cc @bradfitz @danderson @zx2c4
The text was updated successfully, but these errors were encountered: