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
RFC: Vectored Writes #3135
Comments
+1 for adding just a vectored write method to Regarding the argument type, I would heavily lean towards either
I'm not sold too much on the benefit of But I'm also not really opposed for having the |
Given that a |
That'd make it much easier for a composite type that may have forgotten to override the default in an infrequent path could crash a server, instead of occasionally just being a little slower. And the strategy that hyper uses can still be used to try to detect the best solution dynamically. |
"A little slower" is going to be very hard to debug. The default impl should never be used. This is why I wanted to explore the separate trait option. |
I still think grumpy debugging is better than crashing a server and killing thousands of connections. I could see the argument for making it a debug-assert level panic, to help when testing, but once shipped to production, don't blow up. |
It could also log a warning in release mode? |
I would be very scared if someone shipped code to production that did not test the write path... |
A debug panic is probably fine though. |
This adds `AsyncWrite::poll_write_vectored`, and implements it for `TcpStream` and `UnixStream`. Based on the proposal in #3135.
PR is up: #3149 |
This adds `AsyncWrite::poll_write_vectored`, and implements it for `TcpStream` and `UnixStream`. Refs: #3135.
I believe that merging #3149 should have closed this issue? |
This provides a proposal for providing generic vectored writes in Tokio.
Background
writev
Vectored writes are the ability to write multiple, non-contiguous buffers as a single "atomic" write. This helps reduce syscalls and memory copies, since data to be written can frequently be in different buffers.
A simple example is writing a length-delimited protocol where the length goes before the message. Once the message contents are put into a buffer, an encoder needs to write the length to the transport first. You could do that with 2
write
calls, one for the length, and then one for the message. Or you could copy both the length and the message into a single buffer, to only have 1write
call, but that gets more expensive the bigger the message is. Vectored writes solves both issues:Previous Design
Problems
poll_write_buf
even if they couldpoll_write_buf
is much worse than just merging buffers and usingpoll_write
poll_write_buf
took a generic,B: Buf
, which made it forcedyn AsyncWrite
trait objects to always use the default "slow" method, instead of being able to forward on.Options
There seem to be two main options for providing a generic vectored write solution. This proposal explores both of them, and then provides a recommendation afterwards.
1. As Part of
AsyncWrite
The way the standard library supports this for blocking writes is with additional methods on
Write
. This option follows that example:Transports that do no support vectored writes do not need to do anything, as there is a default implementation that forwards to
poll_write
. To deal with the problem of the default forwarding being very slow, an additionalis_write_vectored
method can be checked. A user can ask anAsyncWrite
if it supports vectored writes, and if not, consider using a single flat buffer and only callingpoll_write
.This doesn't solve the issue of implementors forgetting to override the default method when creating wrappers. The suggested fix here is just to pay better attention 🤷.
2. A Separate
AsyncVectoredWrite
traitIt has been suggested that since not all transports can support vectored writes, only those that do should implement a separate trait.
At first glance, this feels really nice. We can get static verification of the capabilities of a transport. There are some parallels to
Iterator
, and howExactSizeIterator
can be indicate a type that knows its exact size, or howDoubleEndedIterator
can also iterate from the back.However, to be really elegant, this would option would require trait specialization. This is similar to how the standard library can be generic over
Iterator
, and optimize/specialize when the type also implementsExactSizeIterator
.Specialization
Transports that could implement
AsyncVectoredWrite
likely could also implementAsyncWrite
. Libraries that want to be generic over a transport would currently need to decide either to only supportAsyncWrite
, or require all transports to implementAsyncVectoredWrite
.In some (distant) future, we'd ideally be able to write something like this:
Alas, that is not currently stable, and won't be for some time.
Dynamic Support
Besides the lack of specialization, there are also occassions when a transport is a composite type that doesn't know if it supports vectored writes at compilation time. Consider the following example:
If the library author wanted to use this type in a place that required
AsyncVectoredWrite
, they could decide to implementAsyncVectoredWrite
in a fashion that forwards towritev
on the plaintext variant, and does the "slow" default similar to the libstd option. But that has the same problem as the original design.BufWriter
For now, a library author that would want to try to use vectored writes would need to require
T: AsyncVectoredWrite
, and users that have transports that don't implement it could pass it into a simpleBufWriter
wrapper. This would likely be a better option than forwarding just 1 buffer at a time.Applying this to the
MaybeTlsStream
above, a library author could extend it like so:A downside to this approach is that combining more and more composite types can easily end up using multiple layers of
BufWriter
, especially when some layers are also trying to be generic over eitherAsyncWrite
orAsyncVectoredWrite
.Recommendation
This proposal recommends option 1, adding methods to
AsyncWrite
.AsyncWrite
andAsyncVectoredWrite
.is_vectored_write
, composite types can forward onto their inner types, and thus intermediate buffering layers are not required.Appendix
Argument Type
There is a question of what the exact argument type for
poll_write_vectored
should be:&[IoSlice]
,&mut impl Buf
, or&mut dyn Buf
.&[IoSlice]
: This matches what is used instd::io::Write
.&mut impl Buf
: Passing a genericBuf
is more convenient, but one of the problems with the original solution is that generic methods cannot be implemented on trait objects (soBox<dyn AsyncWrite>
).&mut dyn Buf
: This fixes the problem with trait objects, and would be more convenient than&[IoSlice]
, but the cost of the dynamic dispatch onBuf
methods concerns some people. Whether the cost is actually noteworthy is not known (simple experiments in hyper did not notice anything).To deviate the least from the standard library, we propose using
&[IoSlice]
. It would be easy for us to add an additionalpoll_write_buf(&mut dyn Buf)
method that fills up a stack[IoSlice]
, reducing boilerplate.The text was updated successfully, but these errors were encountered: