You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
x86-32 "f16" ABI needs SSE, incompatible with i586 targets #131819
So... for the "Rust" ABI we can do our own thing bypassing this (likely similar fo f32/f64, we should just always return f16 in via PassMode::Indirect), but what shall we do for the extern "C" ABI? We'll need a new dedicated compiler error (likely as part of the pass that we are adding here) that rejects returning f16 when the SSE registers are not available.
That will mean that libcore does not compile on i586 targets.... Actually libcore uses the "Rust" ABI so that one is fine. But if we have extern "C" float functions somewhere those would not be portable any more.
I think this will require LLVM changes to properly fix, as f16 is used as the return value of builtins; this is especially apparent when when combined with #[target_feature(enable = "sse2")], like in this example. LLVM will use whatever target features a function has to decide which ABI to use for a builtin, meaning that the no_sse function presumes that the result of __truncdfhf2 has been returned in ax whereas the yes_sse2 function presumes that the result has been returned in xmm0.
A similar issue can occur on PowerPC targets with f128 and the vsx feature (cc #125109). Although #[target_feature(enable = "vsx")] is still unstable, this is still an issue if user code is compiled with -Ctarget-feature=+vsx but compiler-builtins is not (or the other way around: system f128 builtins are compiled with vsx enabled as Clang and GCC don't support _Float128/__float128 when vsx is disabled).
Oh lol so LLVM inserts calls to builtins that use the wrong ABI? Yeah that sounds like an LLVM bug. Could you file an issue there? You probably are better at explaining the problem in their terms. :)
I have been thinking that is would probably be in our advantage to just submit MRs against the ABIs whenever there are ambiguities like this. LLVM seems to usually pass f16 and f128 in vector registers if available (and large enough for f128), otherwise pass it as the same-sized int - but it would be good to start codifying this where applicable, considering we support these types on far more targets than C (unstably of course). __float128/_Float128 really isn't available outside of popular 64-bit targets.
Silently falling back to a different register is a bad idea. It means we get subtly different ABI in ways people don't expect - we already have soundness issues due to that, let's not add more cases like this. ABI choices need to be explicit.
What would be the silent part? I was mostly suggesting that the ABIs should specify a concrete calling convention for primitives when their usual cc is feature-dependent (either via other registers or indirect). Which doesn't help us with the "target features change ABI" issue, but does help us with the "is our extern "C" correct" concern.
The silent part is where using -Ctarget-feature is not expected to change the ABI. Code with arbitrary combinations of target features for the same target should generally be linkable with each other; any change in ABI should be explicitly requested via different flags. Also see #116344, #116558.
For "Rust" we can just define our own ABI in a consistent way -- we already special case f32/f64 returning on x86-32, we should just extend the logic here to the remaining float types.
For non-"Rust" ABI however, we have two options:
partition targets into "with SSE regs in the ABI" and "without SSE regs in the ABI", and reject using target features to cross that boundary
Reject compiling f16-returning extern "C" functions when the SSE regs are missing
@beetrees btw you said this is about sse2, but shouldn't sse be enough as that's the feature controlling the registers?
Oh lol so LLVM inserts calls to builtins that use the wrong ABI? Yeah that sounds like an LLVM bug. Could you file an issue there? You probably are better at explaining the problem in their terms. :)
@beetrees btw you said this is about sse2, but shouldn't sse be enough as that's the feature controlling the registers?
AFAIK it should be, but LLVM currently switches to returning in eax as soon as sse2 is disabled. I've filed an LLVM issue about it at llvm/llvm-project#112890.
Reject compiling f16-returning extern "C" functions when the SSE regs are missing
I guess there is an alternative to this, which is to say that on x86 targets without SSE, f16 uses a different ABI for returns. Not sure if there is an official standard ABI for this, but LLVM implements some de-facto ABI. Then we have to reject ever enabling SSE on that target (including on a per-function level via #[target_feature]) to ensure the correct ABI is used everywhere.
One way to describe this is to say that there are 3 ABIs on x86-32: softfloat, full-hardfloat (with SSE2), and semi-hardfloat (with x87 but not using SSE registers for anything). Every target triple would have to decide which of these to use, and we'd use the check from #134794 to ensure the right target features are enabled.
However, LLVM doesn't actually have a way to force the semi-hardfloat ABI it seems, so wed have to forbid the SSE target feature on those targets...
We definitely need a larger discussion here before f16 gets stabilized. We either accept "some targets just can't pass f16 across a function boundary" or we come up with our own ABI and try to convince LLVM to support that.
Turns out this (unsurprisingly) affects more targets so I opened #138616 to track the over-arching issue. I am leaving this one open for the specific 88-32 issue.
FWIW the status quo is that our post-monomorphization ABI checks catch this and emit an error. So, we're not unsound. We're just in the same boat as types like __m128 -- it's not great usability-wise, but it's probably fine.
We should probably add tests for this though.
EDIT: Well an I guess there's the LLVM bug about using the wrong ABI for libcalls. That's completely broken, not sure what we can do about that.
The post-monomorphization ABI checks currently only catch Complex<f16> as it's PassMode::Cast to a vector. Regular f16 is passed directly and is currently not caught (compiler explorer).
I think there is a broader question here of what the C ABI should be for targets that don't have the target features required for the standard calling convention registers. With f32/f64, this currently occurs on soft-float targets (e.g. x86_64-unknown-none), and rustc currently uses a separate soft-float "C" ABI which passes floating-point numbers in integer registers (AFAIK x86 and x86_64 have no official soft-float ABI, just the de-facto "what GCC/Clang do") . Beyond fixing LLVM to be handle libcall ABIs in a less broken way, I think the question is (for non-Rustic ABIs) if we want to treat f16 (and f128) like we do f32/f64 or like we do vector types.
The post-monomorphization ABI checks currently only catch Complex as it's PassMode::Cast to a vector. Regular f16 is passed directly and is currently not caught (compiler explorer).
Beyond fixing LLVM to be handle libcall ABIs in a less broken way, I think the question is (for non-Rustic ABIs) if we want to treat f16 (and f128) like we do f32/f64 or like we do vector types.
I think the vector type treatment is not suited as a template here. For vector types we get away with checking the signatures of function calls, but for f16 we have the problem that built-in operations like * may need special registers. We would have to extend the post-monomorphization ABI check in a way that it also knows which built-ins may lower to libcalls that need particular target features. That's impractical IMO.
Looks like clang just rejects the equivalent code: https://godbolt.org/z/Eever1Gdc.
So IMO we should do the same. We can allow f16 for the "Rust" ABI where we can do whatever we want, but we shouldn't make up non-standard extensions to the "C" ABI. We have done this in the past and it leads to the mess that is the improper_ctypes lint to warn people about extern "C" with a made-up ABI; maybe it's worth trying a different approach were we just refuse to make up an ABI.
I'm definitely in favour of not inventing non-standard C ABIs. That does lead to the question of what this should do:
#[target_feature(enable = "sse2")]extern"C"fnfunc(x:f16) -> f16{
x
}
One option is to allow it, and only allow it to be called from functions with the target feature enabled, same as is done with vector types at the moment. However, that does mean that if Clang/GCC were to add support for soft float f16 ABI in future, we couldn't support the soft float ABI without it being a breaking change (not sure how likely this is to happen or not). Obviously this isn't a problem for vector types, as they are inherently related to their respective target features, but it's definitely something we should consider whether to allow or not for f16/f128/Complex.
(Interestingly enough, while Clang won't allow user defined to have _Float16/__fp16 arguments/return values, it's more than happy to let LLVM emit libcalls to functions that take/return an f16 (compiler explorer) - I believe LLVM compiler-rt assumes u16 and f16 have the same ABI on targets where Clang doesn't support _Float16. This of course encounters all the usual LLVM-libcalls-depend-on-target-features issues that we've been dealing with.)
That does lead to the question of what this should do:
On which target?
I think on i586 targets, we should allow it to compile, like we do with vector types. On soft-float targets we'll anyway forbid enabling SSE2 (#160302) so the question does not really come up; if clang ever has a soft-float ABI for f16 I expected we'd use that for our soft-float targets.
(Interestingly enough, while Clang won't allow user defined to have _Float16/__fp16 arguments/return values, it's more than happy to let LLVM emit libcalls to functions that take/return an f16 (compiler explorer) - I believe LLVM compiler-rt assumes u16 and f16 have the same ABI on targets where Clang doesn't support _Float16. This of course encounters all the usual LLVM-libcalls-depend-on-target-features issues that we've been dealing with.)
I wonder if that's a bug; the same code with _Float16 gets rejected.
I have no idea yet what our approach to these libcalls should be. Assuming that we want f16 to be always available, including normal operations like * and passing it via the "Rust" ABI, then we either have to get LLVM to to target-feature-dependent libcall lowering ("if the current function has the SSE2 target feature, call function A, else call function B"), or we'll have to have a similar check ourselves in the LLVM backend and generate libcalls instead of fadd if target features that we know LLVM will use are missing.
For that example I was thinking of i586, although there's a similar question on PowerPC with f128 and vsx.
I wonder if that's a bug; the same code with _Float16 gets rejected.
I believe the intention is to support __fp16 on all targets (Clang explicitly errors if you try and use it as an argument or return type), and _Float16 only on targets with a defined ABI. All math is performed as f32, but of course the conversion from f16 to f32 requires a libcall on targets without native support.
I have no idea yet what our approach to these libcalls should be.
@tgross35 made a proposal to get LLVM to emit target-feature-dependent libcalls in llvm/llvm-project#112885 (comment). I think fixing LLVM is the right long-term solution (especially as otherwise LLVM can't do any optimisations), but we could always emit libcalls ourselves when necessary in the short term until LLVM is fixed.
Activity
[-]x86-32 "f16" ABI needs SSE[/-][+]x86-32 "f16" ABI needs SSE, incompatible with i586 targets[/+]beetrees commentedon Oct 17, 2024
I think this will require LLVM changes to properly fix, as
f16is used as the return value of builtins; this is especially apparent when when combined with#[target_feature(enable = "sse2")], like in this example. LLVM will use whatever target features a function has to decide which ABI to use for a builtin, meaning that theno_ssefunction presumes that the result of__truncdfhf2has been returned inaxwhereas theyes_sse2function presumes that the result has been returned inxmm0.A similar issue can occur on PowerPC targets with
f128and thevsxfeature (cc #125109). Although#[target_feature(enable = "vsx")]is still unstable, this is still an issue if user code is compiled with-Ctarget-feature=+vsxbutcompiler-builtinsis not (or the other way around: systemf128builtins are compiled withvsxenabled as Clang and GCC don't support_Float128/__float128whenvsxis disabled).RalfJung commentedon Oct 17, 2024
Oh lol so LLVM inserts calls to builtins that use the wrong ABI? Yeah that sounds like an LLVM bug. Could you file an issue there? You probably are better at explaining the problem in their terms. :)
tgross35 commentedon Oct 17, 2024
I have been thinking that is would probably be in our advantage to just submit MRs against the ABIs whenever there are ambiguities like this. LLVM seems to usually pass
f16andf128in vector registers if available (and large enough forf128), otherwise pass it as the same-sized int - but it would be good to start codifying this where applicable, considering we support these types on far more targets than C (unstably of course).__float128/_Float128really isn't available outside of popular 64-bit targets.RalfJung commentedon Oct 17, 2024
tgross35 commentedon Oct 17, 2024
What would be the silent part? I was mostly suggesting that the ABIs should specify a concrete calling convention for primitives when their usual cc is feature-dependent (either via other registers or indirect). Which doesn't help us with the "target features change ABI" issue, but does help us with the "is our
extern "C"correct" concern.RalfJung commentedon Oct 18, 2024
The silent part is where using
-Ctarget-featureis not expected to change the ABI. Code with arbitrary combinations of target features for the same target should generally be linkable with each other; any change in ABI should be explicitly requested via different flags. Also see #116344, #116558.For "Rust" we can just define our own ABI in a consistent way -- we already special case f32/f64 returning on x86-32, we should just extend the logic here to the remaining float types.
For non-"Rust" ABI however, we have two options:
f16-returningextern "C"functions when the SSE regs are missing@beetrees btw you said this is about
sse2, but shouldn'tssebe enough as that's the feature controlling the registers?beetrees commentedon Oct 18, 2024
I've filed an LLVM issue at llvm/llvm-project#112885.
AFAIK it should be, but LLVM currently switches to returning in
eaxas soon assse2is disabled. I've filed an LLVM issue about it at llvm/llvm-project#112890.RalfJung commentedon Oct 18, 2024
I guess there is an alternative to this, which is to say that on x86 targets without SSE, f16 uses a different ABI for returns. Not sure if there is an official standard ABI for this, but LLVM implements some de-facto ABI. Then we have to reject ever enabling SSE on that target (including on a per-function level via
#[target_feature]) to ensure the correct ABI is used everywhere.Rollup merge of rust-lang#131871 - RalfJung:x86-32-float, r=workingju…
12 remaining items
Auto merge of #131871 - RalfJung:x86-32-float, r=workingjubilee
RalfJung commentedon Dec 26, 2024
One way to describe this is to say that there are 3 ABIs on x86-32: softfloat, full-hardfloat (with SSE2), and semi-hardfloat (with x87 but not using SSE registers for anything). Every target triple would have to decide which of these to use, and we'd use the check from #134794 to ensure the right target features are enabled.
However, LLVM doesn't actually have a way to force the semi-hardfloat ABI it seems, so wed have to forbid the SSE target feature on those targets...
We definitely need a larger discussion here before f16 gets stabilized. We either accept "some targets just can't pass f16 across a function boundary" or we come up with our own ABI and try to convince LLVM to support that.
RalfJung commentedon Mar 17, 2025
Turns out this (unsurprisingly) affects more targets so I opened #138616 to track the over-arching issue. I am leaving this one open for the specific 88-32 issue.
Auto merge of #131871 - RalfJung:x86-32-float, r=workingjubilee
RalfJung commentedon Aug 25, 2026
FWIW the status quo is that our post-monomorphization ABI checks catch this and emit an error. So, we're not unsound. We're just in the same boat as types like
__m128-- it's not great usability-wise, but it's probably fine.We should probably add tests for this though.
EDIT: Well an I guess there's the LLVM bug about using the wrong ABI for libcalls. That's completely broken, not sure what we can do about that.
beetrees commentedon Aug 25, 2026
The post-monomorphization ABI checks currently only catch
Complex<f16>as it'sPassMode::Castto a vector. Regularf16is passed directly and is currently not caught (compiler explorer).I think there is a broader question here of what the C ABI should be for targets that don't have the target features required for the standard calling convention registers. With
f32/f64, this currently occurs on soft-float targets (e.g.x86_64-unknown-none), andrustccurrently uses a separate soft-float "C" ABI which passes floating-point numbers in integer registers (AFAIKx86andx86_64have no official soft-float ABI, just the de-facto "what GCC/Clang do") . Beyond fixing LLVM to be handle libcall ABIs in a less broken way, I think the question is (for non-Rustic ABIs) if we want to treatf16(andf128) like we dof32/f64or like we do vector types.RalfJung commentedon Aug 25, 2026
🤦 of course.
RalfJung commentedon Aug 26, 2026
I think the vector type treatment is not suited as a template here. For vector types we get away with checking the signatures of function calls, but for
f16we have the problem that built-in operations like*may need special registers. We would have to extend the post-monomorphization ABI check in a way that it also knows which built-ins may lower to libcalls that need particular target features. That's impractical IMO.RalfJung commentedon Aug 26, 2026
Looks like clang just rejects the equivalent code: https://godbolt.org/z/Eever1Gdc.
So IMO we should do the same. We can allow
f16for the "Rust" ABI where we can do whatever we want, but we shouldn't make up non-standard extensions to the "C" ABI. We have done this in the past and it leads to the mess that is the improper_ctypes lint to warn people aboutextern "C"with a made-up ABI; maybe it's worth trying a different approach were we just refuse to make up an ABI.beetrees commentedon Aug 26, 2026
I'm definitely in favour of not inventing non-standard C ABIs. That does lead to the question of what this should do:
One option is to allow it, and only allow it to be called from functions with the target feature enabled, same as is done with vector types at the moment. However, that does mean that if Clang/GCC were to add support for soft float
f16ABI in future, we couldn't support the soft float ABI without it being a breaking change (not sure how likely this is to happen or not). Obviously this isn't a problem for vector types, as they are inherently related to their respective target features, but it's definitely something we should consider whether to allow or not forf16/f128/Complex.beetrees commentedon Aug 26, 2026
(Interestingly enough, while Clang won't allow user defined to have
_Float16/__fp16arguments/return values, it's more than happy to let LLVM emit libcalls to functions that take/return anf16(compiler explorer) - I believe LLVMcompiler-rtassumesu16andf16have the same ABI on targets where Clang doesn't support_Float16. This of course encounters all the usual LLVM-libcalls-depend-on-target-features issues that we've been dealing with.)RalfJung commentedon Aug 26, 2026
On which target?
I think on i586 targets, we should allow it to compile, like we do with vector types. On soft-float targets we'll anyway forbid enabling SSE2 (#160302) so the question does not really come up; if clang ever has a soft-float ABI for f16 I expected we'd use that for our soft-float targets.
I wonder if that's a bug; the same code with
_Float16gets rejected.I have no idea yet what our approach to these libcalls should be. Assuming that we want
f16to be always available, including normal operations like*and passing it via the "Rust" ABI, then we either have to get LLVM to to target-feature-dependent libcall lowering ("if the current function has the SSE2 target feature, call function A, else call function B"), or we'll have to have a similar check ourselves in the LLVM backend and generate libcalls instead offaddif target features that we know LLVM will use are missing.beetrees commentedon Aug 26, 2026
For that example I was thinking of i586, although there's a similar question on PowerPC with
f128andvsx.I believe the intention is to support
__fp16on all targets (Clang explicitly errors if you try and use it as an argument or return type), and_Float16only on targets with a defined ABI. All math is performed asf32, but of course the conversion fromf16tof32requires a libcall on targets without native support.@tgross35 made a proposal to get LLVM to emit target-feature-dependent libcalls in llvm/llvm-project#112885 (comment). I think fixing LLVM is the right long-term solution (especially as otherwise LLVM can't do any optimisations), but we could always emit libcalls ourselves when necessary in the short term until LLVM is fixed.