Skip to content

Do not deduplicate captured args while expanding format_args!#149926

Open
ShoyuVanilla wants to merge 1 commit intorust-lang:mainfrom
ShoyuVanilla:no-dedup-fmt
Open

Do not deduplicate captured args while expanding format_args!#149926
ShoyuVanilla wants to merge 1 commit intorust-lang:mainfrom
ShoyuVanilla:no-dedup-fmt

Conversation

@ShoyuVanilla
Copy link
Copy Markdown
Member

@ShoyuVanilla ShoyuVanilla commented Dec 12, 2025

View all comments

Resolves #145739

I ran crater with #149291.
While there are still a few seemingly flaky, spurious results, no crates appear to be affected by this breaking change.

The only hit from the lint was
https://github.com/multiversx/mx-sdk-rs/blob/813927c03a7b512a3c6ef9a15690eaf87872cc5c/framework/meta-lib/src/tools/rustc_version_warning.rs#L19-L30,
which performs formatting on consts of type ::semver::Version. These constants contain a nested ::semver::Identifier (Version.pre.identifier) that has a custom destructor. However, this case is not impacted by the change, so no breakage is expected.

@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Dec 12, 2025

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

Some changes occurred in compiler/rustc_ast_lowering/src/format.rs

cc @m-ou-se

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Dec 12, 2025
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Dec 12, 2025

r? @spastorino

rustbot has assigned @spastorino.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rust-log-analyzer

This comment has been minimized.

@ShoyuVanilla
Copy link
Copy Markdown
Member Author

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 13, 2025
@ShoyuVanilla
Copy link
Copy Markdown
Member Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Dec 13, 2025
@theemathas theemathas added the I-lang-nominated Nominated for discussion during a lang team meeting. label Dec 14, 2025
@theemathas
Copy link
Copy Markdown
Contributor

Nominating as per #145739 (comment)

@traviscross traviscross added P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang T-lang Relevant to the language team needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. labels Dec 14, 2025
@traviscross
Copy link
Copy Markdown
Contributor

traviscross commented Dec 14, 2025

It'd be worth adding a test for the drop behavior.

@traviscross
Copy link
Copy Markdown
Contributor

traviscross commented Dec 14, 2025

Given that this makes more sense for the language, along with the clean crater results and the intuition that it'd be surprising if anything actually leaned on this, I propose:

@rfcbot fcp merge lang

@rust-rfcbot
Copy link
Copy Markdown
Collaborator

rust-rfcbot commented Dec 14, 2025

Team member @traviscross has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Dec 14, 2025
@m-ou-se m-ou-se assigned m-ou-se and unassigned spastorino Dec 17, 2025
@m-ou-se
Copy link
Copy Markdown
Member

m-ou-se commented Dec 24, 2025

I don't think we should do this. It will make the generated code for println!("{x} {x}"); less efficient, as it will get two separate arguments instead of one.

I don't want to end up in a situation where it would make sense for Clippy to suggest something like:

warning: using the same placeholder multiple times is inefficient as of Rust 1.94.0
 --> src/main.rs:3:5
  |
3 |     println!("{x} {x}");
  |     ^^^^^^^^^^^^^^^^^^^
  |
help: change this to
  |
3 -     println!("{x} {x}");
3 +     println!("{x} {x}", x = x);
  |

Adding , x = x shouldn't make a difference. If adding that makes the resulting code more efficient, I strongly feel like we've done something wrong.

@rust-rfcbot concern equivalence

@rust-rfcbot
Copy link
Copy Markdown
Collaborator

🔔 This is now entering its final comment period, as per the review above. 🔔

@rust-rfcbot rust-rfcbot removed the proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. label Feb 25, 2026
@nikomatsakis
Copy link
Copy Markdown
Contributor

nikomatsakis commented Feb 25, 2026

@rfcbot concern just-does-not-seem-worth-it

So I've been kind of sitting this out, and I hate to do this, but I am going to raise a concern for a bit more discussion. The bottom line is that I don't really buy the "consistency" motivation for this change. I see no reason to expect that format!("{X}, {X}") could be consistent with foo(X, X). In fact, I would much more expect consistency between format!("{X}, {X}") and format!("{X}, {X}", X=X).

It seems like the consistency concern comes up when you look at something like format!("{expr()}, {expr()}")?

But I see a rather consistent mental model which is that embedded {} blocks introduce an argument which can be named (in the case of X or a field chain like x.y.z) or anonymous (all other kinds of expressions).

I gather that the main argument in FAVOR of this chang is that it'd be simpler if the embedded {} blocks desugared to always a fresh argument. And this is rather stronger with f strings, i.e., f"foo({X},{X})" doesn't have that "desugars to X=X" to lean on, rather you must explain in terms of format!.

I have a kind of internal heuristic which is like: don't break compatibility if you don't have to. Right now, if this were a fresh design, I suppose I'd be on the fence because of f strings, but given that (a) there is code in the wild; (b) the desugaring is still easy to understand either way; and (c) it has real-world impact on performance and code-size, I don't understand why we would make this change.

