숫자와 숫자 연산의 의미

숫자와 숫자 연산의 의미 (perlnumber)

이 문서는 Perl이 숫자 값을 내부적으로 어떻게 처리하는지 설명해요. 여기에는 Perl의 연산자 오버로딩(operator overloading) 기능은 완전히 제외됩니다. 연산자 오버로딩은 아주 큰 정수에 대한 연산, 임의 정밀도의 부동소수점 연산, 모듈러 연산이나 p-진(p-adic) 연산 같은 '특이한' 숫자에 대한 연산 등, 숫자에 대한 사용자 정의 동작을 가능하게 해 주는 기능이에요. 자세한 내용은 overload 문서를 참고하세요.

$n = 1234;		    # 10진 정수
$n = 0b1110011;	    # 2진 정수
$n = 01234;		    # 8진 정수
$n = 0x1234;	    # 16진 정수
$n = 12.34e-56;	    # 지수 표기법
$n = "-12.34e56";	    # 문자열로 지정된 숫자
$n = "1234";	    # 문자열로 지정된 숫자
$n = "01234";	    # "1234"와 같음; 문자열의 맨 앞 0은
                    # 8진수를 뜻하지 않음

출처: perlnumber - semantics of numbers and numeric operations in Perl

숫자 저장하기 (Storing numbers)

Perl은 숫자를 내부적으로 세 가지 방식으로 표현할 수 있어요: 네이티브 정수(native integer), 네이티브 부동소수점(native floating point number), 그리고 10진 문자열(decimal string)이에요. 10진 문자열은 "12.34e-56"처럼 지수 표기 부분을 가질 수 있어요. 여기서 네이티브(native) 란 "perl을 빌드할 때 사용한 C 컴파일러가 지원하는 형식"을 뜻해요.

'네이티브'라는 용어는 네이티브 정수에서는 그렇게까지 큰 의미가 없어요. 정수에 '네이티브'가 의미하는 바는, 지원되는 참 정수량(integral quantity)의 최댓값과 최솟값 한계가 2의 거듭제곱에 가깝다는 것뿐이에요.

하지만 '네이티브' 부동소수점은 아주 근본적인 제약이 하나 있어요. 바로 2진 분수(binary fraction)로 변환했을 때 표현이 비교적 '짧은' 숫자들만 나타낼 수 있다는 거예요. 예를 들어 0.9는 네이티브 부동소수점으로 표현할 수 없어요. 0.9의 2진 분수는 무한하기 때문이에요:

binary0.1110011001100...

1100이라는 수열이 계속 반복되죠. 이 제약에 더해, 2진수의 지수(exponent)도 부동소수점으로 표현될 때 제약을 받아요. 일반적인 하드웨어에서 부동소수점 값은 최대 53개의 2진 자릿수와 -1024에서 1024 사이의 2진 지수까지 저장할 수 있어요. 10진수로 환산하면 대략 16개의 10진 자릿수와 -304..304 범위의 10진 지수에 해당해요. 이 모든 것이 시사하는 바는, 그런 아키텍처에서 Perl은 12345678901234567 같은 숫자를 정보 손실 없이 부동소수점으로 저장할 수 없다는 거예요.

마찬가지로 10진 문자열은 10진 전개(expansion)가 유한한 숫자들만 표현할 수 있어요. 문자열이므로 길이가 임의로 길 수 있기 때문에, 이런 숫자들의 지수나 10진 자릿수에는 실질적인 한계가 없어요. (다만 우리가 지금 논의하는 것은 이 숫자들의 저장 규칙일 뿐이라는 점을 기억하세요. 그런 '큰' 숫자를 저장할 수 있다고 해서, 그 숫자들에 대한 연산 이 유효 숫자를 전부 사용한다는 뜻은 아니에요. 자세한 내용은 '수치 연산자와 수치 변환'을 보세요.)

사실 네이티브 정수 형식으로 저장된 숫자는 부호 있는 네이티브 형식이나 부호 없는 네이티브 형식 중 하나로 저장돼요. 따라서 네이티브 정수로 저장되는 Perl 숫자의 한계는 보통 -231..232-1이며, 64비트 정수의 경우엔 그에 맞게 수정돼요. 그리고 이것 역시 Perl이 이 범위의 정수에 대해서만 연산을 할 수 있다는 뜻은 아니에요. 부동소수점 형식으로 훨씬 더 많은 정수를 저장할 수 있으니까요.

정리하면, Perl의 숫자 값은 10진 전개가 유한하거나 2진 전개가 '짧은' 숫자들만 저장할 수 있어요.

수치 연산자와 수치 변환 (Numeric operators and numeric conversions)

앞서 언급했듯이 Perl은 숫자를 세 형식 중 아무 것이나로 저장할 수 있어요. 하지만 대부분의 연산자는 보통 그중 한 형식만 이해합니다. 그런 연산자에 숫자 값이 인자로 전달되면, 연산자가 이해하는 형식으로 변환돼요.

