3

In How does DoubleUtil.DoubleToInt(double val) work? we learn that the .NET Framework has a special way of rounding floating point values:

public static int DoubleToInt(double val)
{
    return (0 < val) ? (int)(val + 0.5) : (int)(val - 0.5);
}

Why are they not just using (int)Math.Round(val)?

Or: Why is Math.Round not defined this way if this is superior? There must be some trade-off.

Community
  • 1
  • 1
usr
  • 168,620
  • 35
  • 240
  • 369
  • Because `Math.Round` has overloads for handling how many decimal places to round to and different types, would be my guess. – juharr Sep 13 '16 at 19:42

3 Answers3

4

Math.Round would result in the creation of a double with the exact value needed, which would then need to be converted to an int. The code here avoids the creation of that double. It also allows for the elision of error handling, and the code related to other types of rounding modes or digits to round to.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • This would also create an intermediate double (being `val + 0.5` or `val - 0.5` – Simon Byrne Sep 13 '16 at 20:11
  • @SimonByrne The difference being that it's much easier to perform the addition of a constant value than to do the computation to figure out what the nearest whole number is. It's not the creating of the `double` value, it's the computation to figure out what that double value is. – Servy Sep 13 '16 at 21:11
  • Hm, should the hardware not support both truncation and rounding? I don't see why there would be a cost difference for the pure calculation. Maybe it's about cost induced by validation and detecting the digits to round to. – usr Sep 14 '16 at 18:21
  • @usr The hardware may or may not support it. The `Round` method attempts to use native support, if available, but does the more complex calculation if it's not available. – Servy Sep 14 '16 at 18:22
3

They have different behaviour at value with a fractional part 1/2. According to Math.Round:

If the fractional component of a is halfway between two integers, one of which is even and the other odd, then the even number is returned.

So if val == 0.5, then Math.Round(val) == 0.0, whereas this DoubleToInt would give (int)(0.5+0.5) == 1. In other words, DoubleToInt round 1/2 away from zero (like the standard C round function).

There is also potential here for less desirable behaviour: if val is actually the double before 0.5 (i.e. 0.49999999999999994) then, depending on how C# handles intermediate precision, it may in fact give 1 (as val + 0.5 isn't representable by a double, and could be rounded to 1). This was in fact an infamous specification bug in Java 6 (and earlier).

Community
  • 1
  • 1
Simon Byrne
  • 7,694
  • 1
  • 26
  • 50
1

I could see this being an optimization since to get the same behavior from Round you need to use the MidpointRounding.AwayFromZero option. From the reference source this is implemented via:

private static unsafe double InternalRound(double value, int digits, MidpointRounding mode) {
    if (Abs(value) < doubleRoundLimit) {
        Double power10 = roundPower10Double[digits];
        value *= power10;
        if (mode == MidpointRounding.AwayFromZero) {                
            double fraction = SplitFractionDouble(&value); 
            if (Abs(fraction) >= 0.5d) {
                value += Sign(fraction);
            }
        }
        else {
            // On X86 this can be inlined to just a few instructions
            value = Round(value);
        }
        value /= power10;
    }
    return value;
}

I can only guess that the author of the utility method did some performance comparison.

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122
  • I just benchmarked variations: Round(double) is 140 units (but uses bankers rounding which nobody expects), Round(double, ToEven) is 310 units, DoubleToInt is 42 units and (int) truncation is 9 units. The benchmark included adding the result to an accumulator to make sure the result is consumed. So it's much faster (but incorrect). – usr Sep 18 '16 at 11:32