Am I missing something? Is there a more complex argument?

If I do have this right, can somebody summarize the perf/cost hit and point me at the comments about the complexity of the proposed optimization?

(The optimization smacks to me "sufficiently smart compiler" when a rather "dumb compiler hack" would do the trick...)

EDIT: I read over more the comments. I can see that the f"" strings and the case with more fields does make the desugaring more complex to think about. I'll ponder a bit.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. and removed final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. labels Feb 25, 2026
@nikomatsakis
Copy link
Copy Markdown
Contributor

nikomatsakis commented Feb 25, 2026

Actually, what would help me is to write-out the precise desugaring expected for format!("{X.f}, {X.f}"). @m-ou-se you had a comment but skimming it I didn't quite see what desugaring you expected there?

EDIT: I guess that based on this comment...

Do we expect format_args!("{p.name}", p=get_person()) to work? If so, format_args!("{a.b}") could simply desugar to format_args!("{a.b}", a=a).

...it's clear enough.

@traviscross
Copy link
Copy Markdown
Contributor

The bottom line is that I don't really buy the "consistency" motivation for this change. I see no reason to expect that format!("{X}, {X}") could be consistent with foo(X, X). In fact, I would much more expect consistency between format!("{X}, {X}") and format!("{X}, {X}", X=X).

What about between format!("{X} {X}") and format!("{} {}", X, X)?

Am I missing something? Is there a more complex argument?

I think the best case for the simple argument is demonstrated by the earnest surprise that @theemathas (in #145739) and @Jules-Bertholet (in rust-lang/rfcs#3626 (comment)) had about the behavior. Obviously both are Rust experts. As @Jules-Bertholet said:

const Z: SomeStruct = SomeStruct::new();
println!("{Z.field1} {Z.field2} {Z.field3}");

If Z has interior mutability, then it has to be evaluated several times, no? That’s how consts work everywhere else in Rust. If you want only one instance, use a static

The simple argument really is that simple. From lang, we've pushed a consistent evaluation opsem for consts, the interpolation behavior is inconsistent with that, and that surprises people.


The slightly-more-complex argument, in an RFC 3626 context, goes as follows.

Let's say we want to desugar this:

fn main() {
    println!("{X.f} {X.f}"); //~ Format string to desugar.
}

// Feel free to ignore this scaffolding...
use core::{fmt, sync::atomic::{AtomicU8, Ordering::Relaxed}};

const X: W = W(Y);
const Y: U = U { f: V(AtomicU8::new(0)) };

struct W(U);
struct U { f: V }
struct V(AtomicU8);

impl fmt::Display for V {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let x = self.0.fetch_add(1, Relaxed);
        write!(f, "displayV({x})")
    }
}

impl Drop for W {
    fn drop(&mut self) {
        let x = self.0.f.0.fetch_add(1, Relaxed);
        println!("dropW({x})");
    }
}

impl core::ops::Deref for W {
    type Target = U;
    fn deref(&self) -> &Self::Target {
        let x = self.0.f.0.fetch_add(1, Relaxed);
        println!("derefW({x})");
        &self.0
    }
}

There are three ways that come to mind:

fn main() {
    //println!("{X.f} {X.f}"); //~ Format string to desugar.
    println!("-- 1:");
    println!("{} {}", X.f, X.f); //~ Desugaring 1.
    println!("-- 2:");
    { let x = X; println!("{} {}", x.f, x.f) }; //~ Desugaring 2.
    println!("-- 3:");
    println!("{} {0:}", X.f); //~ Desugaring 3.
}

Playground link

These produce outputs:

-- 1:
derefW(0)
derefW(0)
displayV(1) displayV(1)
dropW(2)
dropW(2)
-- 2:
derefW(0)
derefW(1)
displayV(2) displayV(3)
dropW(4)
-- 3:
derefW(0)
displayV(1) displayV(2)
dropW(3)

I find the output of desugaring 1 satisfying, given our intended opsem expectations for consts. I find the output of desugarings 2 and 3 various degrees of unsatisfying. In particular, the idea that with println!("{X.f} {X.f}") we would get (with desugaring 2) two derefs but only one drop would just seem to be a real wart and a likely-continual source of surprise. But desugaring 3 is no picnic either, especially when we start thinking about how we'd want to handle {x.y} {x.y.z}.

For my part, if we were not to clean this up, then I'd become more skeptical of whether we'd want to expand our interpolation syntax at all.

Is it worth it? I really can't imagine there being meaningful breakage here. Regarding the performance, I'm hopeful that as-if optimizations can cover the common cases, but maybe there would be some cost. Maybe in some cases people should indeed prefer to write format!("{x} {x}", x=X.f) rather than format!("{X.f} {X.f}") -- unless we went with desugaring 3, this would have, if nothing else, fewer derefs. But I can live with that. In the rest of the language, there are plenty of reasons that people should similarly write let x = EXPR; /* Uses x twice */ rather than /* Uses EXPR twice */. That's the cost of our language not being pure.

