go: net: Buffers makes multiple Write calls on Writers that don't implement buffersWriter
The writev syscall is supposed to act like a single write. The WriteTo method of net.Buffers will make a single write syscall on writers that have the unexported writeBuffers method. However, for writers that do not have such a method, it will call Write multiple times. This becomes significant if you are wrapping a *net.TCPConn without embedding, for instance, since it has different performance characteristics with respect to Nagle’s algorithm. Frustratingly, since the writeBuffers method is unexported, there’s no way for the application to know the behavior of Buffers.WriteTo in order to work around the issue.
Repro case: https://play.golang.org/p/rF0JRZs8z8
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 21
- Comments: 20 (14 by maintainers)
I understand the concern. I think that users of the API want to make the choice: not allocating might be important or not issuing more than one write might be important, depending on context. Exporting the interface would be a means of allowing the caller to use a type-assertion to determine capabilities of the
io.Writer, much like other I/O capabilities.Could we perhaps introduce a general I/O interface (consider this a sketch, nothing more):
Then in usage:
I could see this also being done as an alternate method on
net.Buffers, but I still don’t see whynet.Buffersrestricts to particular types, instead of allowing any type that implements theWritevsemantics to benefit.I think the bigger problem here is for datagram sockets.
The underlying system call guarantee a
writevcall will generate a single datagram, so(*net.Buffers).WriteTowill have different behavior when using bare*net.UDPConnvs wrapped, that is single datagram vs multiple.Edit: Even for stream sockets, it is not atomic as the underlying system call does. If there are concurrent writers, the result will be interleaved.
As for
os.Fileusage, I think theBufferstype and related interfaces should be in theiopackage,.os.Fileshould also implementsyscall.ConninterfaceThe current
net.Buffersis also a little hard for reuse, because it modify the slice directly, caller have to keep a own copy for reuse. Use bare [][]byte and let caller do theconsumetracking, or use a ring buffer could be easier and less copying.Edit2: Forget about the implement
syscall.Conninterface part. I am thinking about wiring to the runtime poller, but it doesn’t register to the runtime poller to begin with, except for those sockets already innet.conn.I think it’s probably too late for this as an API change in Go 1.10. Let’s leave this for Go 1.11 and be able to discuss with @bradfitz. I think maybe a more compelling motivation than a custom TCP wrapper would be letting os.File implementations get the writev optimization too.
@stokito ISTM that the meaning of “atomic” is under-specified there. Both the text in the wikipedia and the information from the manpage seem to suggest that it doesn’t mean “either all writes succeed or none of them”, but just that writes by different processes are not interleaved. i.e. it seems it refers to the isolation of ACID, not the atomicity. Also, from what I can tell, the actual POSIX standard does not guarantee even that, unless writes are less than
PIPE_BUFin size ([1] [2]).In my experience, interpreting what guarantees the POSIX standard really makes is very subtle. And what is actually implemented even more so. Personally, I got convinced by the argument that short writes can happen, at least for my usecase.
Step into this problem. net.Pipe impossible to use for tests with net.Buffers where there are multiple writers and single reader. Wire data became interleaved 😦
It didn’t shipped with Go 1.14, right? Any plans to include that
Writeverinterface by @zombiezen ?I’m not convinced there’s any scenario in which concurrent writers without holding a mutex is safe. The write(2) syscall can make short writes on e.g. interrupts, and the Go Write implementation will need a for loop around that -> concurrent Writes can interleave anyway.
Writev matters for datagrams and performance.
I would love to have access to
readvandwritevfrom Go, but I would point out that there’s no way to guarantee those semantics generically across arbitrary operating systems. But the ability to get those atomic writes from multiple buffers is a HUGE performance win for a lot of applications.As another voice: I’m particularly interested in what @rsc mentioned - having
Writevfor*os.File. My use-case is a write-ahead log, which adds a header and footer to some user-provided data. File is opened withO_APPEND, so there is a semantic difference between one and multiple writes. Currently I allocate a buffer and copy everything, but I’d like to avoid that.Personally I like the interface @zombiezen wrote down above and I would put it in
io. This would be akin toio.StringWriter/WriterTo/WriterAt/…where theiopackage defines multiple interfaces to make anio.Writermore efficient in some circumstances and then also offers functions likeio.WriteStringwith the natural fallbacks.This is an interesting one. I think it’s somewhat clear that calling the
Writemethod should turn into a singlewritesystem call for low-level types. But this is theWriteTomethod, and I don’t think there has ever been such a guarantee forWriteTo. In generalWriteTowrites all available data to theWriterargument, which can imply fetching more data. For example, the more-or-less canonical implementation ofWriteTo,bufio.(*Reader).WriteTo, makes multipleWritecalls. Similarly, the original implementation ofWriteTo, in https://golang.org/cl/166041, used multipleWritecalls.But clearly for a low-level type it is desirable to minimize
writecalls, and when usingnet.Buffersit’s inconvenient to not know whether you will get oneWritecall or several. So there does seem to be an argument thatnet.(*Buffers).WriteToshould copy the bytes and callWriteonce. But there is also a counter-argument that the whole point ofnet.Buffersis to avoid copying bytes, and so doing a copy anyhow seems like a bit of a trick.