14

Could anyone tell me why these two modulus calculations yield two different outcomes? I just need to blame someone or something but me for all those hours I lost finding this bug.

public void test1()
{
    int stepAmount = 100;
    float t = 0.02f;
    float remainder = t % (1f / stepAmount);
    Debug.Log("Remainder: " + remainder);
    // Remainder: 0.01

    float fractions = 1f / stepAmount;
    remainder = t % fractions;
    Debug.Log("Remainder: " + remainder);
    // Remainder: 0
}

Using VS-2017 V15.3.5

Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
Madmenyo
  • 8,389
  • 7
  • 52
  • 99
  • 1
    For what its worth, I can't reproduce this on VS for Mac v7.2 (636). Both remainders are `0.0` – InBetween Nov 08 '17 at 21:26
  • Dotnetfiddle shows 0 for both results: https://dotnetfiddle.net/XTHswt I tried the exact same code as what is in the dotnetfiddle link with https://www.jdoodle.com/compile-c-sharp-online and it shows the issue you are describing. – TyCobb Nov 08 '17 at 21:27
  • Reproducible here: https://ideone.com/JHomEl – Ry- Nov 08 '17 at 21:27
  • Reproduced in VS2017 version 15.4.2. – BJ Myers Nov 08 '17 at 21:28
  • what version of visual studio do you use? – Donnoh Nov 08 '17 at 21:30
  • Reproduced with JetBrains Rider 2017.2 – TyCobb Nov 08 '17 at 21:30
  • @Donnoh Just added that, but it is not restricted to just this version apparently. – Madmenyo Nov 08 '17 at 21:31
  • 2
    Oddly behaviour is different if `stepAmount` is const. – mjwills Nov 08 '17 at 21:34
  • On my machine if I change third line to: float remainder = t % (1f / 100); I'm getting two zeros and same results as you otherwise, but I'd also like to know why. – mickl Nov 08 '17 at 21:37
  • 2
    Why are you using floats if precision is necessary? Using your version of VS and using decimal will not provide inconsistent results. – Sam Marion Nov 08 '17 at 21:58
  • @SamMarion float precision is enough for me. But I just encountered this in my code and find it very strange behavior. There where several ways I could solve it but actually pin pointing the bug I had to these lines took hours, since "it really should return 0". – Madmenyo Nov 08 '17 at 22:02
  • Possible duplicate of [Why does this floating-point calculation give different results on different machines?](https://stackoverflow.com/questions/2342396/why-does-this-floating-point-calculation-give-different-results-on-different-mac) – Liam Dec 04 '17 at 15:35

1 Answers1

8

My best bet is that this is due to the liberty the runtime has to perform floating point operations with higher precision than the types involved and then truncating the result to the type's precision when assigning:

The CLI specification in section 12.1.3 dictates an exact precision for floating point numbers, float and double, when used in storage locations. However it allows for the precision to be exceeded when floating point numbers are used in other locations like the execution stack, arguments return values, etc … What precision is used is left to the runtime and underlying hardware. This extra precision can lead to subtle differences in floating point evaluations between different machines or runtimes.

Source here.

In you first example t % (1f / stepAmount) can be performed entirely with a higher precision than float and then truncated when the result is assigned to remainder, while in the second example, 1f / stepAmount is truncated and assigned to fractions prior to the modulus operation.

As to why making stepamount a const makes both modulus operations consistent, the reason is that 1f / stepamount immediately becomes a constant expression that is evaluated and truncated to float precision at compile time and is no different from writing 0.01f which essentially makes both examples equivalent.

InBetween
  • 32,319
  • 3
  • 50
  • 90
  • I just don't know enough to make a good comment on this but doesn't make the "f" in the `(1f / stepAmount)` operation an entire `float` operation? Also (even more strangely?), making `stepamount` a `const` fixes the issue. – Madmenyo Nov 08 '17 at 21:41
  • I think this is correct. When changing the types of t, remainder, and fractions to double in LinqPad I get `Remainder: 0.00999999955296516` for both operations. – Jonathon Chase Nov 09 '17 at 01:48
  • Note that theres just a single `div` instruction in msil. There's no `div` specific for floats or doubles. – Caramiriel Nov 09 '17 at 13:47