Skip to content

x86-32 "f16" ABI needs SSE, incompatible with i586 targets #131819

Open

Description

@RalfJung

View all comments

@beetrees wrote

On 32-bit x86, the ABI specifies that f16 gets returned in xmm0, which is what Clang and GCC do (neither Clang nor GCC support _Float16 when SSE2 is disabled)

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.

Cc @tgross35 @chorman0773

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 17, 2024
  2. changed the title [-]x86-32 "f16" ABI needs SSE[/-] [+]x86-32 "f16" ABI needs SSE, incompatible with i586 targets[/+] on Oct 17, 2024
  3. beetrees commented on Oct 17, 2024

    @beetrees
    Contributor

    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).

  4. RalfJung commented on Oct 17, 2024

    @RalfJung
    MemberAuthor

    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. :)

  5. added
    A-ABIArea: Concerning the application binary interface (ABI)
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    on Oct 17, 2024
  6. tgross35 commented on Oct 17, 2024

    @tgross35
    Member

    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.

  7. RalfJung commented on Oct 17, 2024

    @RalfJung
    MemberAuthor
  8. tgross35 commented on Oct 17, 2024

    @tgross35
    Member

    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.

  9. RalfJung commented on Oct 18, 2024

    @RalfJung
    MemberAuthor

    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?

  10. beetrees commented on Oct 18, 2024

    @beetrees
    Contributor

    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've filed an LLVM issue at llvm/llvm-project#112885.

    @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.

  11. RalfJung commented on Oct 18, 2024

    @RalfJung
    MemberAuthor

    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.

  12. added a commit that references this issue on Oct 19, 2024
  13. added
    S-waiting-on-LLVMStatus: the compiler-dragon is eepy, can someone get it some tea?
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 19, 2024
  14. 12 remaining items

  15. RalfJung commented on Dec 26, 2024

    @RalfJung
    MemberAuthor

    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.

  16. added
    O-PowerPCTarget: PowerPC processors
    and removed
    O-PowerPCTarget: PowerPC processors
    on Mar 17, 2025
  17. RalfJung commented on Mar 17, 2025

    @RalfJung
    MemberAuthor

    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.

  18. RalfJung commented on Aug 25, 2026

    @RalfJung
    MemberAuthor

    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.

  19. beetrees commented on Aug 25, 2026

    @beetrees
    Contributor

    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.

  20. RalfJung commented on Aug 25, 2026

    @RalfJung
    MemberAuthor

    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).

    🤦 of course.

  21. RalfJung commented on Aug 26, 2026

    @RalfJung
    MemberAuthor

    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.

  22. RalfJung commented on Aug 26, 2026

    @RalfJung
    MemberAuthor

    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.

  23. beetrees commented on Aug 26, 2026

    @beetrees
    Contributor

    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" fn func(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.

  24. beetrees commented on Aug 26, 2026

    @beetrees
    Contributor

    (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.)

  25. RalfJung commented on Aug 26, 2026

    @RalfJung
    MemberAuthor

    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.

  26. beetrees commented on Aug 26, 2026

    @beetrees
    Contributor

    On which target?

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ABIArea: Concerning the application binary interface (ABI)A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.F-f16_and_f128`#![feature(f16)]`, `#![feature(f128)]`O-x86_32Target: x86 processors, 32 bit (like i686-*) (also known as IA-32, i386, i586, i686)S-waiting-on-LLVMStatus: the compiler-dragon is eepy, can someone get it some tea?

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions