This is an academic question and so answers such as "just don't do that" miss the point.
I'm not trying to solve a problem - I'm trying to understand an observed behavior, namely a difference in how floating point math appears to function when comparing C and C#
Assumption: float precision in C
It is my assumption that in C floats are implemented using a 23 bit mantissa and 8 bit exponent (https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
For a given number, we can compute the smallest precision - the smallest value you can add to the number where purely structurally it cannot be stored anymore - by computing the value of the last bit of the mantissa.
If the floating point number is evaluated as:
[sign] * 1.[mantissa] * 2^[exponent]
Then because we have 23 bits in the mantissa the value of precision is 2^(exponent-23), where the exponent for a given number is:
floor(log2(number))
So the precision of a fairly large number like 10^9 is computed as follows:
exponent = floor(log2(10^9))
= 29
precision = 2^(exponent-23)
= 2^(29-23)
= 2^6
= 64
This is the bare-metal, lowest absolutely theoretically possible value that can be added to 10^9 when stored as a float, because we're literally flipping the least significant bit of the mantissa:
As visualized by the IEEE-754 Floating Point Converter
I can also validate this with a quick C program (run online):
#include <cstdio>
int main()
{
float number = 1e9f; // exponent: 29, precision: 64
printf("%'.0f\n", number); // prints: 1000000000
number += 30; // 30 rounded to nearest multiple of 64 is 0
printf("%'.0f\n", number); // prints: 1000000000
number += 40; // 40 rounded to nearest multiple of 64 is 64
printf("%0'.0f\n", number); // prints: 1000000064
return 0;
}
It is my assumption that the general 32 bit floating point format (1 bit sign, 8 bit exponent, 23 bit mantissa) is so universal that it's something intrinsic to modern CPUs, and so generally behavior would be the same across programming languages.
Question: float precision in C#
So with that stated, when I try the same validation test in C# the value of the number does not change.
If I use a smaller value 10^8, which would have an exponent of 26 and therefore a precision of 2^(26-23) = 8 given my above assumptions of how the bits of the floating point format represent the number internally, I notice the following behavior:
float number = 1e8f; // exponent: 26, precision: 8
Console.WriteLine($"{number,1:0}"); // prints: 100000000
number += 30; // 30 rounded to multiple of 8 -should- be 32
Console.WriteLine($"{number,1:0}"); // prints: 100000000
number += 40; // 40 rounded to multiple of 8 -should- be 40
Console.WriteLine($"{number,1:0}"); // prints: 100000100
And that... confuses me somewhat. Where did that 100 come from? That's not even a multiple of 2!
with a value of 1e8f C also behaves as expected and supports the precision being a value of '8': cpp.sh/6qesv
Looking at the C# documentation for floating point values nothing jumps out at me that would suggest that C# should handle float addition any differently here than C, and what I would expect given how floating point values are implemented.
The docs do mention that the approximate precision of floats is ~6-9 digits which is frustratingly vague. I suppose that could be an answer: "you're dealing with digits past the guaranteed limit, it's undefined behavior" and while true, that is unsatisfying.
I would like to know, ideally broken down step by step, what actually happened in C#'s implementation there that makes it behave so differently than C here.