이 글은 마지막 commit id가 4ba5527…인 Go 소스 코드를 기준으로 합니다. 함수명, 변수/상수명, 변수/상수값 등은 버전에 따라 다를 수 있습니다.
최신 Go 소스 코드는 Go Google Git Site에서 확인할 수 있습니다.
정수형 상수를 Go 내부에서 처리하는 방법을 구체적으로 확인해 보자. 이것저것 다 귀찮다면 결론이라도 꼭 보길 바란다.
확인을 위해 작성한 코드는 아래와 같다.
|
|
이 코드를 빌드하면 다음과 같은 오류가 발생한다.
./main.go:4:14: constant too large: 14345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345
./main.go:5:16: shift count too large: 512
./main.go:6:16: constant shift overflow
\(2^{512} \simeq 1.34 \times 10^{154} \)이다. 즉, 13 뒤에 0이 153개 붙는 수이다. 이를 기준으로,
i1
은 14 뒤에 0이 153개 붙은 수i2
는 \(2^{513}\)i3
는 \(2^{512}\)
로 각각 할당했다. 값의 기준은 차치하고, 모두 오류가 발생하는 것으로 보아 타입을 지정하지 않은 정수형 상수도 저장할 수 있는 한계가 있다는 것을 알 수 있다.
아래에서 언급하는 Go 컴파일러 소스 파일들은 별도 언급이 없는 한 모두 아래 경로에 위치한다.
go/src/cmd/compile/internal/gc/
순수 literal 상수의 한계 처리
이제 Go 컴파일러의 소스 코드로 들어가 보자. 앞에서의 복잡한 처리는 무시하고, noder.go 부터 보면 될 것 같다. 이 파일에는 expr()
이라는 함수가 있는데, i1
을 처리하는 부분의 코드는 다음과 같다.
// noder.go
func (p *noder) expr(expr syntax.Expr) *Node {
...
case *syntax.BasicLit:
return p.setlineno(expr, nodlit(p.basicLit(expr)))
...
}
여기서 호출하고 있는 basicLit()
은 다음과 같다.
// noder.go
func (p *noder) basicLit(lit *syntax.BasicLit) Val {
...
case syntax.IntLit:
x := new(Mpint)
x.SetString(s)
return Val{U: x}
...
}
Mpint
는 mpint.go에 다음과 같이 정의돼 있다.
// mpint.go
// Mpint represents an integer constant.
type Mpint struct {
Val big.Int
Ovf bool // set if Val overflowed compiler limit (sticky)
Rune bool // set if syntax indicates default type rune
}
다른 언어에서도 주로 사용하는 용어인 BigInt나 MPInt(Multi Precision Integer)는 주로 암호화나 인증 등 처리를 위해 기본 타입보다 큰 수가 필요할 때 사용할 수 있도록 구현한 것인데, Go에서는 math/big 경로에 big이라는 패키지를 제공하고 있고, Mpint는 big.Int를 포함하는 Go 언어 내부 구조체이다.
다시 basicLit()
함수를 보면, 우선 Mpint를 만든 뒤에 SetString()
함수를 이용해 값을 할당하고 있다.
// mpint.go
func (a *Mpint) SetString(as string) {
_, ok := a.Val.SetString(as, 0)
if !ok {
// required syntax is [+-][0[x]]d*
// At the moment we lose precise error cause;
// the old code distinguished between:
// - malformed hex constant
// - malformed octal constant
// - malformed decimal constant
// TODO(gri) use different conversion function
yyerror("malformed integer constant: %s", as)
a.Val.SetUint64(0)
return
}
if a.checkOverflow(0) {
yyerror("constant too large: %s", as)
}
}
드디어 빌드 시 발생한 오류에서 보였던 문구 “constant too large"가 나왔다. SetString()
함수에서는 일단 big.Int에 문자열—소스 코드는 이 단계에서는 다 문자열로 처리된다.—을 이용해 값을 할당하고, checkOverflow()
함수를 호출해 값이 한계를 넘었는지 검사한다.
// mpint.go
func (a *Mpint) checkOverflow(extra int) bool {
// We don't need to be precise here, any reasonable upper limit would do.
// For now, use existing limit so we pass all the tests unchanged.
if a.Val.BitLen()+extra > Mpprec {
a.SetOverflow()
}
return a.Ovf
}
checkOverflow()
함수에서는 big.Int의 값이 사용하는 비트수 BitLen()
과 함수의 인자로 전달된 extra
의 합계가 Mpprec
보다 크면 한계를 벗어난 것으로 처리한다. 그리고 한계를 벗어난 경우는 위 SetString()
함수에서 “constant too large: 1434~“를 출력한다. extra
는 0으로 넘어왔으니 이제 Mpprec
만 남았다. 끝이 보인다.
// mpfloat.go
const (
// Maximum size in bits for Mpints before signalling
// overflow and also mantissa precision for Mpflts.
Mpprec = 512
// Turn on for constant arithmetic debugging output.
Mpdebug = false
)
Mpprec
의 값은 512이다. 즉, 512개의 비트로 표시할 수 있는 수의 최대값이 타입을 지정하지 않은 정수형 상수의 한계이다.
비트 시프트 연산을 이용한 상수의 한계 처리
이제 i2
와 i3
에 대해 알아보자. 다시 noder.go의 expr()
함수로 돌아가 i2
, i3
에 대해 처리하는 부분의 코드를 보면 다음과 같다.
// noder.go
func (p *noder) expr(expr syntax.Expr) *Node {
...
case *syntax.Operation:
...
x := p.expr(expr.X)
...
return p.nod(expr, p.binOp(expr.Op), x, p.expr(expr.Y))
...
}
대강의 동작은 다음과 같다.
expr.X
에는 “2”,expr.Y
에는 “511” 또는 “512"가 저장돼 있다.expr()
함수 재귀호출을 통해expr.X
,expr.Y
를 처리하는데 모두 순수 literal 상수의 한계 처리에서와 동일한 절차를 거쳐Mpint
로 변환된다. 즉, 2, 511, 512 모두 크기와 관계없이Mpint
가 된다.
호출하는 부분은 건너뛰고, 연산자와 두 개의 Mpint
형태의 피연산자가 준비됐으니 계산하는 코드를 살펴보자.
|
|
i2
를 보자. a
는 2, b
는 512이다. 5라인에서 s
에 512를 넣고 6라인의 조건에 의해 “shift count too large"가 출력된다.
반면 i3
는 b
가 511이므로 6라인은 넘어가지만 16라인에서 checkOverflow()
함수의 인자로 511이 넘어가고 a
의 비트수가 2이므로—a
는 2이므로 2진수로는 두 비트가 필요하다.— overflow 처리돼 “constant shift overflow” 오류가 출력된다.
결론
비트 시프트만을 살펴봤지만 더하기, 곱하기 등을 포함한 다른 연산도 최종적으로는 checkOverflow()
를 통해 상수의 한계값을 넘었는지 검사해 해당하는 오류 문구를 출력한다.
이상에서 살펴본 바와 같이 Go의 타입을 지정하지 않은 정수형 상수는 512개의 비트로 표현되는 수가 한계이다. big.Int에서 부호는 별도의 멤버로 관리하므로 \(2^{512} - 1\), float64
로 변환해 출력하면 1.3407807929942597e+154
로 표시되는 이 값이 바로 최대값이다.