Skip to content

ABI for float builtins depends on target features #112885

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

Open
beetrees opened this issue Oct 18, 2024 · 13 comments
Open

ABI for float builtins depends on target features #112885

beetrees opened this issue Oct 18, 2024 · 13 comments
Labels
ABI Application Binary Interface clang:headers Headers provided by Clang, e.g. for intrinsics floating-point Floating-point math

Comments

@beetrees
Copy link
Contributor

When LLVM needs to call a builtin (such as __truncdfhf2 or __addtf3), it will use whatever ABI is the default for the target features of the function that is currently being compiled. This means that on targets where the ABI of float types is target-feature dependant (e.g. the ABI of half changes of 32-bit x86 depending on whether sse2 is enabled, and the ABI of fp128 changes on PowerPC depending on whether vsx is enabled), LLVM will call the same builtin function using two different incompatible ABIs.

The simplest solution to this seems to be extending the target-abi module flag to allow frontends to specify which float ABI gets used for function calls independent of the target features of a function. Other solutions include allowing the frontend to override the names of builtin functions on a per-function-being-compiled level.

Related Rust issues: rust-lang/rust#131819, rust-lang/rust#125109

@RalfJung
Copy link
Contributor

The simplest solution to this seems to be extending the target-abi module flag to allow frontends to specify which float ABI gets used for function calls independent of the target features of a function.

I would argue this is a per-call-site decision, see also #70563.

IIUC, the most problematic situation here is code that is generally built for i386 without any SSE support, but then has some fast-paths for when SSE2 is available. That code will presumably be linked with a compiler_rt built without SSE support, but float operations inside the functions that have SSE2 enabled will expect the callee to use the SSE register. If call could encode which ABI it should use, then libcall lowering could synthesize calls that use the right registers, ignoring the target features enabled for the current function.

@nikic
Copy link
Contributor

nikic commented Oct 18, 2024

@RalfJung For calls to builtins, there is usually no call at the IR level. The call is inserted as part of legalizing a (non-call) IR operation.

@RalfJung
Copy link
Contributor

RalfJung commented Oct 18, 2024

Isn't that happening on some different IR? I'm not familiar with the lower ends of LLVM -- I heard there's Machine IR there.

If this is never in any IR it should be easier -- the legalization "just" has to pass through the information which ABI to use, to make it clear that this should not be adjusted to whatever the target features of the current function are. (I'm sure it's not actually easy, the devil is always in the details.)

@EugeneZelenko EugeneZelenko added clang:headers Headers provided by Clang, e.g. for intrinsics ABI Application Binary Interface floating-point Floating-point math and removed new issue labels Oct 18, 2024
@arsenm
Copy link
Contributor

arsenm commented Oct 19, 2024

The legalization call will be inserted in SelectionDAG or gMIR

@tgross35
Copy link
Contributor

For reference both gcc and clang just reject _Float16 with -mno-sse2 , and neither seem to support __float128 or _Float128 on PowerPC.

@beetrees
Copy link
Contributor Author

Both GCC and Clang support _Float128/__float128 on PowerPC when vsx is enabled. Both require the -mfloat128 option to enable it: Clang will automatically enable the vsx feature when -mfloat128 is used whereas GCC will emit an error if -mfloat128 is used and vsx is not enabled.

@RalfJung
Copy link
Contributor

RalfJung commented Mar 17, 2025

@EugeneZelenko why was this tagged with "clang:..."? This is mostly an LLVM backend issue, causing some issues in Rust that will have to be resolved before we can fully ship f16 / f128.


For reference both gcc and clang just reject _Float16 with -mno-sse2 ,

So... does this mean that these builtins will actually be built with SSE support even on targets where SSE is off-by-default?

@tgross35
Copy link
Contributor

What would an ideal solution look like here? If target features are always sufficient to indicate the ABI, then would a way to change the libcalls like attributes #0 = { "target-features"="-sse", "libcall-legaliation"="FPROUND_F32_F16=__truncdfhf2_nosse" } be enough?

