Go Lang/Study

[GoLang] 실수 오차 없애기

DSeung 2023. 10. 28. 21:09

문제

Go나 타 언어에서 실수(float)를 비교하면 예상했던 것과 다른 결과를 나타내는 경우를 볼 수 있습니다.

아래 코드로 예를 들 수 있습니다.

package main

import "fmt"

func main() {
	// Go에서 지역 변수는 간단한 네이밍 권함
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = 0.3

	if a+b == c {
		fmt.Println("이 조건문은 실행되지 않습니다.")
	}

	// 출력해보면 조건식이 맞을 것 같지만
	fmt.Printf("%f + %f = %f (%v)\n", a, b, a+b, a+b == c)
	// 실제로는 0.1, 0.2, 03는 정확한 0.1, 0.2, 0.3이 아니다. (Go에서 자체 반올림해서 보여주기 때문에 더 햇갈릴 수 있다.)
	fmt.Printf("%0.18f + %0.18f = %0.18f (%v)\n", a, b, c, a+b == c)
}

이 코드를 실행시키면 아래와 같습니다.

0.100000 + 0.200000 = 0.300000 (false)
0.100000000000000006 + 0.200000000000000011 = 0.299999999999999989 (false)

이렇게 되는 이유는 소수점 이하의 수들은 2의 마이너스 승으로 표하게 됩니다.

그 이유는 컴퓨터는 2진수를 사용하기 때문이죠.

 

그렇다 보니 2의 마이너스 승으로는  정확한 0.1을 만들 수 없게 된 것입니다.

그래서 대부분의 언어에서는 근사치를 사용해 실수를 표현하곤 합니다.

 

해결법

그렇다고 0.1 + 0.2 == 0.3과 같은 간다한 비교 전혀 불가능하진 않습니다.

언어마다 해결법이 다른데 Go에서는 아래 두 가지 방법이 있습니다.

package main

import (
	"fmt"
	"math"
	"math/big"
)

func main() {

	// 1. 작은 오차 무시하기
	// go에서 실수를 표현할 때 2가지 수가 발생합니다.
	// 이때 두 수의 차이는 마지막 비트 하나 밖에 차이가 발생하지 않으므로 이걸 무시하면 됩니다.
	// Go에서는 편리하게 math 패키지의 Nextafter 함수가 이를 해결해줍니다.
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = 0.3

	// math.Nextafter는 첫 번째 인수의 값이 두 번째 인수보다 작으면 1비트 증가 크면 1비트 감소시키고 값을 반환해줍니다.
	// 이걸로 비교 가능합니다.
	fmt.Printf("%0.18f + %0.18f = %0.18f (%v)\n", a, b, c, math.Nextafter(a+b, c) == c)

	// 2. math/big을 사용해서 비교합니다.
	d, _ := new(big.Float).SetString("0.1")
	e, _ := new(big.Float).SetString("0.2")
	f, _ := new(big.Float).SetString("0.3")

	g := new(big.Float).Add(d, e)
	// Cmp는 f가 작으면 -1을 크면 1를 같으면 0을 반환합니다.
	// math/big은 format을 사용시 에러 발생하므로 주의가 필요합니다.
	fmt.Println(d, " + ", e, " = ", f, " (", f.Cmp(g) == 0, ")")

}

결과는 아래와 같습니다.

0.100000000000000006 + 0.200000000000000011 = 0.299999999999999989 (true)
0.1  +  0.2  =  0.3  ( true )

 

컴퓨터의 실수 표현에 경우 훌륭하게 정리되어있는 블로그가 많으니 같이 참고하시면 좋을 것 같습니다.

반응형