반올림, Rounding vs Banker’s rounding.
C#(.Net)에서 반올림의 Default는 banker’s rounding이다. 이에 대해 인지하지 못하면 조금 황당한 결과를 얻게 된다.
Banker’s Rounding은 반올림을 하는 자리의 수가 5 즉 가운데일 때, 가장 가까운 짝수로 변경해주는 반올림이다. 아래 소숫점 첫자리를 반올림하는 경우를 보자.
0.5 는 가장 가까운 짝수인 0으로,
1.5 는 가장 가까운 짝수인 2로
2.5 는 가장 가까운 짝수인 2로
3.5 는 가장 가까운 짝수인 4로
4.5 는 가장 가까운 짝수인 4로.
이러한 괴랄한 반올림은 반복 연산을 계속할 때 0.5가 1이 되는 추가 에러가 무한대로 증가하는 것을 방지하고자 고안된 것이다.
1에서 10 사이에 있는 .5 로 떨어지는 수를 생각해보자.
0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5
이 수들을 모두 다 수학적 반올림해서 합을 하면 55가 나온다.
정확한 합인 50과 멀어진다. 자연히 10이 아니라 무한대로 범위를 늘리면 합의 에러가 무한대로 늘어난다.
은행이나 세금의 계산에서 수천만명의 금액들을 처리할 때, 끝자리를 끊어버리려고 반올림할 경우 문제가 발생하는 것이다.
따라서, Banker’s Rounding을 고안하여 이 문제를 해결했는데, Banker’s rounding으로
위의 문제를 풀어 보면
0 + 2+ 2+ 4+ 4+ 6+ 6+ 8+ 8+ 10 = 50
50으로 수렴된다. 비록 1원, 2원 등 작은 금액에서 개인 고객의 돈이 무시할 정도의 금액이 절삭 당하는 일은 있어도, 적어도 전체적인 금액에서 은행이나 국가가 득을 보거나 해를 보는 경우가 없게 된다.
C#에서의 아래 예들을 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
float f1 = Mathf.Round(0.5f); float f2 = Mathf.Round(1.5f); float f3 = Mathf.Round(2.5f); float f4 = Mathf.Round(3.5f); float f5 = Mathf.Round(4.5f); float g1 = Mathf.Round(-0.5f); float g2 = Mathf.Round(-1.5f); float g3 = Mathf.Round(-2.5f); float g4 = Mathf.Round(-3.5f); float g5 = Mathf.Round(-4.5f); Debug.Log($"{f1}, {f2}, {f3}, {f4}, {f5}"); //0, 2, 2, 4, 4 Debug.Log($"{g1}, {g2}, {g3}, {g4}, {g5}"); //0, -2, -2, -4, -4 |
모든 반올림이 짝수로 수렴되는 것을 볼 수 있다.
유니티 라이브러리 말고, System의 Math 라이브러리도 아래와 같이 마찬가지이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
double m1 = System.Math.Round(0.5); double m2 = System.Math.Round(1.5); double m3 = System.Math.Round(2.5); double m4 = System.Math.Round(3.5); double m5 = System.Math.Round(4.5); double n1 = System.Math.Round(-0.5); double n2 = System.Math.Round(-1.5); double n3 = System.Math.Round(-2.5); double n4 = System.Math.Round(-3.5); double n5 = System.Math.Round(-4.5); Debug.Log($"{m1}, {m2}, {m3}, {m4}, {m5}"); //0, 2, 2, 4, 4 Debug.Log($"{n1}, {n2}, {n3}, {n4}, {n5}"); //0, -2, -2, -4, -4 |
RoundToInt도 역시 아래와 같이 Banker’s rounding이 기본이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int s1 = Mathf.RoundToInt(0.5f); int s2 = Mathf.RoundToInt(1.5f); int s3 = Mathf.RoundToInt(2.5f); int s4 = Mathf.RoundToInt(3.5f); int s5 = Mathf.RoundToInt(4.5f); int t1 = Mathf.RoundToInt(-0.5f); int t2 = Mathf.RoundToInt(-1.5f); int t3 = Mathf.RoundToInt(-2.5f); int t4 = Mathf.RoundToInt(-3.5f); int t5 = Mathf.RoundToInt(-4.5f); Debug.Log($"{s1}, {s2}, {s3}, {s4}, {s5}"); //0, 2, 2, 4, 4 Debug.Log($"{t1}, {t2}, {t3}, {t4}, {t5}"); //0, -2, -2, -4, -4 |
아래와 같이 강제로 Normal Rounding 옵션을 집어 넣으면 우리가 익히 수학에서 알고 있는 반올림 결과가 나온다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
double r1 = System.Math.Round(0.5, MidpointRounding.AwayFromZero); double r2 = System.Math.Round(1.5, MidpointRounding.AwayFromZero); double r3 = System.Math.Round(2.5, MidpointRounding.AwayFromZero); double r4 = System.Math.Round(3.5, MidpointRounding.AwayFromZero); double r5 = System.Math.Round(4.5, MidpointRounding.AwayFromZero); double w1 = System.Math.Round(-0.5, MidpointRounding.AwayFromZero);; double w2 = System.Math.Round(-1.5, MidpointRounding.AwayFromZero);; double w3 = System.Math.Round(-2.5, MidpointRounding.AwayFromZero);; double w4 = System.Math.Round(-3.5, MidpointRounding.AwayFromZero);; double w5 = System.Math.Round(-4.5, MidpointRounding.AwayFromZero);; Debug.Log($"{r1}, {r2}, {r3}, {r4}, {r5}"); //1, 2, 3, 4, 5 Debug.Log($"{w1}, {w2}, {w3}, {w4}, {w5}"); //-1, -2, -3, -4, -5 |
Programming Language마다 Default Rounding 방식이 다르다. C#(.Net)은 Banker’s Rounding이 기본이니 정확한 수학적 반올림을 요하는 곳에서는 주의해서 써야 한다.