Why does this floating-point calculation give different results on different machines?
I have a simple routine which calculates the aspect ratio from a floating point value. So for the value 1.77777779, the routine returns the string "16:9". I have tested this on my machine and it works fine.
The routine is given as :
public string AspectRatioAsString(float f)
{
bool carryon = true;
int index = 0;
double roundedUpValue = 0;
while (carryon)
{
index++;
float upper = index * f;
roundedUpValue = Math.Ceiling(upper);
if (roundedUpValue - upper <= (double)0.1 || index > 20)
{
carryon = false;
}
}
return roundedUpValue + ":" + index;
}
Now on another machine, I get completely different results. So on my machine, 1.77777779 g开发者_C百科ives "16:9" but on another machine I get "38:21".
Here's an interesting bit of the C# specifiction, from section 4.1.6:
Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects.
It is possible that this is one of the "measurable effects" thanks to that call to Ceiling. Taking the ceiling of a floating point number, as others have noted, magnifies a difference of 0.000000002 by nine orders of magnitude because it turns 15.99999999 into 16 and 16.00000001 into 17. Two numbers that differ slightly before the operation differ massively afterwards; the tiny difference might be accounted for by the fact that different machines can have more or less "extra precision" in their floating point operations.
Some related issues:
C# XNA Visual Studio: Difference between "release" and "debug" modes?
CLR JIT optimizations violates causality?
To address your specific problem of how to compute an aspect ratio from a float: I'd possibly solve this a completely different way. I'd make a table like this:
struct Ratio
{
public int X { get; private set; }
public int Y { get; private set; }
public Ratio (int x, int y) : this()
{
this.X = x;
this.Y = y;
}
public double AsDouble() { return (double)X / (double)Y; }
}
Ratio[] commonRatios = {
new Ratio(16, 9),
new Ratio(4, 3),
// ... and so on, maybe the few hundred most common ratios here.
// since you are pinning results to be less than 20, there cannot possibly
// be more than a few hundred.
};
and now your implementation is
public string AspectRatioAsString(double ratio)
{
var results = from commonRatio in commonRatios
select new {
Ratio = commonRatio,
Diff = Math.Abs(ratio - commonRatio.AsDouble())};
var smallestResult = results.Min(x=>x.Diff);
return String.Format("{0}:{1}", smallestResult.Ratio.X, smallestResult.Ratio.Y);
}
Notice how the code now reads very much like the operation you are trying to perform: from this list of common ratios, choose the one where the difference between the given ratio and the common ratio is minimized.
I wouldn't use floating point numbers unless I really had to. They're too prone to this sort of thing due to rounding errors.
Can you change the code to work in double precision? (decimal would be overkill). If you do this, does it give more consistent results?
As to why it's different on different machines, what are the differences between the two machines?
- 32 bit vs 64 bit?
- Windows 7 vs Vista vs XP?
- Intel vs AMD processor? (thanks Oded)
Something like this might be the cause.
Try Math.Round
instead of Math.Ceiling
. If you end up with 16.0000001 and round up you'll incorrectly discard that answer.
Miscellaneous other suggestions:
- Doubles are better than floats.
(double) 0.1
cast is unnecessary.- Might want to throw an exception if you can't figure out what the aspect ratio is.
- If you return immediately upon finding the answer you can ditch the
carryon
variable. - A perhaps more accurate check would be to calculate the aspect ratio for each guess and compare it to the input.
Revised (untested):
public string AspectRatioAsString(double ratio)
{
for (int height = 1; height <= 20; ++height)
{
int width = (int) Math.Round(height * ratio);
double guess = (double) width / height;
if (Math.Abs(guess - ratio) <= 0.01)
{
return width + ":" + height;
}
}
throw ArgumentException("Invalid aspect ratio", "ratio");
}
When index is 9, you would expect to get something like upper = 16.0000001 or upper = 15.9999999. Which one you get will depend on rounding error, which may differ on different machines. When it's 15.999999, roundedUpValue - upper <= 0.1
is true, and the loop ends. When it's 16.0000001, roundedUpValue - upper <= 0.1
is false and the loop keeps going until you get to index > 20
.
Instead maybe you should try rounding upper to the nearest integer and checking if the absolute value of its difference from that integer is small. In otherwords, use something like if (Math.Abs(Math.Round(upper) - upper) <= (double)0.0001 || index > 20)
We had printf()-statements with floating point values that gave different roundings on computer 1 versus computer 2, even though both computers contained the same Visual Studio 2019 version and build. The difference was found however in a slightly older Windows 10 SDK versus the newest version. How strange it may seem... After fixing that the differences were gone.
精彩评论