@nikomatsakis
Copy link
Copy Markdown
Contributor

Thinking on this. I think that looking at particular examples isn't that fruitful for me. I get a lot more value out of thinking about the desugaring, making sure it's sensible, that it does the right thing in the common cases, and letting the semanticscs of edge cases fall out from the desugaring.

I think this aligns well with the way people learn: first learn the easy stuff, then learn the details, then let those details guide their intutions (the Terrance Tao More to math than rigor process).

So, let's assume that the backwards compatibility is not an issue here for the moment.

Then I think we are arguing about the desugarings. TC is proposing that a nice desugaring is going to be: {$EXPR} becomes {} and $EXPR in the arguments.

There are other options you could take, e.g., it behaves differently in the particular case of a single variable, or, we create one argument based on the literal bytes of the expression, or more complex things.

I do also think that when we look forward to f"" notation, which I really desperately want (in fact I'd make it the default form of strings in some new edition, myself), the question also gets interesting, because then format! is kind of this weird thing people will not be using, and there's no obvious way to do "named arguments" in f"" syntax -- but I guess learning about format! is fine.

To me, there are several conflicting design axioms here. One of them is "Rust is straightforward" or something like that, but another is "idiomatic nice Rust does the right thing modulo a specific set of compiler optimizations" (and should we include this particular thing in that case).

I am curious: do we have any data about how often it occurs in practice that the same variable is repeated? (I don't recall.)

Thinking about it, I can certainly see the argument that "why should it work differently than repeating variable references in any other context, if that's an efficiency problem, shouldn't we address it holistically".

@scottmcm
Copy link
Copy Markdown
Member

scottmcm commented Mar 4, 2026

The previous few posts make me ponder this: Is there an existing complexity we can re-use for this?

Like what if we imagine that

f"{a.b.foo} {a.b.bar}"

desugared to something more like

tuple_closure_format!("{} {}", || (&a.b.foo, &a.b.bar))

Then we could say "well obviously that follows the same capture rules as closures", using the less-straight-forward-but-more-useful capturing rules we've already defined for closures.

(And by not needing to do it on tokens we could have much smarter rules -- maybe even things smart about Copy.)


For clarity, this is currently a thought exercise, not a concrete "we should change it to work that way" proposal. There might well be reasons this doesn't work at all; I haven't thought through it in detail.

@nikomatsakis
Copy link
Copy Markdown
Contributor

OK, I've given this some more thought -- but before I write anything, I want to confirm something:

The optimization we've been discussing, is the point here to remove the "extra fields" from the format args struct? Can someone link me to the PR or description? That sounds like a pretty tailored and complex optimization, is it intended to be special purpose for the format-args struct or something that would apply more generally?

@RalfJung
Copy link
Copy Markdown
Member

RalfJung commented Mar 4, 2026

The PR with the optimization is #152480.

@nikomatsakis
Copy link
Copy Markdown
Contributor

@RalfJung that's a big PR, is there a shorter description of what it does and how?

@dianne
Copy link
Copy Markdown
Contributor

dianne commented Mar 4, 2026

@nikomatsakis the relevant part is the changes to AST→HIR lowering, https://github.com/rust-lang/rust/pull/152480/changes#diff-8b601779832d29e42b24bb88ccc3a5762217d72f148e8070767ee787b271190c. the way it's implemented there is as a transformation on the AST representation of format_args! before it's lowered to HIR. it scans through the formatting placeholders looking for captures of identifiers that refer to places; if it finds any duplicates, it de-duplicates them.

@nikomatsakis
Copy link
Copy Markdown
Contributor

nikomatsakis commented Mar 11, 2026

Thanks Dianne. Let me see if I can dash off a comment explaining where I stand here.

The TL;DR is that my proposal would be:

  • If you do {identifier}, we deduplicate -- or, my preference actually, expand to all syntactically equal place expressions;
  • If you do {...} anything else, we do not.

This does cost some consistency, but I think in areas where consistency isn't necessarily expected. For example, it's already the case that foo(X, X) is not the same as format!("{X} {X}") -- e.g., format captures references, it doesn't move (which is something people have gotten confused about every time I ran a Rust tutorial, side note).

The alternative gains in consistency but loses some optimization and backwards compatibility. It's not clear to me how much the optimization matters but I think that it may, particularly since we use format-args all over the place in code. I know that people often find the machinery is heavyweight in embedded land.

I don't think the vast majority of users will care whichever way we decide here, but there will be some that are surprised by const-drops running or not running, and some who are surprised that their structs are bigger than they should be. I tend to think the former will only matter to Rust supermavens, and they can learn the way that the desugaring works, it's straightforward enough. The latter is an invisible tax across Rust codebases that may impact every user.

Expanded version:

I think there's definite tension between several good Rust design principles:

Efficient by default -- the idiomatic, obvious Rust code should generate efficient things ~the same as what you would get if you did it by hand, or perhaps more efficient. To that end, if you desugared format!("{x} {x}"), it's unlikely you would store the reference to x twice. It'd be nice to have that property.

No need for a 'sufficiently smart compiler' -- we should not be leaning on super whiz-bang optimizations to get that efficient by default, just the "obvious" ones that compilers typically do, such as inlining, copy prop, CSE. The kind of thing you would do by hand automatically. (We kinda cheat on this one, sometimes, leaning on fancy alias analysis, I think that the work on minirust etc may let us out of that trap.)

Stability without stagnation -- we should try to avoid changing behavior without a strong reason.

Compiler and stdlib aren't special -- we try to expose primitives users could build themselves (or at least have a plan that they can eventually do so...).

Context-free programming -- this is a tricky one, but obviously we aim to reduce the context needed for people to understand what some code will do when it executes. Probably need to either expand or refine this to be more specific.

This one is somewhat aspirational but:

Define through desugaring -- there should be a convenient syntax and an explicit syntax; the convenient one should desugar to the explicit one in a straightforward way. That is then used to resolve non-obvious edge cases around the convenient syntax.

Looking forward, I think we want

  • format!("{...}") to support arbitrary expressions
  • f"{...}" to support arbitrary expressions

Reading over the proposed optimization, it seems to violate "compiler isn't special", in that I don't think we would ever expect to expose that kind of test to a user-defined macro. That's not the end of the world, but it seems unfortunate.

Doing no optimization violates efficient by default -- this may not matter, it's only a small thing, on its own I might say "whatever" but it also changes behavior. My inclination is to try and preserve wins when we can.

I think my proposal wins on stability and perf by default; I think it is neutral towards "define through desugaring", it's a bit more of a complex desugaring, but not wildly so, and it increases consistency with some other things (e.g., format!("{X} {X}", X = X).

The optimization loses big on stdlib isn't special and I think that's kinda worse.

@Jules-Bertholet
Copy link
Copy Markdown
Contributor

FWIW, Niko's “compiler and stdlib aren't special" argument has mostly won me over. If format_args! is “just a macro you could write yourself, it operates on the token stream like every other macro”—then having a few weird edge cases is understandable (as long as they are documented).

However, this is in tension with with the desire to support f"{...}" syntax. That would be a clear signal that formatting is a core part of the language, and we should not be cutting corners with the semantics of the core language. (For similar reasons, I would expect macro_rules! metavars to work inside f-strings, e.g. f"{$metavar}".)

@nikomatsakis
Copy link
Copy Markdown
Contributor

nikomatsakis commented Mar 18, 2026

I've given this some thought, I've also talked to @traviscross and @joshtriplett.

I'm finding that I have a hard time convincing myself one way or the other on this!

I suspect that BOTH of these are, to a first approximation, true:

  • Nobody will notice the semantic difference of duplicating vs de-duplication (i.e., evaluating places tends to be side-effect free);
  • And nobody will notice the performance difference of duplicating vs de-duplication (i.e., the occasional duplicated field will be in the noise.

If you could convince me that one of those was not true -- that somebody would notice -- that'd push me one way or the other more firmly. But I'd need some data. I'm going to try and see how often repeated variables occur in practice.

Assuming my assumptions are valid -- that neither is all that big a deal -- then you have two competing, but largely abstract, principles

  • overall simplicity -- basically that it's nice to say that {xxx} just desugars to "xxx"
  • backwards compatibility, efficient by default -- it's nice that an example like format!("index.crates.io/{pkg}/{pkg}-{version}") doesn't duplicate the field pkg, and of course the semantics around constants are relevant

I think both are important. I go back and forth on which I think are more important.

When it comes to the "fancy compiler optimization", yeah, it kind of lets you have both, but I find it overengineered for the problem, and it expands our "scope" of what it takes to achieve efficiency. If efficiency matters that much, I might rather do it the simple way of saying "we deduplicate at the string level". It is, in a way, less surprising to me.

@RalfJung
Copy link
Copy Markdown
Member

I'm finding that I have a hard time convincing myself one way or the other on this!

FWIW, I sympathize with that. I have also gone through phases of preferring either approach to this.^^ Though IMO the compiler optimizations actually provide a nice way out of this, I find them an elegant solution (have our semantic cake and eat the perf benefits, too) -- except that @m-ou-se doesn't like them, which gives me pause.

@nikomatsakis
Copy link
Copy Markdown
Contributor

So I wrote a little script (gist) to find all string literals, count the number with ANY interpolation variables (well, braces anyhow) and then count the number with repeats.

I ran it across the rust repo and got 2.5%, though that number includes tests.

I'd like to run it across crates.io.

Do with that what you will.

@nikomatsakis
Copy link
Copy Markdown
Contributor

nikomatsakis commented Mar 18, 2026

I ran the script across the top 222 crates from crates.io. I found that about 5% of strings have repeated variables...

--- Summary ---
Mode: all string literals
Strings with interpolation vars: 2417
With repeated variables: 121
Percentage: 5.0%

...that's actually more than I expected! It pushes me to think I am right to hold this concern.

@nikomatsakis
Copy link
Copy Markdown
Contributor

One more piece of data:

I found exactly ZERO instances of repeated "capital" identifiers. e.g., format!("{TAB} {TAB}") in those 222 crates. So I think we can assume that duplicating constants doesn't happen very often, much less constants with a side-effecting destructor of some kind.

@nikomatsakis
Copy link
Copy Markdown
Contributor

If we assume that 5% is common enough that we DO want avoid an extra field for local variables, then I think it's reasonable to assume we also want to avoid an extra field for fields. I don't see why {x} would be more common than {self.field}.

Deref impls can, technically, have side-effects. So while foo.bar is going to require some sensitive types/traits reasoning to deduplicate, if you want to get too precise about it. This implies that the "semantics-preserving" optimization will either get very complicated or not be able to handle this case.

This further pushes me to the conclusion that the most appealing options are

  • Deduplicate place expressions syntactically: more efficient, more complex underlying mechanism to explain desugaring.
  • Never deduplicate: less efficient, simpler desugaring, preserves the intuition that, in a format-string, you can just "drop the string stuff" and you get some expressions that execute.

I think a key variable may be how much you think it matters whether format!("{X} {X}") should be "like" foo(X, X) or if it's ok that the desugaring is a bit more complex than that. Myself, I think it's ok, I see no real evidence that saying "we first deduplicate place expressions" is really going to matter to anyone in practice or that it will be particularly hard to understand once you learn it. And I see some evidence that the perf implication is real (5% of format strings that include some duplicate variable).

@iago-lito
Copy link
Copy Markdown
Contributor

iago-lito commented Mar 19, 2026

@nikomatsakis I don't see why {x} would be more common than {self.field}.

I do. Because although I would very likely write

format!("{x} {x}")

then also if I ever stumble accross this code:

format!("{self.field} {self.field}")

I would very likely, and spontaneously, deduplicate it myself into the following to improve readability:

format!("{x} {x}", x=self.field)

Which I think is, if I'm not alone, an argument in favour of syntactic deduplication.

@traviscross
Copy link
Copy Markdown
Contributor

traviscross commented Mar 22, 2026

@nikomatsakis: I don't know. Presumably we still wouldn't want to deduplicate value expressions. I just struggle with the idea that f"{x.f} {x.f}" and f"{x.f()} {x.f()}" would have different dynamic semantics. Or -- let's get really speculative here -- assume that we someday made it possible for function calls to be place expressions. Then the dynamic semantics of f"{x.f()} {x.f()}" would depend on the signature of f. Or (again extremely speculatively), assume we later added computed fields. Then the dynamic semantics of f"{x.f} {x.f}" might depend on whether f was a real field or a computed one. Given that in no case is x.f (where a Deref::deref call is possible) a pure operation, this just seems unnecessarily strange to me.

Let's also dig into the axiom that the compiler and stdlib aren't special. In a world where functions return places, the lowering would need type information to deduplicate only place expressions. That seems more complicated to me than @dianne's AST → HIR lowering optimization. But if the desugaring doesn't deduplicate place expressions, then this isn't a problem.

@RalfJung
Copy link
Copy Markdown
Member

We already can't really syntactically distinguish place expressions from value expressions -- we need name resolution at least, which arguably is a semantic analysis (user-defined macros cannot do it). x could be a local variable (place expression) or a constant (value expression).

@traviscross
Copy link
Copy Markdown
Contributor

traviscross commented Mar 22, 2026

So I wrote a little script (gist) to find all string literals...

I'm working on a somewhat more precise analysis. I've instrumented rustc to capture details about format strings. I'll be running this through crater.

It's built on top of @dianne's branch (in #152480) so that we can see the effect of that optimization and how much it matters whether we do it at all.

So far, I've run this on a stage 2 build of the compiler and standard library (including all dependencies). In this sample, of format strings that use interpolation at all, only 0.5% have any duplicates. Of these, almost all are places, and @dianne's optimization recovers 96.1% of the size cost (i.e., all but 48 bytes total).

The cost of not doing deduplication at all (i.e., not doing dianne's optimization) on the args: &[rt::Argument<'_>] arrays referenced by the Argument structs totals to +0.41%. Note that this is calculated only over the arrays; it'd be watered down by considering the overhead of the rest of the struct. Calculated over the compiler as a whole, it'd be in the noise -- the absolute difference is only 1.2KB.

See below for the full results.


format_args! Deduplication Analysis
====================================

  Source: 19,405 invocations

BASELINE
────────
Total invocations                        19,405  (100.0%)
  Interpolating (ph > 0)                 12,610  ( 65.0%)
  Fixed strings (ph = 0)                  6,795  ( 35.0%)

  Arguments per invocation:
      0       6,795  ( 35.0%)  ████████████████████████
      1       8,633  ( 44.5%)  ██████████████████████████████
      2       2,683  ( 13.8%)  █████████
      3         825  (  4.3%)  ███
      4         294  (  1.5%)  █
      5          94  (  0.5%)  █
      6          24  (  0.1%)  █
      7          32  (  0.2%)  █
      8          14  (  0.1%)  █
     9+          11  (  0.1%)  █
    mean: 1.0  median: 1  p95: 3  max: 23

CAPTURE MECHANICS
─────────────────
  Of 12,610 interpolating invocations:
    Implicit capture                      4,973  ( 39.4%)
    Explicit named                          625  (  5.0%)
    Positional                            7,493  ( 59.4%)
    Width/precision arg                      64  (  0.5%)

DUPLICATION
───────────
  Invocations with duplicate captures:  63  (  0.5% of interpolating)

  Extra arg slots from duplication:  76 total
    mean: 1.2 per affected invocation

  Extra slots per invocation:
      0           0  (  0.0%)
      1          56  ( 88.9%)  ██████████████████████████████
      2           2  (  3.2%)  █
      3           4  (  6.3%)  ██
      4           1  (  1.6%)  █

  Multiplicity of duplicated names (65 total):
    2x                                       59  ( 90.8%)
    3x                                        1  (  1.5%)
    4+x                                       5  (  7.7%)

  Resolution of duplicated names:
    Places (recovered):         62
    Constants:                   3
    Const parameters:            0
    Other/unknown:               0

OPTIMIZATION
────────────
  Of 63 invocations with duplicates:
    Fully recovered                          60  ( 95.2%)
    Partially recovered                       0  (  0.0%)
    Not recovered                             3  (  4.8%)

  Arg slots: 73 recovered of 76 total  ( 96.1%)

  Size (argument arrays only):
    Old world (current):     298,528 bytes
    No dedup:                299,744 bytes  (+1,216  (+0.41%))
    Optimized:               298,576 bytes  (+48  (+0.02%))

  Optimization recovers 96.1% of the size cost.

@traviscross
Copy link
Copy Markdown
Contributor

traviscross commented Mar 27, 2026

Based on an instrumented top-10k crater run (in #154205, which includes 10,885 crates), here's what I found. Excluding 22 outlier crates, except where mentioned:

Only 1.8% of crates and 0.1% of format_args! invocations have any duplicates at all. Of invocations that use implicit capturing, 0.8% have duplicates.

The median per-crate cost of not deduplicating at all is zero bytes. Considering only the 194 affected crates, the median cost is 32 bytes (mean: 46 bytes). The total cost across all these crates, summed together, is under 9KB.

Including the 22 outliers, the total cost (summed across all 216 affected crates) is under 28KB.

With dianne's optimization, only 27 non-outlier and 37 total crates are affected, with a total cost (summed across all affected crates) of under 1.4KB and 7.4KB, respectively.

The outlier crate most affected by turning off deduplication is hddsgen at 7KB. Every single duplicate for this crate, though, is a place, so dianne's optimization would drop this cost to zero. This crate was first published 27 days ago.

The next-largest outlier crate is pikpaktui, first published 6 weeks ago, at 2.8KB. This one doesn't benefit at all from the optimization.

I don't mean any judgment by saying this, but these two and many earlier outlier crates I looked at seem heavily AI-generated. There's something about the way the models write format strings (and the number of them they write), at least in these outlier cases, that seems different to me than what humans do.

Anyway, the full report is below. For my part, I judge the practical cost of not deduplicating as ε-zero.


format_args! Deduplication Analysis
====================================

  Source: 356,205 unique invocations across 10,885 crates
  (721,552 raw lines; dedup ratio: 2.03x)
  Dedup method: loc-based (file:line:col)

BASELINE
────────
Total invocations                       356,205  (100.0%)
  Interpolating (ph > 0)                251,591  ( 70.6%)
  Fixed strings (ph = 0)                104,614  ( 29.4%)

  Arguments per invocation:
      0     104,614  ( 29.4%)  █████████████████
      1     185,180  ( 52.0%)  ██████████████████████████████
      2      45,765  ( 12.8%)  ███████
      3      12,532  (  3.5%)  ██
      4       4,579  (  1.3%)  █
      5       1,675  (  0.5%)  █
      6         822  (  0.2%)  █
      7         405  (  0.1%)  █
      8         206  (  0.1%)  █
     9+         427  (  0.1%)  █
    mean: 1.0  median: 1  p95: 3  max: 109

CAPTURE MECHANICS
─────────────────
  Of 251,591 interpolating invocations:
    Implicit capture                     58,571  ( 23.3%)
    Explicit named                        6,364  (  2.5%)
    Positional                          192,187  ( 76.4%)
    Width/precision arg                     662  (  0.3%)

DUPLICATION
───────────
  22 outlier crates excluded; [brackets] include all.

  Invocations with duplicate captures:
    of all                          404 [   837]  (  0.1% [  0.2%])
    of interpolating                404 [   837]  (  0.2% [  0.3%])
    of implicit-capture             404 [   837]  (  0.8% [  1.4%])

  Extra arg slots from duplication: 557 [1,736] total
    mean: 1.4 [2.1] per affected invocation

  Extra slots per invocation (all crates):
      0           0  (  0.0%)
      1         531  ( 63.4%)  ██████████████████████████████
      2         155  ( 18.5%)  █████████
      3          43  (  5.1%)  ██
      4          40  (  4.8%)  ██
      5          17  (  2.0%)  █
      6           8  (  1.0%)  █
      7           9  (  1.1%)  █
      8          10  (  1.2%)  █
     9+          24  (  2.9%)  █

  Multiplicity of duplicated names (434 [982] total):
    2x                              354 [   665]  ( 81.6% [ 67.7%])
    3x                               50 [   193]  ( 11.5% [ 19.7%])
    4+x                              30 [   124]  (  6.9% [ 12.6%])

  Resolution of duplicated names:
    Places (recovered):        365 [733]
    Constants:                  69 [249]
    Const parameters:            0 [0]
    Other/unknown:               0 [0]

OPTIMIZATION
────────────
  22 outlier crates excluded; [brackets] include all.

  Of 404 [837] invocations with duplicates:
    Fully recovered                 338 [   660]  ( 83.7% [ 78.9%])
    Partially recovered               3 [     3]  (  0.7% [  0.4%])
    Not recovered                    63 [   174]  ( 15.6% [ 20.8%])

  Arg slots: 471 [1,276] recovered of 557 [1,736] total  ( 84.6% [ 73.5%])

  Size (argument arrays only):
                                          Excl. outliers            All crates
    Old world (current stable):              5,365,040 B           5,667,232 B
    No dedup (PR #149926):             +8,912 B (+0.17%)    +27,776 B (+0.49%)
    Optimized (PR #152480):            +1,376 B (+0.03%)     +7,360 B (+0.13%)

  Optimization recovers 84.6% [73.5%] of the size cost.

PER-CRATE
─────────
  22 outlier crates excluded; [brackets] include all.

  Crates w/ duplication             194 [   216]  (  1.8% [  2.0%])
    Fully covered by opt            167 [   179]  ( 86.1% [ 82.9%])
    Partially covered                 8 [    14]  (  4.1% [  6.5%])
    Not covered                      19 [    23]  (  9.8% [ 10.6%])

  Dup format strings per affected crate (all crates):
      0           0  (  0.0%)
      1         113  ( 52.3%)  ██████████████████████████████
      2          43  ( 19.9%)  ███████████
      3           9  (  4.2%)  ██
      4           8  (  3.7%)  ██
      5           8  (  3.7%)  ██
      6           9  (  4.2%)  ██
      7           3  (  1.4%)  █
      8           8  (  3.7%)  ██
     9+          15  (  6.9%)  ████
    mean: 3.9  median: 1  p95: 12  max: 136

  Per-crate size increase (excl. outliers):
    Without optimization (PR #149926 alone):
      Crates:  194
      Mean:    46 B
      Median:  32 B
      p95:     144 B
      Max:     176 B
      Total:   8,912 B
    With dianne's optimization (PR #152480):
      Crates:  27
      Mean:    51 B
      Median:  32 B
      p95:     139 B
      Max:     176 B
      Total:   1,376 B

  Per-crate size increase (all crates):
    Without optimization (PR #149926 alone):
      Crates:  216
      Mean:    129 B
      Median:  32 B
      p95:     264 B
      Max:     7,184 B
      Total:   27,776 B
    With dianne's optimization (PR #152480):
      Crates:  37
      Mean:    199 B
      Median:  48 B
      p95:     797 B
      Max:     2,784 B
      Total:   7,360 B

  Top crates by residual cost (optimized vs. old):
  Crate                                     Total   w/Dup   Residual       Cost
  ──────────────────────────────────────── ──────  ──────  ─────────  ─────────
  scripty                                      90      11       176B       176B
  media_controller                             29       9       144B       144B
  refine                                      151       1       128B       176B
  claude_statusline_config                    158       6        96B        96B
  qserve                                       24       1        80B       128B
  dog                                          28       4        64B        64B
  llmnop                                       38       4        64B        64B
  mevlog                                      323       2        64B        64B
  sec                                          76       2        64B        64B
  secry                                        76       2        64B        64B
  yarsi                                        10       4        64B        64B
  bridge_echo                                  66       2        48B        64B
  libgo                                        23       3        48B        48B
  chectarine                                   48       1        32B        32B
  find_sqlite                                  17       2        32B        32B
  technique                                   134       1        32B        32B
  EZDB                                        335       1        16B        16B
  cargo_playdate                              237       1        16B        16B
  cargo_vstyle                                269       2        16B        32B
  crosslink                                 2,766       4        16B        64B

  Top crates by total cost (no dedup vs. old):
  Crate                                     Total   w/Dup       Cost   Residual
  ──────────────────────────────────────── ──────  ──────  ─────────  ─────────
  ask_bayes                                    52       5       176B         0B
  cadar                                       400       6       176B         0B
  refine                                      151       1       176B       128B
  scripty                                      90      11       176B       176B
  dataviz                                     103      10       160B         0B
  function_grep                                 8       5       160B         0B
  mdbook_mermaid_animate                       53       2       160B         0B
  crowbook                                    298       6       144B         0B
  media_controller                             29       9       144B       144B
  mollendorff_forge                         1,275       6       144B         0B
  tissue                                        5       2       144B         0B
  agnix_lsp                                   109       8       128B         0B
  ati                                         748       6       128B         0B
  baibot                                      525       8       128B         0B
  colorgen_nvim                                26       7       128B         0B
  gonidium                                    224       8       128B         0B
  lumen_sqlite_mcp                            204       5       128B         0B
  qserve                                       24       1       128B        80B
  subxt                                       165       5       128B         0B
  visualize_boost_pads                          3       2       128B         0B

OUTLIER ANALYSIS
────────────────
  Detection: IQR method on per-crate total cost (no dedup vs. old)
  Q1: 16 B  Q3: 84 B  IQR: 68 B  Threshold: Q3 + 1.5*IQR = 186 B

  22 outlier crates excluded from figures above:
    command                                  cost:    192 B  residual:    192 B  (8 dup fmt strings)
    actr_cli                                 cost:    208 B  residual:     16 B  (2 dup fmt strings)
    party                                    cost:    208 B  residual:      0 B  (7 dup fmt strings)
    ralph_workflow                           cost:    208 B  residual:      0 B  (8 dup fmt strings)
    michelson_ast                            cost:    224 B  residual:      0 B  (6 dup fmt strings)
    neuralnyx                                cost:    224 B  residual:      0 B  (6 dup fmt strings)
    ureeves_wasmtime                         cost:    224 B  residual:      0 B  (8 dup fmt strings)
    aws_mock                                 cost:    240 B  residual:      0 B  (8 dup fmt strings)
    fb2epub                                  cost:    240 B  residual:     80 B  (11 dup fmt strings)
    interthread                              cost:    256 B  residual:      0 B  (8 dup fmt strings)
    oxker                                    cost:    256 B  residual:     48 B  (15 dup fmt strings)
    build_script_build                       cost:    288 B  residual:      0 B  (13 dup fmt strings)
    nectar                                   cost:    320 B  residual:      0 B  (20 dup fmt strings)
    rift_lint                                cost:    352 B  residual:    352 B  (12 dup fmt strings)
    ci_config_tests                          cost:    512 B  residual:     16 B  (25 dup fmt strings)
    beast                                    cost:    688 B  residual:    480 B  (17 dup fmt strings)
    leak                                     cost:    704 B  residual:    656 B  (23 dup fmt strings)
    superttt                                 cost:  1,056 B  residual:      0 B  (2 dup fmt strings)
    lib_flutter_rust_bridge_codegen          cost:  1,136 B  residual:      0 B  (39 dup fmt strings)
    incodoc_ssg                              cost:  1,360 B  residual:  1,360 B  (25 dup fmt strings)
    pikpaktui                                cost:  2,784 B  residual:  2,784 B  (34 dup fmt strings)
    hddsgen                                  cost:  7,184 B  residual:      0 B  (136 dup fmt strings)

SUMMARY
───────
  Across 10,885 crates with 356,205 unique format_args! invocations
  (22 outlier crates excluded; [brackets] include all):

  -   0.1% [  0.2%] of format strings have duplicated captures (404 [837]).
  -   0.8% [  1.4%] of implicit-capture format strings have duplicated captures.
  - 194 [216] crates (  1.8% [  2.0%]) have at least one format string with duplication.

  - dianne's optimization recovers  84.6% [ 73.5%] of all duplicate arg slots (471 [1,276] of 557 [1,736]).
  - Residual cost: 1,376 [7,360] bytes across the ecosystem.

  - 167 [179] of 194 [216] affected crates ( 86.1% [ 82.9%]) are fully covered by the optimization.
  - Constants (not recoverable): 69 [249] unique duplicated constant names.

@Skgland
Copy link
Copy Markdown
Contributor

Skgland commented Mar 27, 2026

Based on an instrumented top-10k crater run (in #154205, which includes 10,885 crates), here's what I found.

For awareness there is a crater issue regarding top-{n} not actually testing the top-n crates:
rust-lang/crater#813

@theemathas
Copy link
Copy Markdown
Contributor

checks

Yup, it doesn't even compile majorly-used crates like serde or libc. The data is invalid. 🫠

@traviscross
Copy link
Copy Markdown
Contributor

OK. I'll schedule a full crater run then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-nominated Nominated for discussion during a lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. I-libs-api-nominated Nominated for discussion during a libs-api team meeting. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

format_args deduplicates consts with interior mutability or destructor