Take ownership of buffer in QueuePacketConn QueueIncoming/WriteTo.

This design is easier to misuse, because it allows the caller to modify
the contents of the slice after queueing it, but it avoids an extra
allocation + memmove per incoming packet.

Before:
	$ go test -bench='Benchmark(QueueIncoming|WriteTo)' -benchtime=2s -benchmem
	BenchmarkQueueIncoming-4         7001494               342.4 ns/op          1024 B/op          2 allocs/op
	BenchmarkWriteTo-4               3777459               627 ns/op            1024 B/op          2 allocs/op
After:
	$ go test -bench=BenchmarkWriteTo -benchtime 2s -benchmem
	BenchmarkQueueIncoming-4        13361600               170.1 ns/op           512 B/op          1 allocs/op
	BenchmarkWriteTo-4               6702324               373 ns/op             512 B/op          1 allocs/op

Despite the benchmark results, the change in QueueIncoming turns out not
to have an effect in practice. It appears that the compiler had already
been optimizing out the allocation and copy in QueueIncoming.
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40187

The WriteTo change, on the other hand, in practice reduces the frequency
of garbage collection.
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40199
This commit is contained in:
David Fifield 2022-09-28 15:21:23 -06:00
parent d4749d2c1d
commit 839d221883
2 changed files with 52 additions and 13 deletions

View file

@ -42,8 +42,9 @@ func NewQueuePacketConn(localAddr net.Addr, timeout time.Duration) *QueuePacketC
} }
} }
// QueueIncoming queues and incoming packet and its source address, to be // QueueIncoming queues an incoming packet and its source address, to be
// returned in a future call to ReadFrom. // returned in a future call to ReadFrom. This function takes ownership of p and
// the caller must not reuse it.
func (c *QueuePacketConn) QueueIncoming(p []byte, addr net.Addr) { func (c *QueuePacketConn) QueueIncoming(p []byte, addr net.Addr) {
select { select {
case <-c.closed: case <-c.closed:
@ -51,11 +52,8 @@ func (c *QueuePacketConn) QueueIncoming(p []byte, addr net.Addr) {
return return
default: default:
} }
// Copy the slice so that the caller may reuse it.
buf := make([]byte, len(p))
copy(buf, p)
select { select {
case c.recvQueue <- taggedPacket{buf, addr}: case c.recvQueue <- taggedPacket{p, addr}:
default: default:
// Drop the incoming packet if the receive queue is full. // Drop the incoming packet if the receive queue is full.
} }
@ -84,22 +82,20 @@ func (c *QueuePacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
} }
// WriteTo queues an outgoing packet for the given address. The queue can later // WriteTo queues an outgoing packet for the given address. The queue can later
// be retrieved using the OutgoingQueue method. // be retrieved using the OutgoingQueue method. This function takes ownership of
// p and the caller must not reuse it.
func (c *QueuePacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { func (c *QueuePacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
select { select {
case <-c.closed: case <-c.closed:
return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)}
default: default:
} }
// Copy the slice so that the caller may reuse it.
buf := make([]byte, len(p))
copy(buf, p)
select { select {
case c.clients.SendQueue(addr) <- buf: case c.clients.SendQueue(addr) <- p:
return len(buf), nil return len(p), nil
default: default:
// Drop the outgoing packet if the send queue is full. // Drop the outgoing packet if the send queue is full.
return len(buf), nil return len(p), nil
} }
} }

View file

@ -0,0 +1,43 @@
package turbotunnel
import (
"testing"
"time"
)
type emptyAddr struct{}
func (_ emptyAddr) Network() string { return "empty" }
func (_ emptyAddr) String() string { return "empty" }
// Run with -benchmem to see memory allocations.
func BenchmarkQueueIncoming(b *testing.B) {
conn := NewQueuePacketConn(emptyAddr{}, 1*time.Hour)
defer conn.Close()
b.ResetTimer()
s := 500
for i := 0; i < b.N; i++ {
// Use a variable for the length to stop the compiler from
// optimizing out the allocation.
p := make([]byte, s)
conn.QueueIncoming(p, emptyAddr{})
}
b.StopTimer()
}
// BenchmarkWriteTo benchmarks the QueuePacketConn.WriteTo function.
func BenchmarkWriteTo(b *testing.B) {
conn := NewQueuePacketConn(emptyAddr{}, 1*time.Hour)
defer conn.Close()
b.ResetTimer()
s := 500
for i := 0; i < b.N; i++ {
// Use a variable for the length to stop the compiler from
// optimizing out the allocation.
p := make([]byte, s)
conn.WriteTo(p, emptyAddr{})
}
b.StopTimer()
}