Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions text/0000-manyregs-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
- Feature Name: manyregs_abi
- Start Date: 18-10-2026
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

## Summary
[summary]: #summary

Add an ABI (provisional name: "manyregs") that passes arguments in registers as much
as possible.

## Motivation
[motivation]: #motivation

Most of the time, passing arguments in registers is significantly faster than passing
them by stack.

At the moment, most ABIs only allow a limited number of registers to be used for
passing or returning arguments - especially for vector code.

The idea of the `extern "Rust_manyregs"` ABI is to allow for as many arguments as
allowed by the architecture to be passed directly in registers. This would be
especially relevant in specialized SIMD DSP kernels, but such function calls could
provide subtle speedups in other locations too.

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

An `extern "Rust_manyregs"` function uses as many registers as possible for argument
passing. This can be faster than the standard Rust ABI in some cases.

Unwinding through a "manyregs" function causes termination.

@RalfJung RalfJung Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes out of nowhere and is only explained later, making the RFC hard to read.

View changes since the review


## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The specifics of `Rust_manyregs` are architecture dependent and somewhat dependant on
what LLVM would allow. It is an internal ABI, and thus no explicit guarantees are made
(especially across compiler versions).

As this ABI passes vectors by registers, it is subject to the same correctness checks
as the extern "C" ABI - in particular, it is an error to pass an argument that would
be passed by register if a target feature were present if the feature is not present.

## Drawbacks
[drawbacks]: #drawbacks

It is one more ABI. It would need some amount of LLVM support.

If the ABI actually uses "as many registers as possible", some ISA extensions
(i.e. avx512 on x86) can extend the number of registers available. Thus,
whether avx512 is enabled on the function can change the ABI, which can lead to
UB.
Comment on lines +50 to +53

@tgross35 tgross35 Oct 18, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a huge thing. rustc currently allows building one crate with -Ctarget-feature=+avx512f and link that to a crate without it. So how do we avoid breaking extern "Rust_manyregs" function pointers?

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't even need multiple crates, #[target_feature] is enough.

