rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.52k stars 12.73k forks source link

What does the % operator on floats do? #57738

Closed AljoschaMeyer closed 5 years ago

AljoschaMeyer commented 5 years ago

I can't find any documentation on the behavior of the Rem implementation for f64 (or f32). Where can I find out how it precisely works, regarding rounding modes etc? Is it the IEEE 754-2008 "remainder" operation?

ExpHP commented 5 years ago

Is it the IEEE 754-2008 "remainder" operation?

Definitely not.

% on floats is weird because it's a modulus operation that has no corresponding integer division operation in the standard library. Its integral division function (which for the sake of discourse I will call a /& b) is the following:

a /& b = f64::trunc(a / b)

and when I refer to this as its corresponding division function, I mean that /& and % approximately obey the following law (which is common for pairs of integer division and modulus operations):

a = (a /& b) * b + (a % b)

I believe it is the same as the C function fmod.


Other notable properties:


I really do feel that this stuff should be documented somewhere.

ExpHP commented 5 years ago

sorry, floor is wrong, I edited my post. The division is truncated towards zero like it is for the % operation on primitive integers.

AljoschaMeyer commented 5 years ago

Thank you @ExpHP. For a bit more context: Where could I have found that information myself? And how did it come about that rust diverges from IEEE 754 here - is this llvm semantics cropping up?

frewsxcv commented 5 years ago

Not sure if this is currently documented anywhere, but at the very least, it should be mentioned here

ExpHP commented 5 years ago

@AljoschaMeyer to be honest, this is more or less how I naturally suspected it would work, based on experience with a similar % operator in Python (although that language actually has a corresponding integer division operation on floats, spelled //). Beyond that, I simply verified my suspicions by running examples on the playground.

Your post is the very first time I've ever heard that IEEE-754 specifies a modulus operation. Its definition (as the partner to round(a / b)) is not something I've ever noticed appearing in the standard library of any language (let alone being implemented as a builtin binary operator!). And speaking personally, although I've used this operation in the past, I have a strong distaste for it as people in my field of research (crystal physics) often use it incorrectly.

I'm not sure what provides the implementation, or if it is even correctly rounded. However, one can see from the trivial trait impl that it is a compiler-builtin operation that likely lowers to some intrinsic.

cuviper commented 5 years ago

Floating point % becomes frem in LLVM IR. https://github.com/rust-lang/rust/blob/01f8e25b15f4ab157c8e7c9c56054df7595ec0e1/src/librustc_codegen_ssa/mir/rvalue.rs#L584-L585 https://github.com/rust-lang/rust/blob/01f8e25b15f4ab157c8e7c9c56054df7595ec0e1/src/librustc_codegen_llvm/builder.rs#L378-L383

aaronfranke commented 5 years ago

Would it be possible to implement a canonical modulus function, perhaps %%? Such that:

5 %% 3 returns 2
-5 %% 3 returns 1
5 %% -3 returns -1
-5 %% -3 returns -2

The rule is that the output wraps around the second argument on a range [0, b). If the first argument is already on said range it will be output with no change, such that -2 %% -3 returns -2.

ExpHP commented 5 years ago

There is an effort to add euclidean modulus and division to all numerical types. These define the modulus to lie in [0, abs(b)), and are what I deem the "canonical" modulus function, rather than yours which is the floored modulus.

(worth noting: Both yours and my definition are typically capable of producing a value that is exactly equal to abs(b) for floating point numbers in most implementations, as the result of -1e-20 mod b)

DevQps commented 5 years ago

@AljoschaMeyer Would you still like to fix this? Otherwise I could take an attempt at improving the documentation (Y).

AljoschaMeyer commented 5 years ago

@DevQps Please go ahead =)

DevQps commented 5 years ago

@steveklabnik I'd like to fix this, and since there were no other assignee's I didn't know who else to ping :)

I searched libstd and libcore for an implementation of Rem<f32> but nothing came up. I guess the reason for that is that it's builtin in the compiler? I was wondering is there any place where I can add documentation to this trait implementation? Or are we stuck here?

cuviper commented 5 years ago

@DevQps -- the actual operation is in the compiler, but the trait is still here: https://github.com/rust-lang/rust/blob/33ef0bad21d6bb646c7c3ab0dbf381ca96c324bf/src/libcore/ops/arith.rs#L538-L552

steveklabnik commented 5 years ago

Beat me to it, @cuviper. Let me know if you need any help with that @devqps; with the macro it is a bit weird

DevQps commented 5 years ago

@cuviper @steveklabnik Thanks for the info! I took a look at it (and looked some information up about macro's as well). I see these solutions:

  1. Write generic documentation inside rem_impl_float that covers both impl Rem<f32> and impl Rem<f64>. With this I mean that we use the term "float" instead of the more concrete "f32" and "f64". For as far as I know, the % operations do not differ between these types.

  2. Unroll (remove and replace with concrete impl) rem_impl_float such that we can write more concrete documentation for impl Rem<f32> and impl Rem<f64>.

Additional question: forward_ref_binop! generates: Rem<&f32> for f32, Rem<f32> for &f32 and Rem<&f32> for &f32 but is used by many other functions as well. Should we leave those without documentation? Or should we also unroll those? I could also adjust forward_ref_binop! to take an additional doc string but I guess we would have to go and add doc strings all over the place (since it's used for other operations as well) so I think that is not a good idea.

I wonder what you think is the best, and then I will go and create a Pull Request! (Unless you also have open points of course)

steveklabnik commented 5 years ago

I'd go with option 1.