가능한 변환은 여섯 가지예요:

네이티브 정수        --> 네이티브 부동소수점	(*)
네이티브 정수        --> 10진 문자열
네이티브 부동소수점 --> 네이티브 정수		(*)
네이티브 부동소수점 --> 10진 문자열		(*)
10진 문자열        --> 네이티브 정수
10진 문자열        --> 네이티브 부동소수점	(*)

이 변환들은 다음과 같은 일반 규칙에 따라 이루어져요:

  • 원본 숫자를 대상 형식으로 표현할 수 있으면 그 표현이 사용돼요.
  • 원본 숫자가 대상 형식으로 표현할 수 있는 한계 밖에 있으면, 가장 가까운 한계값의 표현이 사용돼요. (정보 손실)
  • 원본 숫자가 대상 형식으로 표현할 수 있는 두 숫자 사이에 있으면, 그중 하나의 표현이 사용돼요. (정보 손실)
  • 네이티브 부동소수점 --> 네이티브 정수 변환에서는 결과의 크기가 원본의 크기보다 작거나 같아요. ('0으로 반올림')
  • 10진 문자열 --> 네이티브 정수 변환을 정보 손실 없이 수행할 수 없다면, 결과는 10진 문자열 --> 네이티브 부동소수점 --> 네이티브 정수 변환 순서와 호환돼요.

이 변환들을 따르는 다음의 (*) 표시는 실제 전환 단계가 C 컴파일러에 의해 수행됨을 의미해요. 특히, 사용 중인 컴파일러의 버그나 특성 때문에 위 규칙 중 일부가 깨질 수 있어요.

Perl 수치 연산의 종류 (Flavors of Perl numeric operations)

숫자 인자를 받는 Perl 연산은 그 인자를 네 가지 방식 중 하나로 다루는데, 정수 형식, 부동소수점 형식, 문자열 형식 중 하나로 강제하거나, 피연산자의 형식에 따라 다르게 동작할 수 있어요. 수치 값을 특정 형식으로 강제한다고 해서 그 값에 저장된 숫자가 바뀌지는 않아요.

정수 형식의 인자가 필요한 연산은 모두 그 인자를 모듈러 연산(modular arithmetic)으로 다루어요. 예를 들어 32비트 아키텍처에서는 mod 2**32처럼요. 그래서 sprintf "%u", -1sprintf "%u", ~0과 같은 결과를 내요.

산술 연산자 (Arithmetic operators)

이항 연산자 + - * / % == != > < >= <= 와 단항 연산자 - abs -- 는 인자를 정수로 변환하려 시도해요. 두 변환이 모두 정밀도 손실 없이 가능하고, 연산도 정밀도 손실 없이 수행될 수 있다면 정수 결과가 사용돼요. 그렇지 않으면 인자가 부동소수점 형식으로 변환되고 부동소수점 결과가 사용돼요. 위에서 설명한 변환 캐싱 덕분에, 정수 변환이 부동소수점 숫자의 소수 부분을 버리지 않아요.

++

++는 위 연산자들과 동일하게 동작하지만, /^[a-zA-Z]*[0-9]*\z/ 형식에 맞는 문자열이라면 perlop에 설명된 문자열 증가(string increment)가 사용돼요.

use integer 동안의 산술 연산자 (Arithmetic operators during use integer)

use integer;가 적용되는 스코프에서는, 위에 나열된 거의 모든 연산자가 인자를 정수 형식으로 강제하고 정수 결과를 반환해요. 예외인 abs ++ --use integer;에서도 동작이 바뀌지 않아요.

기타 수학 연산자 (Other mathematical operators)

** sin exp 같은 연산자는 인자를 부동소수점 형식으로 강제해요.

비트 연산자 (Bitwise operators)

인자가 문자열이 아니라면 정수 형식으로 강제돼요.

use integer 동안의 비트 연산자 (Bitwise operators during use integer)

인자를 정수 형식으로 강제해요. 또한 시프트 연산은 내부적으로 기본인 부호 없는 정수 대신 부호 있는 정수를 사용해요.

정수를 기대하는 연산자 (Operators which expect an integer)

인자를 정수 형식으로 강제해요. 예를 들어 sysread의 세 번째와 네 번째 인자에 적용돼요.

문자열을 기대하는 연산자 (Operators which expect a string)

인자를 문자열 형식으로 강제해요. 예를 들어 printf "%s", $value에 적용돼요.

인자를 특정 형식으로 강제한다고 해서 저장된 숫자가 바뀌지는 않지만, Perl은 그런 변환의 결과를 기억해요. 특히 첫 번째 변환이 시간이 걸릴 수는 있지만, 반복되는 연산은 변환을 다시 수행할 필요가 없어요.

더 알아보기 (Learn more)