본 게시글은
서울대학교 데이터사이언스대학원 성효진 교수님의
데이터사이언스를 위한 컴퓨팅 시스템 강의를
학습을 목적으로 재구성하였습니다
오늘은 시스템 프로그래밍 수업의 두 번째 시간
컴퓨터에서 가장 기본이 되는
데이터 타입과 0과 1만 인식하는 컴퓨터가
이러한 데이터 타입을 어떻게 표현하는지에 대한 내용을 배웠다
어떤 프로그램을 컴퓨터로 표현할 때
결국 컴퓨터 내부에서는 그 프로그램을 0과 1로 해석한다
이러한 0과 1의 단위를 Bits(비트)라고 하고
컴퓨터는 이러한 비트를 어떠한 방식으로 조합하고
어떠한 방식으로 표현을 하는지 약속이 되어있다
이번 수업은 비트 표현법의 약속에 대한 내용이고
이는 전자적으로 컴퓨터가 정보를 저장하기 위한 방법이다
아래 그래프를 보면 0일 때는 전압이 0.0V에서 0.2V
1일 때는 0.9V에서 1.1V인 것을 볼 수 있다
우리가 흔히 0과 1로 데이터를 표현한다고하면
이진법을 떠올릴 것이다
컴퓨터와 친해지기 위해서는 이진법 표현법에 익숙해야하는데
간단하게 이진법을 좀 알아보자
이진법은 0과 1로 수를 표현하는 방식이다
가장 큰 수가 왼쪽이고 오른쪽으로 가면서 작아진다
이미 다 알겠지만 위의 예시를 간단하게 살펴보자
십진수인 15213은 11101101101101로 표현할 수 있다
1.20에서 정수부인 1은 이진법으로 1이고
그 아래 0.20의 이진표현은 0011이 반복되는 순환소수이기때문에
1.0011001100110011[0011]과 같은 방식으로 표기한다
그다음 마지막으로 1.5213 * 10의 4제곱은
앞의 1.5213을 이진법으로 표현한다음
10의 4제곱을 2의 13제곱으로 표현하는데
이는 정확하게 10의 4제곱 = 10000이 2의 13제곱은 아니지만
2의 제곱수 중 가장 10000에 가까운 수이기 때문에
그냥 그렇게 표현한다고한다
우리가 컴퓨터에서 흔히 쓰는 단위인 Byte(바이트)는
8비트이다 (1 Byte = 8 Bits)
따라서 1바이트는 00000000부터 11111111까지
표현이 가능하며
십진수로 표현하면 0부터 255까지
16진수로 표현하면 00부터 FF까지이다
보통 16진수를 표현하는 단위를 word라고 부르는데
이게 컴퓨터 시스템에서 매우 중요한 개념이다
왜냐하면 이 word 단위가 프로그램을 access할 수 있는
최적의 단위이기 때문이다
예를 들면 보통 int는 4바이트의 크기를 갖는데
그 바이트를 word단위로 저장한다고한다
또한, c나 c++ 프로그래밍을하며
어떤 주솟값을 출력하면 0xFA1D37B와 같은 형식이
나오는 것을 확인할 수 있는데
이도 word단위로 저장되는 것이라고 한다
우리가 흔히 알고있는 각 데이터타입의 크기이다
위 ppt는 c언어의 데이터 타입이다
데이터 타입 중 크기가 가장 작은 것은 char이며 1바이트이고
range가 작은 int인 short는 2바이트라고한다
그리고 흔히들 알고있는 int는 4바이트이다
그 이후의 데이터타입들은
하드웨어에 따라 크기가 달라질 수도 있다고한다
예전과 같은 경우는 위처럼
고정된 크기를 갖는 data type을 사용하는 것이
국룰(?)이었다
하지만 AI 시대가 되고나서
인공지능 학습시키며 가중치 계산 할 때
가중치의 precision을 16비트로 변환하는 것을
익숙하게 볼 수 있을 것이다
원래 인공지능 가중치의 data type인 float은
32비트가 표준이지만
인공지능의 빠른 학습을 위해
16비트, 혹은 8비트로 정확도를 줄여서 계산을 하는 것이다
이는 메모리 효율을 위하여
정확도를 어느정도 trade off 하는 것인데
인공지능 학습 시에 이런 일들이 많이 발생한다
이제는 이러한 비트를 어떻게 조작하는지
Bit Manipulation에 대해 배워보자
Boolean Algebra는 한국어로 부울대수라고하며
하드웨어를 구성하는 가장 기본적인 로직이다
이를 정의한 사람이 Boole(부울)이고
그 사람의 이름을 따서 부울대수라고 부른다고한다
이 부울대수는 컴퓨터에서
0과 1의 데이터를 바꿀 수 있는 수학적인 정의이다
부울대수에 대해서 간단히 살펴보면
AND
둘다 1이어야 1이고
다른 경우는 다 0
A&B = A and B
OR
둘다 0이어야 0이고
다른 경우는 다 1
A|B = A or B
NOT
0이면 1로
1이면 0으로
반대로 변환해주는 것
~A = not A
XOR
두개가 같으면 0이고
다르면 1
A^B = A xor B
이 부울대수는 하드웨어 계산의 가장 최소 단위이며
이를 조합해서 다양한 하드웨어를 만든다
간단하게 부울대수를 활용한
비트연산에 대해 알아보자
이미 이산수학이나 다른 수업에서
많이 배웠을 것인데
위의 부울대수를 잘 생각하며 적용해보면 쉽다
각각 왼쪽부터 오른쪽 순서대로
And, Or, Not, Xor 연산이다
보통 이런 비트를 집합(set)으로 어떻게 표현하냐면
1인 비트의 index를 표기한다
01101001이 있으면
오른쪽부터 Index 0으로 시작해서 마지막이 7이다
위 예시에서 1인 비트가 있는 index는
0, 3, 5, 6이므로 이를
{0, 3, 5, 6}으로 표현한다
아래 operation은
위 예시의 두 비트 벡터를
각각 연산을 하였고
이를 집합으로 표현한 결과이다
이러한 bit-level operation을
c언어에서 한 번 알아보자
각 &, |, ~, ^는 c언어에서 사용이 가능한데
연산자를 앞에 붙여주면 사용이 가능하다
integral로 표현되는 모든 데이터타입에
적용이 가능하다
char 데이터로 예시를 든 위 ppt를 보면
~0x41, ~0x00, 0x69 & 0x55와 같은 식으로
사용해주면된다
학생들이 이러한 비트 연산자와
Logic operation(논리 연산자)를 많이 헷갈려하는데
헷갈려하면 안된다
가장 큰 차이는 논리 연산자는
결과값이 무조건 true 아니면 false가 나온다는 것이다(1 or 0)
논리 연산자는 &&, ||, !이 있다
보통 비트 연산자가 1개
논리 연산자가 2개씩 붙어있다고 생각하면 쉽다
위 예시는 논리 연산자의 예시인데
모두 결과값이 0x00 혹은 0x01이 나오는 것을 알 수 있다
이제 shift operation에 대해 알아보자
shift operation은 비트값을 옆으로 하나씩 미는 것이다
left shift는 왼쪽으로 미는 것이고
right shift는 오른쪽으로 미는 것이다
비트 단위에서 하나의 자리수는 2의 제곱으로
계속 커지기 때문에
left shift는 기존의 수에다가 2를 곱하는 것과 같고
right shift는 2를 나누는 것과 같다
만약 << 3과 같이 쓰면
3개의 자리만큼 left shift를 했고
나머지는 0으로 채운다
그리고 밀려나는 세개의 비트는 그냥 버린다
right shift에는 채워지는 값을
어떻게 처리하냐에 따라
logical shift와 arithmetic shift로 구분되는데
logical shift는 그냥 0으로 채우는 것이고
arithmetic shift(산술 shift)는 최상위비트(MSB)의 값에
맞게 채워주는 것인데 이는 숫자의 부호와 관련이 있다
앞에서 MSB를 얘기하며 수의 부호 얘기가 나왔는데
이제 우리가 흔히 쓰는 integer에서
signed integer와 unsigned integer에 대해 알아보자
쉽게 말하면 signed integer는 부호가 있는 int이고
unsigned integer는 부호가 없는 int이다
ppt 가장 위의 unsigned 계산을 보면
비트 수가 w개라면
2의 0제곱부터 2^w -1까지의 값을 표현한다
그래서 많이 사용하는 unsigned integer의 경우
4 * 8 = 32비트의 공간을 차지하므로
0부터 2^32 -1까지만큼 표현한다
반대로 signed integer의 경우 부호가 있는 숫자인데
가장 왼쪽의 값 Most Significant Bit(MSB) 값에따라
음수 혹은 양수인지 구분한다
MSB가 0이면 양수, 1이면 음수가 된다
위 B2T(X)식에서 시그마가 누락되어있는데
수식은 이렇다
그러면 c언어에서 15213과 -15213을
이진법으로 확인해보자
위 ppt를 보면
15213은 00111011 01101101
-15213은 1100100 10010011
임을 알 수 있다
위 논리에 따르면 MSB만 다르고
나머지 비트는 똑같아야하는 것이 아닌가?
위 예시를 보면 거의 완전 반대로
비트가 표현되어있음을 확인할 수 있다
왜 이렇게 되어있는 것일까?
이는 바로 이전의 많은 학자들이 연구하다가
MSB만 반대로하여 부호와 수를 표기하는거의
치명적인 허점을 발견했기 때문이다
바로 이렇게 하면 0을 표현하는 방식이
2개가 되어버린다는 점이다
MSB가 0으로 표현된 0과
1로 표현된 0
이렇게 2개가 생겨버리는데
이게 별거 아닌 것 같지만
이렇게 0을 표현하는 방식이 2개라는 점은
컴퓨터가 연산할 때 굉장히 치명적인 문제가 된다
이에 따라 Two's Complement(2의 보수)라는 방법으로
음수를 비트로 표현해주기로 한다
2의 보수는 원칙적으로는
2^n제곱에서 원래 수를 뺀 값이다
하지만 일반적으로
전체 비트를 반전해준다음 1을 더해주는 방식으로
편하게 계산한다
어떻게해서 이렇게 편하게 계산되는지는
다음 슬라이드에서 알아보도록하자
15213과 -15213을 각각 계산한 방법이다
앞의 내용을 잘 이해했다면
크게 어렵지 않을 것이다
15213은 그냥 일반 이진법 계산대로
계산하여 다 더한 것이고
-15213은 가장 왼쪽 비트가 1이기 때문에
이게 32768이 되고
원래 수에서 32678을 빼면 되는 것이다
2의 보수가
모든 비트 반전에 1을 더하면 되는 것임을 증명해보자
증명도 아닌 간단한 이해..?
A + (-A) = 0 이고
1 + (-1)도 0이 된다
따라서 A + (-A) = 0 = 1 + (-1)인데
이를 이항하면
-A = (-1 -A) + 1이 된다
그런데 -1-A는 ~A와 같다
따라서 -A = ~A + 1이 되어버리기 때문에
sign bit를 포함해서 전체를 뒤집은 다음
마지막 자리에 1을 더해준 것과 같은 결과가 나오게 된다
수의 범위에 대한 내용이다
W=16인 수의 unsigned, signed의 min max값이다
여기서 외워두면 좋은 점은
부호가 있을 때 모든 비트가 1이면 -1이 된다
UMax의 경우 MSB를 부호로 치지않기 때문에
모든 비트가 1일 경우 65535인 max값이 나오게된다
각 비트 크기 별
usigned와 signed의 max, min 값이다
이런 int max, long max값이 그렇게 낯설지는 않을텐데
그 이유는 우리가 코딩할 때 흔히 많이 사용하기 때문이다
이런 max, min 값은 c나 c++언어에서
매크로로 정의되어있으며
다들 한 번씩 해봐서 알겠지만
for loop 돌면서 최솟값, 최댓값 찾아줄 때
보통 가장 처음 값을 INT_MAX 혹은 INT_MIN
이런 식으로 선언해준다
이러한 max나 min값은 고정된게 아니고
하드웨어마다 달라질 수 있다
unsigned와 signed의 numeric value이다
signed는 맨 왼쪽의 비트가 1로 바뀌면서
음수로 바뀌는 것을 확인할 수 있다
이러한 signed와 unsigned의
type casting에 관한 내용이다
사실 이 내용은 어느정도 생략하려고한다
왜냐하면 signed와 unsigned를
casting 할 수 있지만
이는 굉장히 위험한 casting 방법이며
실제로 프로그래밍을 할 때 반드시 "지양"해야하는 것이기 때문이다
이런식으로 위험한 캐스팅을 계속 하다보면
내가 예상하지 않았던 값이
나도 모르는 사이에 들어갈 수 있다고한다
보통 conventional적으로
u와 같은 suffix가 없으면
signed integer로 이해한다
컴파일 도중 w bit크기의 어떤 데이터타입을
(ppt에서는 signed integer)
w+k bit크기로 확장시켜야 할 때가 있다
이럴 때 작은 비트에서 큰 비트로
확장시켜주는 것을 Sign Extension이라고 하는데
새로 채워지는 곳에
MSB에 있는 비트를 채워준다
한 마디로 양수인 경우 확장되는 곳에 0이 들어가고
음수인 경우는 1이 들어가게 된다
예시를 한 번 살펴보자
2바이트의 15213을 4바이트로 확장하면
15213은 양수기 때문에 MSB가 0이고
확장되는 앞의 2바이트는 0으로 채워진다
반면에 2바이트의 -15213을 4바이트로 확장할 때
-15213은 MSB가 1이기 때문에
확장되는 앞의 2바이트가 1로 채워진다
반대로 큰 크기의 비트를 작은 크기의 비트로 줄이는 것을
truncate라고 하는데
이는 상위 비트를 제거하고 하위 비트만 남기게 된다
수학적으로 이렇게 하위 비트만 남기게 되는 것이
mod operation(나머지 연산)과 동일한 결과를
갖게 되므로
truncate의 결과는 mod operation과 비슷해진다
다음으로는 unsigned addition이다
unsigned끼리 더하는 연산인데
w bits 크기의 두 데이터가 더해지면서
자릿수가 올라가면 w+1 bits 크기의 데이터가 나올 수 있다
이러한 경우에 이를 처리하는 방식이 2가지가 있는데
Standard Addition Function은
overflow가 되는 비트를 무시하는 truncate 방식이고
Modular Arithmetic 방식은
초과되는 부분을 모듈러 연산을 통해
나머지 값에 대해서만 값을 남겨놓아
비트 범위 내로 조정하는 방식이다
시각화를 하면 이렇게 된다고 한다
overflow가 되었을 때
modular 연산을 수행한 모습이다
2의 보수 더하기에 대한 내용이다
2의 보수 연산에서도
위 unsigned addition과 동일하다
overflow가 된 부분을 그냥 truncate한다
곱하기에서는 어떨까
곱하기는 당연히 더하기보다
overflow가 날 가능성이 증가하게된다
곱하기 연산을 하면 원래 크기였던 w bits보다
큰 값이 나타날 수 있게 되고
값을 정확하게 표현할 수 있는
result range보다 더 커질 수 있다
따라서 너무도 당연하지만
곱셈을 할 때는 데이터 타입을 생각보다 더 크게 정해야한다
또 c라이브러리 중에서 arbitrary precision이라는
library도 있는데
이런 비트 범위를 벗어나는 연산을
자동으로 처리해주는 라이브러리라고한다
내부적으로 result range를 벗어나면
자동으로 값을 더 큰 데이터타입으로 옮겨준다고한다
unsigned의 곱하기 연산이다
이도 비슷하게
overflow된 비트값들을 버리거나
아니면 mod 연산을 수행해서 나머지만 남긴다
아까 위에서 shift를 표현하면서
left shift는 기존 값에 2를 곱해주는 결과와
동일하다고 말을 했었다
위에서 multiplication 연산을 봐서 알겠지만
하드웨어적으로 multiplication 연산은
구현이 굉장히 복잡하다
따라서 2를 곱할 일이 있다면
multiplication 연산을 수행하는 것보다
left shift를 하는 것이 훨씬 편하다
따라서 우리의 컴파일러는
2를 곱하는게 확실하다면
그냥 left shift를 때려버린다
나누기도 마찬가지
divide 연산은 하드웨어적으로
굉장히 복잡한 연산이기 때문에
2를 나눌일이 있다면 right shift를 때려버린다
그렇다면 근본적인 질문
우리는 unsigned를 왜 어떨 때 사용해야하는가?
우선 unsigned에 대해서 정확하게 이해하지 못한다면
"절대 사용하지마라"
실제로 시스템 프로그래밍 수업들으면서
가장 많이 듣는 말이
"underlying에서 무슨 일이 일어나는지도 모르면서
그걸 어떻게 사용하지?" 였다..
(근데 그게 abstraction 아닌가?)
아무튼 unsigned integer를 사용해야하는 경우는
값이 항상 overflow 되지 않으면서
modular한 값을 계속 가질 때 의미가 있는 경우에
사용해야한다고 한다
signed같은 경우는
결과가 원하는 범위 내에 있을 때만 가능하다고 한다
위 예시를 보고 오늘 수업을 마무리하려고한다
array a의 index를 탐색해야하는 i를
unsigned로 정의해준다
index는 0부터 시작하는 양수 값이라
unsigned로 정의해도 괜찮을 것 같지만
c언어 표준에서는 unsigned addition은
나머지 연산과 동일하게 작동하기때문에
프로그래머가 의도한대로 작동하지 않을 수가 있다
따라서 c언어에서는 보통
데이터타입의 크기를 표현하기위해
size_t라는 unsigned value의 데이터 타입이 있고
이를 사용하는 것이 좋다
다음 시간에는
pointer, string 등이 메모리에서 어떻게 표현이 되는지
알아본다고한다
그럼 오늘 수업은 여기까지 ..-!