Force function calling convention to avoid using SSE/SIMD registers

Hi fellow Rustaceans,

TL;DR: Is there some way to specify that a function (either on the callee or caller side) cannot use SIMD (SSE) XMM registers in its calling convention?

As part of an unusual research OS project, I have two crates that run in the same address space:

  1. lib_reg: a crate that is compiled without SIMD support (soft_float target feature enabled), and exposes several public functions.
  2. lib_sse: a crate that is compiled with SIMD support (sse2 target feature enabled).

I want to call lib_reg's functions from the lib_sse crate.

However, the caller site in the lib_sse crate places arguments inside the XMM registers, but the callee function in the lib_reg crate expects those arguments to be placed in RDI, RSI, etc, so linking the two doesn’t work when they are compiled separately. Additionally, the XMM registers are sometimes used to pass the return value back from the callee function, which the lib_sse crate expects to happen, but the lib_reg crate cannot do.

A typical disassembly of the caller code in lib_sse looks like this:

movsd  -0x40(%rbp),%xmm0
movsd  -0x38(%rbp),%xmm1
movsd  -0x30(%rbp),%xmm2
movsd  -0x28(%rbp),%xmm3
callq  *%rcx   # calls a function in lib_reg

One potential solution to this would be to declare the lib_reg functions with a calling convention that stipulates that only normal registers (and stack space) could be used, and disallows using XMM registers for passing arguments. This could look something like:

extern "no_sse_regs" fn my_public_func(...)

However, no such calling convention exists, and when I compile the lib_sse crate with target_features = +sse2, the compiler is allowed to use SIMD registers for every single function in that crate. It’d be nice if I could specify which functions are allowed to use SIMD registers in their calling convention, and which ones aren’t.

I looked into the #[target_feature(enable = ...)] feature, but it doesn’t quite do what I need it to.

One way to circumvent this would be to use unsafe inline assembly blocks to, which is kind of what system calls require, i.e., manually placing arguments into specific registers, but that loses the language-level info like types, borrowing/lifetimes, mutability, etc. It’s also intractable to manually specify argument placement for hundreds of functions.

Any ideas on how to realize this? Thanks!

One way would be to use integers as parameters and transmute them to the desired type.

Thanks for the reply! Yes, I had considered something like that (syscall-esque calling conventions), but there are many other non-SIMD crates that also invoke functions in lib_reg, so I cannot change anything on the lib_reg side, because that would force all of its other callers to change their function signatures correspondingly. Then we lose type checking and other advantages of using Rust. It also introduces unsafety that I feel shouldn’t be necessary, in an ideal case.

Perhaps you could create a wrapper of the lib_reg functions just for lib_sse?

You can disable SSE for your build and enable it manually back for lib_sse functions, of course you’ll have to use #[inline(never)] for lib_reg functions. Here is an example:

In future we may get #[target_feature(disable="...")] which was specifically proposed for cases like yours.

Also maybe you will be able to build crates separately (with appropriate rustc options) and then manually link them. (see cargo build --verbose output)

1 Like

Also, rather than transmuting to integer you could pass by reference, reducing unsafe code but potentially adding overhead.

Thanks everyone. After speaking to some of the LLVM maintainers, I learned that disabling both SLP and loop autovectorization will remove the usage of SIMD registers from calling conventions, whilst still allowing the compiler to generate SIMD instructions within a function block. This works so long as you don’t use vector or float types in the actual function signature, which is sufficient for my use case.

1 Like