PS알못 OrbitHv의 PS logo PS알못 OrbitHv의 PS

작성: 2021-01-16 23:59
수정: 2021-02-06 23:06
난이도: Bronze Ⅴ

※ 이 포스트는 까먹을 만한/더 알면 좋은 것만 정리합니다

나머지

두 수 A와 B의 나머지 연산은 $B=Aq+r(A, B, q\in\mathbb{Z},0\le r\lt A)$의 관계식으로 정의되는데, 이 때 r이 나머지가 된다. 관계식에서 조건으로 주어졌듯이, 나머지 연산은 두 수가 정수일 때만 정의한다. 따라서 실수일 때에는 작동하지 않는다. C/C++에서는 이에 대해 컴파일 에러를 띄운다.

다만 Java와 Python은 위 관계식을 확장시켜서 실수일 때도 돌아가게끔 해놓았다. 정확히는 관계식을 $B-Aq=r$로 고친 후, r이 원하는 범위로 들어올 때까지 B에서 A를 계속 뺀다. 그러다가 r이 적당한 값이 되면 뺄셈을 멈추고 남은 값을 출력한다.

정수형 타입

정수형의 범위와 오버플로우

C/C++

타입 범위 크기
char -128~127 / -27~27-1 1 byte
unsigned char 0~255 / 0~28-1 1 byte
short -32768~32767 / -215~215-1 2 bytes
unsigned short 0~65535 / 0~216-1 2 bytes
int -2147483648~2147483647 / -231~231-1 4 bytes
unsigned int 0~4294967295 / 0~232-1 4 bytes
long int 또는 long long과 같은 범위1 4 or 8 bytes
unsigned long unsigned int 또는 unsigned long long과 같은 범위1 4 or 8 bytes
long long -9223372036854775808~9223372036854775807 / -263~263-1 8 bytes
unsigned long long 0~18846744073709551613 / 0~264-1 8 bytes

Java

타입 범위 크기
boolean false~true 1 byte
byte -128~127 / -27~27-1 1 byte
short -32768~32767 / -215~215-1 2 bytes
char 0~65535 / 0~216-1 2 bytes
int -2147483648~2147483647 / -231~231-1 4 bytes
long -9223372036854775808~9223372036854775807 / -263~263-1 8 bytes

위의 표에서 int를 예시로 들면, int는 32비트(4바이트) 중에서 가장 왼쪽에 있는 1비트를 부호를 나타내는 데에 사용한다.2 이 비트값이 0이면 0을 포함한 양수, 1이면 음수를 나타내는 것이다. n번째 비트를 bn라고 하면 int 형의 비트 배열 {bi}은 $-2^{31}b_{31}+2^{30}b_{30}+…+2^{1}b_{1}$을 나타낸다. bi는 0 또는 1이기 때문에 int의 범위가 위와 같이 정해진다.

이제 두 int 변수의 덧셈을 생각해보자. CPU 위에서 돌아가는 add 명령(어셈블리)은 부호를 신경쓰지 않고 32비트 전체에 대해 가산 연산을 진행한다. 이제 2000000000을 비트로 나타내보면 01110111 00110101 10010100 00000000이 되고, 여기에 2000000000을 또 더하면 11101110 01101011 00101000 00000000이 될 것이다. 그런데 가장 왼쪽 비트를 보면 1이다. 따라서 이 수는 음수임을 알 수 있고, 2의 보수법을 이용하여 부호를 +로 바꿔보면 00010001 10010100 11011000 00000000, 294967296이다. 따라서 int의 범위 안에서는 2000000000 + 2000000000가 -294967296이 되는 것이다. 이런 식으로 부호가 아닌 데이터가 부호 비트에 영향을 주거나, 연산의 결과가 변수 자체의 크기보다 커서 잘려나가는 것을 오버플로우라고 한다.

정수형 상수의 타입 표기

C/C++/Java에서 그냥 숫자만 입력해서 정수형 변수에 값을 대입하는 경우가 있다. 이 때 이 숫자는 int형으로 컴파일러가 자동으로 인식한다. 그러면 int형 밖의 정수는 어떻게 해야할까? 이를 위해서 주어진 상수를 어떤 타입으로 접미사가 존재한다. 그 종류는 아래와 같다.

접미사 타입
(없음) int
U, u unsigned int
L, l long
UL, ul unsigned long
LL, ll long long
ULL, ull unsigned long long

위의 접미사 중 Ll만 Java에 존재한다.

예시
unsigned long long n = 1000000000000000ULL;

고정적인 정수형(C/C++)

intlong 같은 타입들은 환경에 따라 그 크기가 변할 수 있다. 예를 들어, 성능은 썩 아니지만 충분히 작은 CPU가 필요한 시스템은 더 작은 크기를 할당하여 사용해야할 수도 있다. 하지만 이와 같은 환경에 상관없이 언제 어디서나 일정한 크기를 가져야 하는 값이 있을 수 있다. 이 때 사용할 수 있는 타입이 있다. 그 종류는 아래와 같다.

