golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.39k stars 17.71k forks source link

proposal: io,net: add WriteMany interface #68625

Open Jorropo opened 4 months ago

Jorropo commented 4 months ago

Proposal Details

This proposal aims at making a user-implementable version of net.Buffers.

Add a new interface to io and implement it on net.* which implement net.buffersWriter:

type ManyWriter interface {
 // WriteMany has the same semantics as appending the buffers back to back into one buffer and calling [Writer.Write] method,
 // but it allows to remove the need to allocate and copy all the buffers in one contiguous memory region.
 // The implementation cannot write to any of the passed buffer or buffers slice.
 WriteMany(...[]byte) (n int64, err error)
}

note: this return int64 because it would be easy to build let's say 16GiB buffer on a 32bits pointer architecture by having the byte arrays alias each other.

(*net.Buffers).WriteTo would also check if the underlying implementation supports io.ManyWriter as well as net.buffersWriter.

gabyhelp commented 4 months ago

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Jorropo commented 4 months ago

opening as a formal proposal to solve #21676, feel free to close a dup

Jorropo commented 4 months ago

And in case of rehashing arguments from #21676 the problem I am seeing: I have protocol with <small header> | <big payload> and a method like this:

func (*T) WritePayload(b []byte) error

Here are the possible solutions today and why they are not that good:

  1. Nagle's algorithm add latency in the pipeline, plus does not solve the double syscall overhead (with double digit Gbit/s TCP I have many profiles with >60% of time spent in syscall.Syscall6 doing TCP)
  2. net.Buffers it is not composable, in most of my usecases I have a TCPConn wrapped in TLS, HTTP or yamux.
  3. bufio.Writer still do two .Write call, the header is small enough to be fully buffered, then the payload is written to, it does not fit into the buffer, so bufio.Writer flushes the header and then pass the payload as-is in a second .Write call.
    • note: if we had such interface it would be useful to optimize this codepath so it use .WriteMany to write the buffered portion and the inbound payload in one call, would be done conditionally with a conditional type assertion but that would be a future proposal if something like this one passes.
  4. manually doing t.underlying.Write(append(header, payload...)) generate garbage
  5. do solution 4 but store buffers in a receiver field and recycle them this double the memory usage of this one implementation / consumer pair and does not remove the cost of copying bytes around (worst CPU cache utilization)
  6. re-architecture the code to do dma style buffer access in callbacks (func (*T) WritePayload(n uint, func(dest []byte) error) error) create callback hell when composed and is not always possible (I don't always know the maximum size of the payload ahead of time)
jfrech commented 3 months ago

Does defining a dual io.ManyReader have any known applications?

Jorropo commented 3 months ago

@jfrech ManyReader ¿ not ManyWriter ?

Idk this is not this proposal feel free to open an other one.