Pre-RFC: Float-free libcore


#1

phil-opp made the PR on GitHub, but this is blocking me so i wrote the RFC. phil-opp is welcome to claim this from me if they wish.

Summary

Add target has_floating_point property and disable floating-point instruction emission when compiling libcore if false.

Motivation

  • Some processors, e.g. some ARM processors, lack hardware for floating-point operations.
  • Many kernels, e.g. Linux, forbid floating-point. Saving the floating-point registers is costly, and if the kernel uses floating-point instructions it must do so at every interrupt and system call. As kernel code usually never needs floating-point, it makes sense to forbid it altogether to not pay the cost of saving the registers.

The modifications proposed in this RFC would enable writing code for such processors or for such kernels in Rust.

Even if floating-point features of libcore are not used, when it is built for a target with floating-point enabled, some non-floating-point operations may be emitted as MMX or SSE instructions on architectures which have them, so emitting such instructions must be disabled, but on some such architectures, e.g. amd64, the ABI specifies floating-point arguments passed in SSE registers, so floating-point must be disabled altogether.

Detailed design

Add an optional has_floating_point property, default true, gated as cfg_target_has_floating_point. Disable all floating-point use in libcore if this flag is false.

Drawbacks

This increases the complexity of the libcore code slightly.

Alternatives

  • Delete all floating-point code from libcore
  • Do nil, and let users who need this patch their own libcore
  • Switch to soft-float rather than disable if the flag is false

Unresolved questions

None so far


#2

This seems a bit hand-wavy about what it means to “disable floating point”. Is this only about definitions in libcore? But then one can still create float values and copy them around, because that’s part of the language. Since this would access FP registers on some ABIs (e.g., x86-64). This suggests one might want more far-reaching consequences such as disabling the primitive type altogether.


#3

an alternative would be to provide soft floats by mapping the representation of f32 and f64 to u32 and u64. When unused, the soft float impls can probably be stripped by LTO?


#4

I agree with rkruppe, i think this should disable the f32 and f64 types in the language, since any use of them is invalid in an environment without floating-point support.

Full software floating-point is a possibility, however I think it is too much of an undertaking for little benefit.


#5

Note that LLVM will happily use the floating-point registers, even when compiling code that doesn’t contain floating-point types. This happens when generating optimized memcpy and memset expansions, or when auto-vectorizing integer arithmetic for example.

It is possible to request that LLVM doesn’t do this with a target configuration flag.


#6

Noted in RFC.

Yes

Maybe, but i think this out of scope of this RFC: we can build a float-free libcore whether or not the primitive types are enabled. Many kernels which forbid floating-point are written in C which also has primitive floating-point types, and merely forbid using them.

Ah yes, this is actually why building libcore with floating-point enabled causes grief in these cases :slight_smile:

Noted in RFC.


#7

But that doesn’t come from libcore. It comes from LLVM. Removing all mentions of f32 and f64 from the source code won’t do anything about that. The flag would also have to affect code generation, i.e., it would have to be passed to LLVM somehow. This clashes with your earlier statement that the RFC is only about definitions in libcore.


#8

For that you use an ABI that does not contain the FP registers. The problem is that the relevant ABI does not support returning floating point values at all, so you must not use values with these types.


#9

Yes. I added some explanation in the RFC.


#10

For a kernel you actually want to be able to use floating point (and vector), but only in well defined places. That is what Linux does, at least on some architectures. Removing float/vector from the language wouldn’t allow that IIUIC.


#11

It could be clearer.

  • This seems to mix up code generation and the definition of libcore. They are seperate mechanisms. The target spec affects code generation, and cfg values affect libcore.
  • Is has_floating_point part of the target spec? Is it a cfg value?
  • If has_floating_point is a cfg value what is its impact on core?
  • Do you want to eliminate the definitions related to floating point math in core? What are they specifically and how will they be compiled out?
  • Some float-related parts of libcore will probably never generate code unless invoked. Should those be removed?
  • Is this RFC about compiler code generation or libcore or both?

#12

After reading @phil_opp’s patch I see that it’s entirely about removing parts of libcore. I didn’t get that from the RFC.

I think it should probably say exactly what’s going to be removed and why.

It should address how this will affect the ecosystem. Does every crate that wants to work in a float-free environment need to take special precautions? Every no_std-capable crate would potentially and unexpectedly fail to compile in a not(has_floating_point) environment. That’s a big impact. Why is it ok and how to we mitigate it?

Will this foreseeably need to be done for std?

Are there other global target properties where this pattern might apply? Conditionally removing parts of core is not great! Doing it a lot could be pretty disastrous.

The type of division of responsibilities this RFC is proposing is usually handled through crate decomposition in the std facade. Can core be split into more crates (e.g. a core facade) to handle this problem?


#13

I think this proposal is missing the forest for the trees, so to speak. The problem is that we don’t have a software float implementation, and we’re just assuming that hardware floats exist everywhere (they don’t). Removing float usage from libcore doesn’t actually address this, and in fact makes it worse because it’s a massively breaking change as @brson points out. How are we supposed to use a language feature if we can’t reliably assume it exists?

tl;dr we need software floats.


#14

While software floats address the use case of “I want to run on a platform that doesn’t have an FPU”, there is another use case: “I have FP registers but I don’t want to use them, neither explicitly nor implicitly”. Soft floats don’t address that, at most incidentally if it also disables the emission of FP instructions in LLVM.

Also, any time some big runtime component is added, people will want to opt out of it. If it’s just a bunch of functions from libcore (or a library libcore links to), LTO/gc-sections might take care of it, but this needs to be kept in mind.


#15

That’s a good point. Perhaps we could also add float-related attributes for this, something like:

  • #![cfg(target_float_type=x)], where x is either "hardware", "software" or "none";
  • #![no_hardware_float], which opts out of hardware floats (i.e. forces the use of software floats);
  • #![no_float], which disables float types, features that rely on float types, and also (if necessary) suppresses the emission of FP-related instructions.

#16

The problem with soft-float is that not platforms have a soft-float ABI, in particular x86_64 and AArch64. Implementing soft-float support for these platforms would require a lot of work and is mostly outside the scope of the Rust project.

I like the idea of a #![no_float] marker though, which would work like #![no_std]. This attribute would have to imply #![no_std] since libstd requires float support. Like no_std, this will create a set of crates which are specified to not require floating-point support, and thus are usable in programs that don’t use FP registers.


#17

I since made a pull request.


#18

As far as kernels are concerned, I know that at least on Linux you can only use floating point (and I believe SIMD as well) by explicitly saving and restoring the CPU registers. The region in between is not allowed to block. So this is mostly useful for things like crypto code and checksums (where it is a big performance win, enough to justify saving and restoring all of the registers, and where periodic, explicit polls can be made). However, it also means that LLVM has already solved this problem if it is capable of compiling Linux kernel modules (which I believe it is, at least to some degree).