타입 의미 범위 크기
int8_t 8비트 크기의 부호있는 정수 -27~27-1 1 byte
int16_t 16비트 크기의 부호있는 정수 -215~215-1 2 bytes
int32_t 32비트 크기의 부호있는 정수 -231~231-1 4 bytes
int64_t 64비트 크기의 부호있는 정수 -263~263-1 8 bytes
uint8_t 8비트 크기의 부호없는 정수 0~28-1 1 byte
uint16_t 16비트 크기의 부호없는 정수 0~216-1 2 bytes
uint32_t 32비트 크기의 부호없는 정수 0~232-1 4 bytes
uint64_t 64비트 크기의 부호없는 정수 0~264-1 8 bytes

C는 #include <stdint.h>, C++은 #include <cstdint>를 통해 사용할 수 있다.

실수형 타입

실수형의 범위와 특수값

C/C++/Java 기준으로 실수형은 floatdouble이 있다. 여기에 C/C++은 long double이라는 타입을 하나 더 가진다. Python은 실수 또한 미친듯이 넓은 범위를 자랑한다.

타입 범위 크기
float ±3.4028235×1038 / ±(2-2-23)×2127 4 bytes
double ±1.7976931×10308 / ±(2-2-52)×21023 8 bytes
long double ±1.1897314×104932 / ±(2-2-63)×216383 80 bits3

실수형 변수는 부호, 지수부, 가수부로 나뉜다. 부호 비트를 m, 지수부를 {ei}(n개 비트), 가수부를 {si}(k개 비트)라고 하는데, 타입 별 비트 수는 아래와 같다.

타입 부호 지수 가수
Single(float) 1 8 23
Double(double) 1 11 52
(long double) 1 17 64(1/63)

이 비트의 값에 따라 실수형 변수가 나타내는 값은 아래와 같다.

경우
normalized value(지수부가 0 초과 2n-1 미만) $(-1)^{m}\times 2^{e-2^{n-1}+1}\times (1.s_{k-1}s_{k-2}…s_{0})$
denormalized value(지수부가 0) $(-1)^{m}\times 2^{2-2^{n-1}}\times (0.s_{k-1}s_{k-2}…s_{0})$
지수부가 2n-1이고 가수부가 0 부호 비트가 0일 경우 +∞, 1일 경우 -∞
지수부가 2n-1이고 가수부가 0이 아님 NaN

일반적인 경우는 normalized value와 같이 값을 인식한다. 하지만 특수한 경우가 있는데, denormalized, infinity, 그리고 NaN이다. denormalized value는 지수부가 0인 경우인데, 계산법이 약간 다르다. 이는 normalized value의 최소값(float의 경우 $2^{2-2^{n-1}}$)과 0 사이의 간격을 일정하게 메꾸기 위해 사용하는 범위이다. Infinity는 말 그대로 무한대를 의미하며, 실제로는 이 타입이 표시할 수 있는 최대의 실수값을 넘어갔다는 의미로 쓰인다. NaN은 연산의 결과가 정의되지 않는다는 의미로 주로 쓰인다. $\sqrt{-1}$이나 $\frac{0}{0}$같은 경우 그 결과가 NaN으로 나온다.

실수형 변수의 정확성

정수형 변수와는 다르게 실수형 변수는 나타낼 수 있는 값을 순서대로 나열했을 때 그 거리가 일정하지 않다. 가장 거리가 짧은 부분은 denormalized에서의 $2^{2-2^{n-1}}\times 2^{-k}$이고, 가장 거리가 긴 부분은 지수부가 특수값이 아닌 최대값(2n-2)일 때 $2^{2^{n-1}-2}\times 2^{-k}$이다. 값이 커질수록 그 간격도 커지는 경향성을 가진다. 이를 수직선에 나타낸 그림은 아래와 같다.

By Joeleoj123 - Own work, CC BY-SA 4.0, Link

실수형 연산은 정수형 연산과는 다르게 숨겨진 비트 2개를 사용하는데, 연산의 결과값에 가장 가까운 나타낼 수 있는 실수값을 찾는 데에 사용된다. 이 과정에서 값의 정확성이 떨어지게 된다.

정확성 때문에 발생하는 오차가 하나 더 있다. 바로 연산의 순서에 따라 결과값이 달라진다는 것이다. 아래의 예시를 보면 알 수 있다.

#include <stdio.h>

int main() {
    printf("%.15lf\n", 20000000000+0.000001+0.000001);
    printf("%.15lf\n", 20000000000+(0.000001+0.000001));
}

이것을 실행시키면 값이 각각 20000000000.000000000000000, 20000000000.000003814697266이 나온다. 첫 번째 것은 더했는데도 심지어 값이 그대로다! 일반적인 수학이라면 덧셈의 순서가 어떻든 값이 같아야하지만(결합법칙) 여기서는 성립하지 않을 수가 있다.

정리


  1. 환경에 따라 다르다.  2

  2. 편의상 가장 왼쪽이라고 했지만 big-endian, little-endian, middle-endian이냐에 따라 그 위치가 다르다. 

  3. long double이 80비트를 할당받아야 한다는 것은 강제사항은 아니다. 언어나 환경에 따라 변수의 크기가 다를 수도 있다. 경우에 따라 8바이트, 80비트, 16바이트 등을 사용한다.