Math.Round의 함정?

Programming 2024. 8. 26. 22:39 by 빠재

Math.Round의 버그?

여느 때와 같이 반올림 코드를 작성하고 테스트를 하는 중 이상한 현상을 발견했습니다. 분명 반올림된 값이 들어가야 하는데 오히려 내림이 되는 경우가 있는 것입니다.

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine(Math.Round(38.4d)); // 38
        Console.WriteLine(Math.Round(38.5d)); // 38 ?????
        Console.WriteLine(Math.Round(38.6d)); // 39
    }
}

우리가 평소 알던 사사오입(0.5 미만은 버리고 그 이상은 올림)과는 다른 결과가 나오는 것 같습니다.

소숫점 정밀도 문제인가 싶어 원래 소수점을 최대 정밀도로 출력해보았습니다.

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine(38.4d.ToString("G17")); // 38.399999999999999
        Console.WriteLine(38.5d.ToString("G17")); // 38.5
        Console.WriteLine(38.6d.ToString("G17")); // 38.600000000000001
    }
}

정밀도 문제는 아닌 것 같습니다.

버그가 아닙니다

이런 C#의 Math.Round의 동작은 사실 공식 문서에도 설명되어 있습니다.

C#에서 정확히 두 정수 사이에 있는 .5의 값을 어떻게 처리해야 하는지는 Midpoint Rounding(중간점 반올림)라고 하며 이와 관련해서 MidpointRounding enum값으로 제어할 수 있습니다. 기본값은 MidpointRouting.ToEven, 즉 가장 가까운 짝수를 가리키도록 되어 있는데, 오사오입, 또는 banker's rounding, 은행가의 반올림 이라고도 합니다. 통계적으로 오차가 적은 반올림 기법이어서 주로 회계, 공학 분야에서 많이 사용한다고 합니다.

유니티의 Mathf.RoundToInt 에서도 오사오입이 적용되고 있습니다.

하지만 회계는 회계고 평소 쓰던 사사오입 반올림을 원하는 경우라면 MidpointRouting.AwayFromZero 인자를 사용하면 됩니다.

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine(Math.Round(38.4d, MidpointRouting.AwayFromZero)); // 38
        Console.WriteLine(Math.Round(38.5d, MidpointRouting.AwayFromZero)); // 39
        Console.WriteLine(Math.Round(38.6d, MidpointRouting.AwayFromZero)); // 39
    }
}

다른 방법으로는 원래 숫자에 0.5를 더한 뒤 Math.Floor를 호출할 수도 있습니다.

public int Round(double num)
{
    return Math.Floor(num + 0.5d);
}

다른 프로그래밍 언어는?

부동소수점 연산에 대한 표준인 IEEE 754에서도 오사오입을 기본 반올림 기법으로 제시하고 있습니다. 그렇기 때문에 최신 프로세서나 모던 프로그래밍 언어 일부가 오사오입을 채택하고 있습니다.

오사오입을 기본으로 사용하는 언어는 아래와 같습니다:

  • C#
  • Python 3
  • R

사사오입을 기본으로 사용하는 언어는 아래와 같습니다:

  • C/C++
  • JavaScript
  • Python 2
  • Go
  • Rust
  • Swift
Nav
1" /> 2" /> 3" /> 4" /> ···" /> 76" />