So... does this mean that these builtins will actually be built with SSE support even on targets where SSE is off-by-default?

Which libraries? Since Clang and GCC both reject _Float16 on -m32 r -mno-sse2, I'm assuming there aren't any C libraries that can be providing these builtins. In the case of Rust's i586 no-sse target, LLVM treats half as a u16 and uses that ABI for builtins.

LLVM actually seems to use different symbols for f16 conversions based on hardware support, using either __gnu_h2f_ieee/__gnu_f2h_ieee or __extendhfsf2/__truncsdhf2 based on (iirc) SoftPromoteFloat. I haven't seen anything to indicate this is intentional, however. https://rust.godbolt.org/z/1qq9b8Paq

@arsenm
Copy link
Contributor

arsenm commented Mar 18, 2025

What would an ideal solution look like here? If target features are always sufficient to indicate the ABI, then would a way to change the libcalls like attributes #0 = { "target-features"="-sse", "libcall-legaliation"="FPROUND_F32_F16=__truncdfhf2_nosse" } be enough?

This is leaking way too specific backend details in very a bad way. There should be a separate higher level ABI control to disable SSE registers, rather than having the target features directly set ABI

@tgross35
Copy link
Contributor

This is leaking way too specific backend details in very a bad way. There should be a separate higher level ABI control to disable SSE registers, rather than having the target features directly set ABI

What would this look like, since it needs to somehow be per-function controllable? As show in the top post's examples.

LLVM could also specify different builtin names, e.g. suffix with _softfloat or _hardfloat when target features mean a different ABI from the default would be used.

@RalfJung
Copy link
Contributor

Which libraries? Since Clang and GCC both reject _Float16 on -m32 r -mno-sse2, I'm assuming there aren't any C libraries that can be providing these builtins. In the case of Rust's i586 no-sse target, LLVM treats half as a u16 and uses that ABI for builtins.

Hm, I didn't say "library" so I am confused now...

What I mean is: there are certain standard symbols LLVM expects to exist that provide these operations, such as __truncdfhf2 or __addtf3. I assume there's some C implementation of them somewhere. And I guess that those are built with SSE as otherwise f16 couldn't even be used?

This is leaking way too specific backend details in very a bad way.

LLVM is already leaking all those details. Frontends have to know which target features affect the ABI and how to be able to guard against LLVM just making up a different unexpected ABI.

I'd love to see this reformed. :)

@beetrees
Copy link
Contributor Author

LLVM compiler-rt will use uint16_t for f16 if _Float16 is not supported by the C compiler (detected here, used here for extend and here for truncate).

@tgross35
Copy link
Contributor

What I mean is: there are certain standard symbols LLVM expects to exist that provide these operations, such as __truncdfhf2 or __addtf3. I assume there's some C implementation of them somewhere. And I guess that those are built with SSE as otherwise f16 couldn't even be used?

I don't believe SSE is ever somehow enabled for the purpose of supporting these symbols. As far as I can tell, when vector registers are not available, LLVM changes its default ABI for these types to pass as integers and expects builtin calls to do the same (which is reasonable). This is visible in Clang for __float128, it gets passed as an i128 (register pair) without SSE. Clang rejects _Float16 earlier for whatever reason, but LLVM still expects half builtins to use i16.

As beetrees mentioned, compiler-rt has a fallback that uses the integer ABI but I don't think it could ever be used from C (also GCC doesn't seem to have something similar), though it does have the __gnu_h2f_ieee functions on Arm). We can use it from Rust or other languages, however, and our implementation effectively does the same (even though we still use f16, thanks to the LLVM fallback).

GCC rejects __float128 and _Float16 on x86-64 without SSE for anything that would return the types or call a builtin, though it allows the types as parameters. These get passed indirectly.

Some experimentation https://gcc.godbolt.org/z/Yc5s5ocE5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ABI Application Binary Interface clang:headers Headers provided by Clang, e.g. for intrinsics floating-point Floating-point math
Projects
None yet
Development

No branches or pull requests

6 participants