Closed nyurik closed 4 months ago
This sounds like something we could fix directly in libstd. Do you know of any attempts of doing that, or why it hasn't been done? We do similar trickery for format!
after all
I tried it with https://rust.godbolt.org/z/Y8djWsq1P
This issue is also tracked in https://github.com/rust-lang/rust/issues/99012 - but it appears to be highly complex and nowhere near being solved in a near future, thus warranting a workaround at least in the popular crates.
(I have not read most of https://github.com/rust-lang/rust/issues/99012.)
As I understand it, write!(f, "...")
does not optimize in a straightforward way to f.write_str("...")
because:
write!
does not get to assume f
implements core::fmt::Write, or std::io::Write. The only thing it gets to assume about f
is that f.write_fmt(...)
is callable and takes 1 argument of type core::fmt::Arguments. It would not be backward compatible for write!(f, "...")
to expand to anything other than f.write_fmt(...)
.
It does not even get to assume whether write_fmt
takes &mut self
or &self
or self
or Box<Self>
, which makes it extremely challenging to even apply autoref-based specialization or similar hack.
This limitation came up previously in https://github.com/rust-lang/rust/issues/99684#issuecomment-1197003807 with some language suggestions, but they are far-fetched.
See also https://github.com/rust-lang/rust/pull/99689 where it says "desired (unimplementable)".
We do not want any part of any write_fmt
method from the standard library to be inlined. They are called so often that this is practically guaranteed to regress compile times.
I think there is a libstd fix and a compiler fix that are worth trying:
Standard library: change core::fmt::Formatter::write_fmt
(and maybe others) to the following, and see how bad the regression is.
+ #[inline]
pub fn write_fmt(&mut self, f: fmt::Arguments) -> fmt::Result {
+ if let ([s], []) = (f.pieces, f.args) {
+ self.buf.write_str(s)
+ } else {
write(self.buf, f)
+ }
}
Compiler voodoo. Make write!
lower directly to a dedicated AST node, the way format_args!
does since https://github.com/rust-lang/rust/pull/106745. If the receiver ends up being core::fmt::Formatter (or one of a small hardcoded set of other types) then it turns into write_str
, otherwise it needs to be handled in a way that is indistinguishable from f.write_fmt(...)
— no idea how feasible this is.
@dtolnay thx so much for the in-depth write-up! TIL. I can try the first approach (relatively simple). The second one clearly involves a lot more in-depth compiler knowledge... maybe someday
P.S. Turns out Arguments::as_str already does all the needed detections!
I tried the above suggestion, and it seems to already produce a significantly smaller assembly as seen in https://github.com/rust-lang/rust/pull/121001#issuecomment-1940574019
My only concern is that it will impact either the build time or the bin size in some other case... guess will have to wait for the results...
Apparently
write!
generates more code thanwrite_str
when used with a simple string, so optimizing it.