Skip to content
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

Tracking issue for improving std::fmt::Arguments and format_args!() #99012

Open
40 of 50 tasks
m-ou-se opened this issue Jul 7, 2022 · 32 comments
Open
40 of 50 tasks

Tracking issue for improving std::fmt::Arguments and format_args!() #99012

m-ou-se opened this issue Jul 7, 2022 · 32 comments
Assignees
Labels
A-fmt Area: `core::fmt` C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC I-heavy Issue: Problems and improvements with respect to binary size of generated code. I-slow Issue: Problems and improvements with respect to performance of generated code. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@m-ou-se
Copy link
Member

m-ou-se commented Jul 7, 2022

Earlier this year in the libs team meeting, I presented several different ideas for alternative implementations of std::fmt::Arguments which could result in smaller binary size or higher performance. Now that #93740 is mostly done, I'll be shifting my focus to fmt::Arguments and exploring those ideas.

Currently, fmt::Arguments is the size of six pointers, and refers to three slices:

  • A &'static [&'static str] containing the literal parts around the formatting placeholders. E.g. for "a{}b{}c", these are ["a", "b", "c"].
  • A &[&(ptr, fn_ptr)] which is basically a &[&dyn Display] (but can point to Debug or Hex etc. too), pointing to the arguments. This one is not 'static, as it points to the actual arguments to be formatted.
  • A Option<&'static [FmtArgument]>, where FmtArgument is a struct containing all the options like precision, width, alignment, fill character, etc. This is unused (None) when all placeholders have no options, like in "{} {}", but is used and filled in for all place holders as soon as any placeholder uses any options, like in "{:.5} {}".

Here's a visualisation of that, for a "a{}b{:.5}c" format string:

Diagram

An important part of this design is that most of it can be stored in static storage, to minimize the amount of work that a function that needs to create/pass a fmt::Arguments needs to do. It can just refer to the static data, and only fill in a slice of the arguments.