I comment on this below in the RFC, but I have three possible solutions in mind (in order of personal preference):

  • only use 32 vector registers if there is an argument of type _mm512 or similar (if such an argument is present, and #[target_feature] does not contain avx512, the function will fail to compile -- and similarly callsites are checked)
  • stick to 16 GPR and 16 vector registers, always (IIRC effectively equivalent to LLVM's x86_regcall)
  • have extern Rust_manyregs-avx512

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it seems better to have the ABI depend on which ABI-changing target features you have in the extern "Rust_manyregs-feature1-feature2" ABI string (since that's part of the type of all functions/fnptrs) rather than what target features you enable with compile flags or #[target_feature]. e.g. extern "Rust_manyregs-apx". Obviously you then require those ABI-changing target features to be enabled in both the function and any callers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would be much better to start with the low hanging fruit, which is what can be done without language work. If we can use the registers available in the target's baseline then that already gets us quite a lot:

  • i586: 0 gpr -> 8 gpr
  • i686: 0 gpr, 0 vreg -> 8 gpr 8 vreg
  • x86-64: 6 gpr, 8 vreg -> 16 GPR, 16 vreg
  • aarch64: 8 GPR, 8 vreg -> 31 GPR, 32 vreg

(All of those probably have a few reserved GPRs)

That can be done to the default Rust ABI so everybody benefits without opting in, and doesn't even need an RFC or a new LLVM ABI (assuming LLVM's fastcc can be adapted). What's currently written up in this RFC needs a lot more design work on both the Rust and LLVM sides.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can do pass-by-value of ymm/zmm registers on x86 without breaking backwards compatibility (see https://github.com/rust-lang/rust/blob/b6eeb93c992027a03e1fd120b6652546396b142c/compiler/rustc_target/src/callconv/mod.rs#L799) - we could do that with a new opt-in ABI.

I don't believe fastcc exposes significantly more registers than the standard ccs - you get a couple more, but AFAIU (from a quick consultation of $LLM) the other biggest difference is that the callee cleans up the stack.

I'm also happy to just expose more ABIs currently existing in LLVM as a simpler initial approach, if we think that would be better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can do pass-by-value of ymm/zmm registers on x86 without breaking backwards compatibility (see https://github.com/rust-lang/rust/blob/b6eeb93c992027a03e1fd120b6652546396b142c/compiler/rustc_target/src/callconv/mod.rs#L799) - we could do that with a new opt-in ABI.

So, yes, but I'm not looking at only the described change. There are a few separate aspects of the Zulip discussion and what's in this RFC (from a few different people):

  1. Pass types that fit in baseline registers in registers.
  2. Return more values in registers.
  3. Pass more arguments in registers where the type fits into baseline registers, but there are enough args that it gets spilled to the stack instead.
  4. Use non-baseline features to indicate larger types like __m512d can be passed/returned in registers.
  5. Use non-baseline features to indicate more registers are available for passing/returning, e.g. AVX-512 implying 32 registers.

The status quo as of 1.99 https://rust.godbolt.org/z/Exo36YYKd:

  1. __m128 is currently passed indirectly in the Rust ABI
  2. (f64, f64) is returned in registers, something like (f64, f64, f64, f64) is not.
  3. A function that takes 7 int arguments or 9 floats will spill, even though there are free registers.
  4. Not yet possible with the Rust ABI (this RFC), but is the current behavior with extern "C"
  5. Not yet possible (this RFC)

This RFC is proposing new language features that are going to be rather difficult to specify and only help if you opt in. I think it's more useful to work on the first three items than the last two since they can apply to the default Rust ABI, meaning all users benefit, and it doesn't need any language changes.

I don't believe fastcc exposes significantly more registers than the standard ccs - you get a couple more, but AFAIU (from a quick consultation of $LLM) the other biggest difference is that the callee cleans up the stack.

It doesn't implement our ideal ABI at this time, but the description at https://llvm.org/docs/LangRef.html#calling-conventions sounds like its goal fits our needs. If the LLVM maintainers are open to adjusting it to pass and return with more registers, then that's a lot less effort than adding a new ABI.

E.g. I don't see why fastcc couldn't be regcall on x86 and something similar on other platforms.

I'm also happy to just expose more ABIs currently existing in LLVM as a simpler initial approach, if we think that would be better.

While I do think it would be nice to have the preserve_*, regcall, and vectorcall CCs, those are likely to help more with C compat than with performance. We'd still have to check improper_ctypes for example.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__m128 is currently passed indirectly in the Rust ABI

That's changing with Rust 1.100: rust-lang/rust#161583

@RalfJung RalfJung Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I comment on this below in the RFC, but I have three possible solutions in mind (in order of personal preference):

This is the main difficulty of the RFC and will need language-level work (as in, user-visible error messages). The RFC is IMO quite incomplete without a proposed solution of this issue, as the tradeoffs change significantly depending on the solution that is picked. This question is too central to delegate it to the future as an "unresolved question".



## Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

See the "motivation" section.

The naming of the ABI string could definitely be improved.

I propose disallowing unwinding through such functions because they are usually
performance-critical and do not unwind, but we could allow unwinding or we could

@nagisa nagisa Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide exact technical motivation/examples to warrant this limitation. My own handwavey intuition is that there is going to be little performance impact to functions that would not otherwise unwind anyway and setting up a handler to forcibly abort here can have a negative impact (which we deal with on extern "C" and the likes because it is important for soundness.)

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually have a strong opinion here - I had a handwavy feeling that the possibility of unwinding of such functions could produce worse asm, but I don't have any evidence.

add a `-unwind` variant.

We could have the Rust ABI behave this way. I believe this would break compatibility
in cases in which one passes by value register-like arguments to functions that don't
have the required target features.


## Prior art
[prior-art]: #prior-art

On x86, `x86_regcallcc` passes up to 16 integer / vector arguments via registers.

The proposed `rust_preserve_none_cc` ABI (https://github.com/rust-lang/rust/issues/151401)

@nagisa nagisa Oct 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this CC be implemented differently in the backend compared to rust_preserve_none_cc? LLVM supports this calling convention only for couple of targets. Would manyregs CC also use preserve_none calling convention? What about architectures which don't have implementations for this convention? Should the behaviour of manyregs be a guarantee or can it suffice as a hint?

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if a new ABI in this direction could be implemented without adding a new ABI to LLVM - I suspect not.

rust_preserve_none_cc is similar, but different in that it does not allow passing by value any more xmm registers than the standard C convention does.

On x86, the closest equivalent is x86_regcall, but even that convention doesn't use more than 16 XMM registers.

is somewhat similar in spirit.

## Unresolved questions
[unresolved-questions]: #unresolved-questions

- What should we do with ISAs that extend the set of available vector registers?
- Have a separate ABI string for those
- Limit the register count to the minimum guaranteed available
- Limit the register count to the minimum that the arguments guarantee is
available (i.e. 32 if a _mm512 argument is present)
- How should the ABI behave in terms of callee-saved / caller-saved registers?
- All registers are caller-saved
- Some number of registers are callee-saved unless they are used as arguments
- ???

## Future possibilities
[future-possibilities]: #future-possibilities

Does it make sense to have the "Rust" ABI behave in a similar way?