Some downsides:

  • A fmt::Arguments is still relatively big (six pointers in size), and not a great type to pass by value. It could be just two pointers in size (one to static data, one to dynamic data), such that it fits in a register pair.
  • It costs quite a lot of static storage for some simple format strings. For example, "a{}b{}c" needs a &["a", "b", "c"], which is stored in memory as a (ptr, size) pair referencing three (ptr, size) pairs referencing one byte each, which is a lot of overhead. Small string literals with just a newline or a space are very common in formatting.
  • When even just a single formatting placeholder uses any non-standard options, such as "{:02x}", a relatively large array with all the (mostly default) formatting options is stored for all placeholders.
  • The non-static part that contains the pointers to the arguments contains the pointers to the relevant Display/Debug/etc. implementation as well, even though that second part is constant and could be static. (It's a bit tricky to split those, though.)
  • Even when formatting a simple &str argument with a simple "{}" placeholder, the full Display implementation for &str is pulled in, which include code for all the unused options like padding, alignment, etc.

Issues like those are often reason to avoid formatting in some situations, which is a shame.

None of these things are trivial to fix, and all involve a trade off between compile time, code size, runtime performance, and implementation complexity. It's also very tricky to make these tradeoffs for many different use cases at once, as the ways in which formatting is used in a program differs vastly per type of Rust program.

Still, there are many ideas that are worth exploring. It's hard to predict which one will end up being best, so this will involve several different implementations to test and benchmark.

I'll explain the different ideas one by one in the comments below as I explore them.


To do:

@m-ou-se m-ou-se added I-slow Issue: Problems and improvements with respect to performance of generated code. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-libs Relevant to the library team, which will review and decide on the PR/issue. A-fmt Area: `core::fmt` labels Jul 7, 2022
@m-ou-se m-ou-se self-assigned this Jul 7, 2022
@m-ou-se
Copy link
Member Author

m-ou-se commented Jul 7, 2022

Most likely, the best alternative will look vaguely like this:

Diagram 2

That is, it tries to:

  • Make fmt::Arguments as small as possible. (Two pointers.)
  • Move as much data as possible into static storage. (Everything except for the pointers to the arguments, which must be dynamic.)

The most interesting question is what goes in the block with the question marks, the static data that encodes the format string. (Perhaps it could even include the three strings, to save the overhead of the pointers and indirection, at least in the case of tiny strings like these.)

There are many ways to represent a format string in a data structure. There's a trade off to be made between the size of that structure, runtime performance, and complexity.

One of the ideas I want to explore more is to represent it as a single array of 'commands' such as print string "a" set precision to 5 print argument 0 etc. Effectively making a (tiny, trivial) domain specific 'virtual machine' with just ~10 instructions that's easily interpreted by std::fmt::write.

This direction is something I experimented a bit with in #84823, but still requires a lot more exploration to get it to something that can be a good alternative.

Another option we discussed in the libs meeting earlier this year, is to represent it as compiled code instead of a regular data structure. format_args!() would expand to a closure, and fmt::Arguments would effectively be a &dyn Fn. This allows for inlining and optimizing the Display implementations that it uses (which might result in smaller code size if much can be optimized away), but it also means that every single format_args!() will result in its own closure, which can blow up code size and compilation time for programs that do a lot of formatting.

Unfortunately, it takes quite some time to build every single experiment, as it requires a re-implementation of not only fmt::Arguments, but als of the format_args!() macro, every time.

@m-ou-se
Copy link
Member Author

m-ou-se commented Jul 7, 2022

An interesting case is something like format_args!("{} {}", arg0, "string literal"). Ideally, that would optimize to format_args!("{} string literal", arg0), which is an idea that's tracked here: #78356

However, if that second argument is a dynamically chosen &str, we cannot optimize it away, and we end up with an unfortunate amount of indirection thanks to how format_args!() captures all arguments by reference: it captures a &&str. That looks something like this:

Diagram 3

It means that the &str needs to be temporarily placed on the stack such that a &&str can reference it. Additionally, it's unfortunate that the entire String as Display implementation is included, even though just f.write_str() would've been enough, as no special formatting options are used.

If we go for the closure idea, we could instead get something that looks like this:

Diagram 4

So,

  • A compiler-generated structure for the captures, since we now simply make use of the capturing behavior of a regular closure. It could decide to capture a copy of &str instead of requiring a &&str. (It could even just reference the outer stack frame, such that the 'captures' structure is simply the stack frame that already exists.)
  • It could inline <String as Display>::fmt and optimize away nearly all of it. (Alternatively, we could teach the builtin format_args!() macro to generate a simple write_str call for a "{}"-formatted &str, instead of a call to Display::fmt.)

@m-ou-se
Copy link
Member Author

m-ou-se commented Jul 7, 2022

A tricky problem with the closure approach, is how to implement fmt::Arguments::as_str(). It's not impossible, but it's a tricky to encode a special case for fmt::Arguments that are just a single static string literal, if fmt::Arguments just encode something like a &dyn Fn. (Without making the structure larger again. It'd be really nice if it fits in a register pair.)

@m-ou-se
Copy link
Member Author

m-ou-se commented Jul 7, 2022

If we do not go for the closure approach, a tricky problem is how to make format_args!("{}", a) expand to include the right <T as Display>::fmt function pointer in the static data, since we don't have a typeof(a) operator in Rust.

Currently we just make use of something like &dyn Display which handles that for us, but that means that the function pointer ends up in the dynamic part together with the pointer to the argument (as part of a wide pointer), instead of separately as part of the static data.

One possibility of handling this is described in #44343, but it's quite tricky to get that right. The last comment there is a comment from me from 2020 saying that I'd try to implement it. I tried it, but it turned out to be quite tricky to get right and make it compile in all cases, and it got messy quite fast. Specifically, creating just a [fn(*const Opaque); N] with that approach works out okay-ish, but creating a structure that also contains other information (flags, precision, string pieces, etc.) gets quite complicated. Perhaps there's an easier approach possible nowadays, maybe with the help of some (to be created) (unstable) const eval related feature.

@joboet
Copy link
Member

joboet commented Jul 8, 2022

In the VM approach, the "opcodes" can be encoded together with the static string: since in UTF-8, byte values >= 0xf5 are not allowed, they can be used to encode up to 10 opcodes, which could either specify formatting options or indicate that the next bytes are a pointer to a formatting function. Additionally, by using an "end" opcode, the string pointer does not need to include size, while at the same time allowing internal NUL-bytes.

@kadiwa4
Copy link
Contributor

kadiwa4 commented Jul 9, 2022

What about putting all the &'static strs into a single one and then only storing the index where each of the parts end? So for format!("a{}bc{}d", …) there would be a string literal "abcd", stored as a thin pointer to the first byte, and one end index for each slice that needs to be extracted from the string ([1, 3, 4] in this case; to extract "a", "bc" and "d").

This would almost cut the size of the current &'static [&'static str] in half.

@Amanieu
Copy link
Member

Amanieu commented Jul 13, 2022

A tricky problem with the closure approach, is how to implement fmt::Arguments::as_str(). It's not impossible, but it's a tricky to encode a special case for fmt::Arguments that are just a single static string literal, if fmt::Arguments just encode something like a &dyn Fn. (Without making the structure larger again. It'd be really nice if it fits in a register pair.)

Would using a custom trait instead of Fn help here? It would have 2 methods: one which corresponds to the original Fn and one which returns an Option<&'static str>.

@m-ou-se
Copy link
Member Author

m-ou-se commented Aug 8, 2022

I've opened a compiler MCP for changing how format_args!() expands, to make it easier to work on these improvements.

@m-ou-se
Copy link
Member Author

m-ou-se commented Aug 8, 2022

The implementation of the format_args!() builtin macro (compiler/rustc_builtin_macros/src/format.rs) currently takes the output of rustc_parse_format and processes it, resolves numeric and named arguments (e.g. matches {a} to a = 1, etc.), figures out the right display trait (e.g. UpperHex for {:X}) produces diagnostics about invalid options or missing arguments (etc.), and generates the code that format_args!() will expand to that will eventually create a fmt::Arguments object.

It's a lot of code.

Any new implementation of fmt::Arguments will need big modifications to this code.

I'm now working on splitting this code into a part into two steps:

  1. Resolving the arguments, resolving the display trait, producing diagnostics/errors, etc.
  2. Generating the code.

Between step 1 and 2, there will be a (relatively simple) intermediate representation. Then a new implementation of fmt::Arguments only needs a new implementation of step 2: converting this intermediate representation to code. Then we can leave the first step alone.

Currently, these two steps are quite interwoven, so it'll take some time and effort to separate.

Additionaly, I think it'd be nice if we could delay step 2 until later in the compilation process, making the intermediate representation part of the AST and HIR, which is what my opened an MCP for: rust-lang/compiler-team#541

@m-ou-se
Copy link
Member Author

m-ou-se commented Sep 1, 2022

I made a template PR for those who want to help out by experimenting with a new fmt::Arguments implementation: #101272

Unfortunately, it's a lot of work to try out a new idea for a fmt::Arguments implementation, since it requires not only updating the fmt::Arguments struct and its methods, but also a new core::fmt::write() implementation, and a new format_args!() builtin macro implementation.

The template PR provides a starting point that points out exactly what to implement.

I'll work on getting #100996 merged (which is for now included in the template PR), and will start on the closure approach right after. Please feel free to try out another approach (like that 'VM' idea or something else) and leave a comment here with what you're working on, and ping me when you have something ready to test. :)

There's a few benchmarks here thanks to @mojave2. It'd be nice if we could also include a representative performance benchmark for formatting in https://perf.rust-lang.org/ or at least as an optional #[bench] test in rust-lang/rust, so we have a good way to measure results.

@Kobzol
Copy link
Contributor

Kobzol commented Sep 1, 2022

We're now in the process of adding an experimental version of runtime benchmarks into rustc-perf (rust-lang/rustc-perf#1423), I'll try to add some fmt benchmarks once/if it gets merged.

@m-ou-se
Copy link
Member Author

m-ou-se commented Sep 13, 2022

Some very promising first results from the closure approach: #101568 (comment)

@m-ou-se
Copy link
Member Author

m-ou-se commented Sep 27, 2022

Update: The compiler MCP for making format_args!() its own ast node has been accepted, and #100996 has been reviewed and approved, and is about to be merged.

The closure approach produces some great results for small programs, but doesn't perform as well in all situations. It also can significantly increase compilation time.

Once #100996 is merged, I'll be much easier to make changes to fmt::Arguments. I'll start with a few small incremental changes that should be easy to review, before continuing with more experimental ideas again.

Now that the compiler MCP has been accepted, I'll also work on moving the data types from ast.rs of #100996 into the actual AST, moving the 'actual' expansion to a later point in the compilation process, to kickstart the work on #78356.

bors added a commit to rust-lang-ci/rust that referenced this issue Sep 28, 2022
Rewrite and refactor format_args!() builtin macro.

This is a near complete rewrite of `compiler/rustc_builtin_macros/src/format.rs`.

This gets rid of the massive unmaintanable [`Context` struct](https://github.com/rust-lang/rust/blob/76531befc4b0352247ada67bd225e8cf71ee5686/compiler/rustc_builtin_macros/src/format.rs#L176-L263), and splits the macro expansion into three parts:

1. First, `parse_args` will parse the `(literal, arg, arg, name=arg, name=arg)` syntax, but doesn't parse the template (the literal) itself.
2. Second, `make_format_args` will parse the template, the format options, resolve argument references, produce diagnostics, and turn the whole thing into a `FormatArgs` structure.
3. Finally, `expand_parsed_format_args` will turn that `FormatArgs` structure into the expression that the macro expands to.

In other words, the `format_args` builtin macro used to be a hard-to-maintain 'single pass compiler', which I've split into a three phase compiler with a parser/tokenizer (step 1), semantic analysis (step 2), and backend (step 3). (It's compilers all the way down. ^^)

This can serve as a great starting point for rust-lang#99012, which will only need to change the implementation of 3, while leaving step 1 and 2 unchanged.

It also makes rust-lang/compiler-team#541 easier, which could then upgrade the new `FormatArgs` struct to an `ast` node and remove step 3, moving that step to later in the compilation process.

It also fixes a few diagnostics bugs.

This also [significantly reduces](https://gist.github.com/m-ou-se/b67b2d54172c4837a5ab1b26fa3e5284) the amount of generated code for cases with arguments in non-default order without formatting options, like `"{1} {0}"` or `"{a} {}"`, etc.
github-actions bot pushed a commit to rust-lang/miri that referenced this issue Mar 6, 2024
perf: improve write_fmt to handle simple strings

In case format string has no arguments, simplify its implementation with a direct call to `output.write_str(value)`. This builds on `@dtolnay` original [suggestion](serde-rs/serde#2697 (comment)). This does not change any expectations because the original `fn write()` implementation calls `write_str` for parts of the format string.

```rust
write!(f, "text")  ->  f.write_str("text")
```

```diff
 /// [`write!`]: crate::write!
+#[inline]
 #[stable(feature = "rust1", since = "1.0.0")]
 pub fn write(output: &mut dyn Write, args: Arguments<'_>) -> Result {
+    if let Some(s) = args.as_str() { output.write_str(s) } else { write_internal(output, args) }
+}
+
+/// Actual implementation of the [`write`], but without the simple string optimization.
+fn write_internal(output: &mut dyn Write, args: Arguments<'_>) -> Result {
     let mut formatter = Formatter::new(output);
     let mut idx = 0;
```

* Hopefully it will improve the simple case for the rust-lang/rust#99012
* Another related (original?) issues #10761
* Previous similar attempt to fix it by by `@Kobzol` #100700

CC: `@m-ou-se` as probably the biggest expert in everything `format!`
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Apr 7, 2024
perf: improve write_fmt to handle simple strings

In case format string has no arguments, simplify its implementation with a direct call to `output.write_str(value)`. This builds on `@dtolnay` original [suggestion](serde-rs/serde#2697 (comment)). This does not change any expectations because the original `fn write()` implementation calls `write_str` for parts of the format string.

```rust
write!(f, "text")  ->  f.write_str("text")
```

```diff
 /// [`write!`]: crate::write!
+#[inline]
 #[stable(feature = "rust1", since = "1.0.0")]
 pub fn write(output: &mut dyn Write, args: Arguments<'_>) -> Result {
+    if let Some(s) = args.as_str() { output.write_str(s) } else { write_internal(output, args) }
+}
+
+/// Actual implementation of the [`write`], but without the simple string optimization.
+fn write_internal(output: &mut dyn Write, args: Arguments<'_>) -> Result {
     let mut formatter = Formatter::new(output);
     let mut idx = 0;
```

* Hopefully it will improve the simple case for the rust-lang/rust#99012
* Another related (original?) issues rust-lang#10761
* Previous similar attempt to fix it by by `@Kobzol` #100700

CC: `@m-ou-se` as probably the biggest expert in everything `format!`
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 20, 2024
Rewrite and refactor format_args!() builtin macro.

This is a near complete rewrite of `compiler/rustc_builtin_macros/src/format.rs`.

This gets rid of the massive unmaintanable [`Context` struct](https://github.com/rust-lang/rust/blob/76531befc4b0352247ada67bd225e8cf71ee5686/compiler/rustc_builtin_macros/src/format.rs#L176-L263), and splits the macro expansion into three parts:

1. First, `parse_args` will parse the `(literal, arg, arg, name=arg, name=arg)` syntax, but doesn't parse the template (the literal) itself.
2. Second, `make_format_args` will parse the template, the format options, resolve argument references, produce diagnostics, and turn the whole thing into a `FormatArgs` structure.
3. Finally, `expand_parsed_format_args` will turn that `FormatArgs` structure into the expression that the macro expands to.

In other words, the `format_args` builtin macro used to be a hard-to-maintain 'single pass compiler', which I've split into a three phase compiler with a parser/tokenizer (step 1), semantic analysis (step 2), and backend (step 3). (It's compilers all the way down. ^^)

This can serve as a great starting point for rust-lang/rust#99012, which will only need to change the implementation of 3, while leaving step 1 and 2 unchanged.

It also makes rust-lang/compiler-team#541 easier, which could then upgrade the new `FormatArgs` struct to an `ast` node and remove step 3, moving that step to later in the compilation process.

It also fixes a few diagnostics bugs.

This also [significantly reduces](https://gist.github.com/m-ou-se/b67b2d54172c4837a5ab1b26fa3e5284) the amount of generated code for cases with arguments in non-default order without formatting options, like `"{1} {0}"` or `"{a} {}"`, etc.
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 20, 2024
More core::fmt::rt cleanup.

- Removes the `V1` suffix from the `Argument` and `Flag` types.

- Moves more of the format_args lang items into the `core::fmt::rt` module. (The only remaining lang item in `core::fmt` is `Arguments` itself, which is a public type.)

Part of rust-lang/rust#99012

Follow-up to rust-lang/rust#110616
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 27, 2024
Rewrite and refactor format_args!() builtin macro.

This is a near complete rewrite of `compiler/rustc_builtin_macros/src/format.rs`.

This gets rid of the massive unmaintanable [`Context` struct](https://github.com/rust-lang/rust/blob/76531befc4b0352247ada67bd225e8cf71ee5686/compiler/rustc_builtin_macros/src/format.rs#L176-L263), and splits the macro expansion into three parts:

1. First, `parse_args` will parse the `(literal, arg, arg, name=arg, name=arg)` syntax, but doesn't parse the template (the literal) itself.
2. Second, `make_format_args` will parse the template, the format options, resolve argument references, produce diagnostics, and turn the whole thing into a `FormatArgs` structure.
3. Finally, `expand_parsed_format_args` will turn that `FormatArgs` structure into the expression that the macro expands to.

In other words, the `format_args` builtin macro used to be a hard-to-maintain 'single pass compiler', which I've split into a three phase compiler with a parser/tokenizer (step 1), semantic analysis (step 2), and backend (step 3). (It's compilers all the way down. ^^)

This can serve as a great starting point for rust-lang/rust#99012, which will only need to change the implementation of 3, while leaving step 1 and 2 unchanged.

It also makes rust-lang/compiler-team#541 easier, which could then upgrade the new `FormatArgs` struct to an `ast` node and remove step 3, moving that step to later in the compilation process.

It also fixes a few diagnostics bugs.

This also [significantly reduces](https://gist.github.com/m-ou-se/b67b2d54172c4837a5ab1b26fa3e5284) the amount of generated code for cases with arguments in non-default order without formatting options, like `"{1} {0}"` or `"{a} {}"`, etc.
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 27, 2024
More core::fmt::rt cleanup.

- Removes the `V1` suffix from the `Argument` and `Flag` types.

- Moves more of the format_args lang items into the `core::fmt::rt` module. (The only remaining lang item in `core::fmt` is `Arguments` itself, which is a public type.)

Part of rust-lang/rust#99012

Follow-up to rust-lang/rust#110616
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 27, 2024
perf: improve write_fmt to handle simple strings

In case format string has no arguments, simplify its implementation with a direct call to `output.write_str(value)`. This builds on `@dtolnay` original [suggestion](serde-rs/serde#2697 (comment)). This does not change any expectations because the original `fn write()` implementation calls `write_str` for parts of the format string.

```rust
write!(f, "text")  ->  f.write_str("text")
```

```diff
 /// [`write!`]: crate::write!
+#[inline]
 #[stable(feature = "rust1", since = "1.0.0")]
 pub fn write(output: &mut dyn Write, args: Arguments<'_>) -> Result {
+    if let Some(s) = args.as_str() { output.write_str(s) } else { write_internal(output, args) }
+}
+
+/// Actual implementation of the [`write`], but without the simple string optimization.
+fn write_internal(output: &mut dyn Write, args: Arguments<'_>) -> Result {
     let mut formatter = Formatter::new(output);
     let mut idx = 0;
```

* Hopefully it will improve the simple case for the rust-lang/rust#99012
* Another related (original?) issues rust-lang#10761
* Previous similar attempt to fix it by by `@Kobzol` #100700

CC: `@m-ou-se` as probably the biggest expert in everything `format!`
bors added a commit to rust-lang-ci/rust that referenced this issue Oct 13, 2024
…<try>

Evaluate `std::fmt::Arguments::new_const()` during Compile Time

Fixes rust-lang#128709

This PR aims to optimize calls to string formating macros without any arguments by evaluating `std::fmt::Arguments::new_const()` in a const context.

Currently,
`println!("hola")` compiles to `std::io::_print(std::fmt::Arguments::new_const(&["hola\n"]))`.

With this PR,
`println!("hola")` compiles to `std::io::_print(const { std::fmt::Arguments::new_const(&["hola\n"]) })`.

This is accomplished in two steps:

1.  Const stabilize `std::fmt::Arguments::new_const()`.
2.  Wrap calls to `std::fmt::Arguments::new_const()` in an inline const block when lowering the AST to HIR.

This reduces the generated code to a `memcpy` instead of multiple `getelementptr` and `store` instructions even with `-C no-prepopulate-passes -C opt-level=0`. Godbolt for code comparison: https://rust.godbolt.org/z/P7Px7de6c

This is a safe and sound transformation because `std::fmt::Arguments::new_const()` is a trivial constructor function taking a slice containing a `'static` string literal as input.

CC rust-lang#99012
bors added a commit to rust-lang-ci/rust that referenced this issue Oct 14, 2024
…<try>

Evaluate `std::fmt::Arguments::new_const()` during Compile Time

Fixes rust-lang#128709

This PR aims to optimize calls to string formating macros without any arguments by evaluating `std::fmt::Arguments::new_const()` in a const context.

Currently,
`println!("hola")` compiles to `std::io::_print(std::fmt::Arguments::new_const(&["hola\n"]))`.

With this PR,
`println!("hola")` compiles to `std::io::_print(const { std::fmt::Arguments::new_const(&["hola\n"]) })`.

This is accomplished in two steps:

1.  Const stabilize `std::fmt::Arguments::new_const()`.
2.  Wrap calls to `std::fmt::Arguments::new_const()` in an inline const block when lowering the AST to HIR.

This reduces the generated code to a `memcpy` instead of multiple `getelementptr` and `store` instructions even with `-C no-prepopulate-passes -C opt-level=0`. Godbolt for code comparison: https://rust.godbolt.org/z/P7Px7de6c

This is a safe and sound transformation because `std::fmt::Arguments::new_const()` is a trivial constructor function taking a slice containing a `'static` string literal as input.

CC rust-lang#99012
bors added a commit to rust-lang-ci/rust that referenced this issue Oct 28, 2024
…<try>

Evaluate `std::fmt::Arguments::new_const()` during Compile Time

Fixes rust-lang#128709

This PR aims to optimize calls to string formating macros without any arguments by evaluating `std::fmt::Arguments::new_const()` in a const context.

Currently,
`println!("hola")` compiles to `std::io::_print(std::fmt::Arguments::new_const(&["hola\n"]))`.

With this PR,
`println!("hola")` compiles to `std::io::_print(const { std::fmt::Arguments::new_const(&["hola\n"]) })`.

This is accomplished by wrapping calls to `std::fmt::Arguments::new_const()` in an inline const block when lowering the AST to HIR.

This reduces the generated code to a `memcpy` instead of multiple `getelementptr` and `store` instructions even with `-C no-prepopulate-passes -C opt-level=0`. Godbolt for code comparison: https://rust.godbolt.org/z/P7Px7de6c

This is a safe and sound transformation because `std::fmt::Arguments::new_const()` is a trivial constructor function taking a slice containing a `'static` string literal as input.

CC rust-lang#99012
bors added a commit to rust-lang-ci/rust that referenced this issue Feb 20, 2025
Experiment: New format_args!() representation

This is part of rust-lang#99012

// TODO: description
jieyouxu added a commit to jieyouxu/rust that referenced this issue Mar 10, 2025
…=scottmcm

Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
jieyouxu added a commit to jieyouxu/rust that referenced this issue Mar 10, 2025
…=scottmcm

Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
jieyouxu added a commit to jieyouxu/rust that referenced this issue Mar 11, 2025
…=scottmcm

Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
bors added a commit to rust-lang-ci/rust that referenced this issue Mar 11, 2025
…cottmcm

Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
bors added a commit to rust-lang-ci/rust that referenced this issue Mar 12, 2025
Reduce FormattingOptions to 64 bits

This is part of rust-lang#99012

This reduces FormattingOptions from 6-7 machine words (384 bits on 64-bit platforms, 224 bits on 32-bit platforms) to just 64 bits (a single register on 64-bit platforms).

Before:

```rust
pub struct FormattingOptions {
    flags: u32, // only 6 bits used
    fill: char,
    align: Option<Alignment>,
    width: Option<usize>,
    precision: Option<usize>,
}
```

After:

```rust
pub struct FormattingOptions {
    /// Bits:
    ///  - 0-20: fill character (21 bits, a full `char`)
    ///  - 21: `+` flag
    ///  - 22: `-` flag
    ///  - 23: `#` flag
    ///  - 24: `0` flag
    ///  - 25: `x?` flag
    ///  - 26: `X?` flag
    ///  - 27: Width flag (if set, the width field below is used)
    ///  - 28: Precision flag (if set, the precision field below is used)
    ///  - 29-30: Alignment (0: Left, 1: Right, 2: Center, 3: Unknown)
    ///  - 31: Always set to 1
    flags: u32,
    /// Width if width flag above is set. Otherwise, always 0.
    width: u16,
    /// Precision if precision flag above is set. Otherwise, always 0.
    precision: u16,
}
```
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this issue Mar 14, 2025
…cottmcm

Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Mar 17, 2025
Reduce formatting `width` and `precision` to 16 bits

This is part of rust-lang/rust#99012

This is reduces the `width` and `precision` fields in format strings to 16 bits. They are currently full `usize`s, but it's a bit nonsensical that we need to support the case where someone wants to pad their value to eighteen quintillion spaces and/or have eighteen quintillion digits of precision.

By reducing these fields to 16 bit, we can reduce `FormattingOptions` to 64 bits (see rust-lang/rust#136974) and improve the in memory representation of `format_args!()`. (See additional context below.)

This also fixes a bug where the width or precision is silently truncated when cross-compiling to a target with a smaller `usize`. By reducing the width and precision fields to the minimum guaranteed size of `usize`, 16 bits, this bug is eliminated.

This is a breaking change, but affects almost no existing code.

---

Details of this change:

There are three ways to set a width or precision today:

1. Directly a formatting string, e.g. `println!("{a:1234}")`
2. Indirectly in a formatting string, e.g. `println!("{a:width$}", width=1234)`
3. Through the unstable `FormattingOptions::width` method.

This PR:

- Adds a compiler error for 1. (`println!("{a:9999999}")` no longer compiles and gives a clear error.)
- Adds a runtime check for 2. (`println!("{a:width$}, width=9999999)` will panic.)
- Changes the signatures of the (unstable) `FormattingOptions::[get_]width` methods to use a `u16` instead.

---

Additional context for improving `FormattingOptions` and `fmt::Arguments`:

All the formatting flags and options are currently:

- The `+` flag (1 bit)
- The `-` flag (1 bit)
- The `#` flag (1 bit)
- The `0` flag (1 bit)
- The `x?` flag (1 bit)
- The `X?` flag (1 bit)
- The alignment (2 bits)
- The fill character (21 bits)
- Whether a width is specified (1 bit)
- Whether a precision is specified (1 bit)
- If used, the width (a full usize)
- If used, the precision (a full usize)

Everything except the last two can simply fit in a `u32` (those add up to 31 bits in total).

If we can accept a max width and precision of u16::MAX, we can make a `FormattingOptions` that is exactly 64 bits in size; the same size as a thin reference on most platforms.

If, additionally, we also limit the number of formatting arguments, we can also reduce the size of `fmt::Arguments` (that is, of a `format_args!()` expression).
lopopolo added a commit to artichoke/strftime-ruby that referenced this issue Mar 20, 2025
Rust PR rust-lang/rust#136932 (part of rust-lang/rust#99012) limited
format string width and precision to 16 bits, causing panics when
dynamic padding exceeds `u16::MAX`.

These tests validate handling excessively large widths discovered via
fuzzing in artichoke/strftime-ruby. They ensure correct, panic-free
behavior consistent with CRuby's `Time#strftime`.

Additionally add tests for width specifiers which exceed `INT_MAX` to
ensure they return the formatting string verbatim, which were among the
cases discussed in the upstream PR.

See:

- Upstream report: rust-lang/rust#136932 (comment)
- Proposed fix: rust-lang/rust#136932 (comment)
bors added a commit to rust-lang-ci/rust that referenced this issue Mar 22, 2025
Reduce FormattingOptions to 64 bits

This is part of rust-lang#99012

This reduces FormattingOptions from 6-7 machine words (384 bits on 64-bit platforms, 224 bits on 32-bit platforms) to just 64 bits (a single register on 64-bit platforms).

Before:

```rust
pub struct FormattingOptions {
    flags: u32, // only 6 bits used
    fill: char,
    align: Option<Alignment>,
    width: Option<usize>,
    precision: Option<usize>,
}
```

After:

```rust
pub struct FormattingOptions {
    /// Bits:
    ///  - 0-20: fill character (21 bits, a full `char`)
    ///  - 21: `+` flag
    ///  - 22: `-` flag
    ///  - 23: `#` flag
    ///  - 24: `0` flag
    ///  - 25: `x?` flag
    ///  - 26: `X?` flag
    ///  - 27: Width flag (if set, the width field below is used)
    ///  - 28: Precision flag (if set, the precision field below is used)
    ///  - 29-30: Alignment (0: Left, 1: Right, 2: Center, 3: Unknown)
    ///  - 31: Always set to 1
    flags: u32,
    /// Width if width flag above is set. Otherwise, always 0.
    width: u16,
    /// Precision if precision flag above is set. Otherwise, always 0.
    precision: u16,
}
```
bors added a commit to rust-lang-ci/rust that referenced this issue Mar 22, 2025
Reduce FormattingOptions to 64 bits

This is part of rust-lang#99012

This reduces FormattingOptions from 6-7 machine words (384 bits on 64-bit platforms, 224 bits on 32-bit platforms) to just 64 bits (a single register on 64-bit platforms).

Before:

```rust
pub struct FormattingOptions {
    flags: u32, // only 6 bits used
    fill: char,
    align: Option<Alignment>,
    width: Option<usize>,
    precision: Option<usize>,
}
```

After:

```rust
pub struct FormattingOptions {
    /// Bits:
    ///  - 0-20: fill character (21 bits, a full `char`)
    ///  - 21: `+` flag
    ///  - 22: `-` flag
    ///  - 23: `#` flag
    ///  - 24: `0` flag
    ///  - 25: `x?` flag
    ///  - 26: `X?` flag
    ///  - 27: Width flag (if set, the width field below is used)
    ///  - 28: Precision flag (if set, the precision field below is used)
    ///  - 29-30: Alignment (0: Left, 1: Right, 2: Center, 3: Unknown)
    ///  - 31: Always set to 1
    flags: u32,
    /// Width if width flag above is set. Otherwise, always 0.
    width: u16,
    /// Precision if precision flag above is set. Otherwise, always 0.
    precision: u16,
}
```
github-actions bot pushed a commit to rust-lang/rustc-dev-guide that referenced this issue Mar 24, 2025
Reduce FormattingOptions to 64 bits

This is part of rust-lang/rust#99012

This reduces FormattingOptions from 6-7 machine words (384 bits on 64-bit platforms, 224 bits on 32-bit platforms) to just 64 bits (a single register on 64-bit platforms).

Before:

```rust
pub struct FormattingOptions {
    flags: u32, // only 6 bits used
    fill: char,
    align: Option<Alignment>,
    width: Option<usize>,
    precision: Option<usize>,
}
```

After:

```rust
pub struct FormattingOptions {
    /// Bits:
    ///  - 0-20: fill character (21 bits, a full `char`)
    ///  - 21: `+` flag
    ///  - 22: `-` flag
    ///  - 23: `#` flag
    ///  - 24: `0` flag
    ///  - 25: `x?` flag
    ///  - 26: `X?` flag
    ///  - 27: Width flag (if set, the width field below is used)
    ///  - 28: Precision flag (if set, the precision field below is used)
    ///  - 29-30: Alignment (0: Left, 1: Right, 2: Center, 3: Unknown)
    ///  - 31: Always set to 1
    flags: u32,
    /// Width if width flag above is set. Otherwise, always 0.
    width: u16,
    /// Precision if precision flag above is set. Otherwise, always 0.
    precision: u16,
}
```
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Mar 24, 2025
Reduce FormattingOptions to 64 bits

This is part of rust-lang/rust#99012

This reduces FormattingOptions from 6-7 machine words (384 bits on 64-bit platforms, 224 bits on 32-bit platforms) to just 64 bits (a single register on 64-bit platforms).

Before:

```rust
pub struct FormattingOptions {
    flags: u32, // only 6 bits used
    fill: char,
    align: Option<Alignment>,
    width: Option<usize>,
    precision: Option<usize>,
}
```

After:

```rust
pub struct FormattingOptions {
    /// Bits:
    ///  - 0-20: fill character (21 bits, a full `char`)
    ///  - 21: `+` flag
    ///  - 22: `-` flag
    ///  - 23: `#` flag
    ///  - 24: `0` flag
    ///  - 25: `x?` flag
    ///  - 26: `X?` flag
    ///  - 27: Width flag (if set, the width field below is used)
    ///  - 28: Precision flag (if set, the precision field below is used)
    ///  - 29-30: Alignment (0: Left, 1: Right, 2: Center, 3: Unknown)
    ///  - 31: Always set to 1
    flags: u32,
    /// Width if width flag above is set. Otherwise, always 0.
    width: u16,
    /// Precision if precision flag above is set. Otherwise, always 0.
    precision: u16,
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-fmt Area: `core::fmt` C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC I-heavy Issue: Problems and improvements with respect to binary size of generated code. I-slow Issue: Problems and improvements with respect to performance of generated code. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests