(1화) 빠르게 끝내는 컴퓨터 구조
CS
목차
개요
이 글을 읽는 방법
기본적으로 이 글은 프로그래밍 경험 있는 독자, 컴퓨터 구조에 대해 아주 조금이라도 아는 독자들에게 추천합니다. 내용은 기초 레벨이며 프로그래밍을 위해선 이 내용 정도는 알아야한다고 생각하는 필자입니다.
처음 등장하는 용어들에 대해선 최대한 각주 등으로 설명하나, 생략되어있을 수 있습니다.
또한 목차 순서대로 가지 않아도 되고, 모르는 개념만 골라서 읽어도 좋습니다.
수정할 부분이나 궁금한 점 등은 아래의 메신저로 연락 주시면 감사드리겠습니다.
Discord: @rlawnsdud
1. 컴퓨터가 이해하는 데이터
1-1
차례에선 이 2진법에 대해 자세하게 다룰 예정이며, 다음 절에선 명령어의 정의와 이에 대한 기본적인 것을 설명하고, 글 전체에 걸쳐 자세히 설명할 예정이다.
컴퓨터 구조론 기초에 들어가기 앞서 가장 중요한, 기본적인 요소라 하면 컴퓨터는 2진법, 즉 0
과 1
만 이해할 수 있다는 것이다.
1-1. 데이터
먼저 2진법이라는 단어가 생소할 수 있다. 우리가 일반적으로 쓰이는 숫자 체계는 무엇인가?
1-1-1. n진법
체계라 하면 이해가 안될 수 도 있으니 쉽게 생각해보자. 우리가 쓰는 숫자(아라비아 숫자)는 총 10개의 숫자가 있다. (1 2 3 4 5 6 7 8 9 0
)
9
를 넘어가면 10
이 된다는건 당연시 되어왔을 건데, 10
이란 수는 1
과 0
의 조합인 수다. 이처럼 9
를 넘어가면 기존의 숫자인 1
과 0
의 조합이 된다. 즉 한 자리에서 수가 10
이 될때마다 자리올림이 발생하고, 다음 자리로 1
을 올려주는 과정이라 할 수 있다.
10
은 1 x 10 + 0 x 1
라는 식으로 표현할 수 도 있다. 일의 자리에서 10
으로 올려졌으므로 십의 자리로 1
이 올라갔다.
즉 n진법
이라 하면, n
을 기준으로 하여 자리올림하는 수의 표현법이라 할 수 있다.
이제부터 여러 진법의 수가 나올텐데, 그냥 쓰면 헷갈릴 수 있다. 이때 아래첨자를 사용하여 이를 구분하는데, 이 글에선 괄호 속 수를 통하여 여러 진법을 구분할 것이다.
예를 들어
- 2진법의
1011
=>1011₂
또는1011b
- 8진법의
144
=>144₈
또는144o
- 10진법의
104
=>104₁₀
또는104d
(생략)- 16진법의
3F2
=>3F2₁₆
또는0x3F2
라는 식으로 표현할 예정이다. 다만 10진법의 경우 일반적으로 사용하는 진법이라 접미사는 생략할 예정이다.
1-1-2. 2진법
설명이 길었다. 이제 이 체계를 그대로 적용하는데, 숫자가 0
과 1
밖에 없는것이 2진법이다.
0₂
다음 1₂
이 되고, 그 다음(1₂ + 1₂
)은 자리올림이 발생하여 10₂
이 된다. 여기서 다음은 11₂
, 그 다음은 100₂
이 되는 식이다. (각각 0
1
2
3
4
에 해당한다)
이는 원리이고, 이를 쉽게 계산하는 방법이 크게 두가지가 존재한다. 하나는 나눗셈을 이용하는 방법, 다른 하나는 각 자리에 2의 n제곱
을 적용하여 계산하는 방법이다.
이 글에서 설명한 방법은 후자를 택하여 계산해볼 예정이다. 만약 1011
이라는 2진법을 10진수로 변환해보자.
1
2
3
4
5
6
7
1 0 1 1 b
8 4 2 1 d
=> 2³ x 1
+ 2² x 0
+ 2¹ x 1
+ 2⁰ x 1
이 과정을 반대로 하면 10진수를 2진수로 변환할 수 있다. 이 방법 말고도 다른 방법이 있으나, 다른 글을 참고하길 바란다.
1-1-2-1. 2진법의 음수 표현
그럼 음수는 어떻게 표현되는가에 대한 궁금증을 가졌을 수 도 있다.
결론부터 말하면 2의 보수를 통해 음수로 변환하여 연산한다. 보수의 사전적 의미는 보충해주는 수인데, 10진법에서 2에 대한 5의 보수는 3이 된다. 필자가 수학을 좋아하진 않으나 자세한 설명은 생략하겠다..
2진법에서 1의 보수는 모든 자리의 수를 1로 만드는 수이다. 즉 0101₂
(5)의 1의 보수는 1010₂
(10)가 된다.
그 다음으로 2의 보수는 1의 보수에 1을 더하면 된다. 예를 들어 0101₂
(5)의 1의 보수는 1010₂
(10)이고, 2의 보수는 1011₂
(11)가 된다. 검산을 했을 때 0101₂
+ 1011₂
= 1 0000₂
이 되므로, 2의 보수가 맞다.
왜 1의 보수와 2의 보수를 설명했냐, 먼저 1의 보수를 이용한 음수 표현을 알아보자.
음수는 최상위 비트(가장 왼쪽의 비트)를 부호 비트로 사용한다.(0
= 양수, 1
= 음수) 그리고 남은 비트의 1의 보수를 구하면 음수가 된다. 즉 기존의 양수가 2^8개(256)의 수를 표현할 수 있었다면, 최상위 비트를 부호 비트로 사용하기 때문에 표현 범위는 -127 ~ 127가 된다.
하지만 여기서 문제점이 발생하는데, 0의 보수 1111 1111₂
과 0인 0000 0000₂
, 즉 0이 2개가 된다. 이러한 문제점으로 고안된 것이 2의 보수이며, 오늘날엔 2의 보수를 사용한다. 2의 보수로 설정하게 되면 -128 ~ 127개로 위 문제가 해결된다.
예시로 -3 + 5
를 계산 해보자. 3의 2의 보수는 1111 1101₂
이고, 5는 0000 0101₂
이다. 이를 더하면 아래와 같다:
1
2
3
4
1111 1101 (-3)
+ 0000 0101 (5)
---------
1 0000 0010 (2)
여기서 최상위비트 1은 캐리이다. 결과값이 음수라면 플래그라는 곳에 음수 부호 플래그가 저장된다.
1-1-3. 그 외 진법
이 외에도 8진법이나 16진법 등이 있으나, 16진법의 경우 컴퓨터를 다루다보면 자주 등장하는 진법이니 간략히 설명하겠다.
16진법은 이름에서도 알 수 있듯이 16개의 수를 사용하는데, 여기선 0-9
의 수와 A-F
의 문자로 총 16개를 사용한다.
10진수 10
은 16진수 A₁₆
로 표현될 수 있고, 10진수 15
은 F₁₆
라 표현되며, 그 다음은 16
은 10₁₆
로 표현된다. 20
은 14₁₆
가 된다.
16진수의 변환법, 8진수 등은 다른 글을 참조해보길 바란다.
1-1-4. 문자의 표현
컴퓨터에선 문자 또한 0과 1로 표현된다. 이를 설명하기 전에 문자 집합과 인코딩/디코딩에 대해 알아보자.
- 문자 집합(charset): 컴퓨터가 이해 가능한 문자의 모음
- 인코딩(encoding): 문자를 이진으로 이루어진 데이터로 변환하는 과정
- 디코딩(decoding): 이진으로 이루어진 데이터를 문자로 변환하는 과정
컴퓨터는 문자를 인코딩/디코딩할때 문자 집합을 참조하여 변환하기 때문에, 어떠한 문자가 해당 문자 집합에 속하지 않는다면 해당 문자를 표현할 수 없다.
만약 문자 집합이 영어 알파벳(a-zA-Z)만 존재한다면, 2와 같은 숫자의 문자는 표현할 수 없다는 의미다.
문자 집합엔 여러가지가 있으나, 이 글에서 알아볼 문자 집합은 아래의 ASCII와 유니코드다.
1-1-4-1. ASCII
초기에 개발된 문자 집합 중 하나로, 영어 알파벳, 아라비아 숫자(0-9), 일부 특수문자 및 제어 문자로 구성되었다.
예를 들어 ASCII A
는 65
로 인코딩된다. 반대로 97
이라는 값이 있다면, 이는 ASCII a
로 디코딩된다. 이처럼 각 문자 집합의 문자엔 하나의 할당된(부여된) 값이 존재하는데, 이를 코드 포인트라고 한다.
ASCII는 문자 하나당 8비트로 구성되는데, 표현 가능한 문자는 128개 뿐이다.
그럼 7비트 아니냐? (2⁷), ASCII는 7비트를 문자로 할당하고, 나머지 하나는 오류 검출을 위한 패러티 비트인데, 이는 추후에 설명할 예정이다.
하지만 이러한 ASCII만으론 한글, 한자 등의 다양한 문자를 표현하기엔 턱없이 부족하다. 이러한 문제를 해결하기 위해 Extended ASCII 등의 확장된 문자 집합도 등장하였으나, 여전히 부족하다.
그래서 초장기엔 여러 언어에 해당하는 문자 집합을 개발하게 되었으나, 각 언어마다 매번 문자 집합을 다르게 해줘야 하며 여러 언어를 혼용할 수 없기 때문에 유니코드라는 문자 집합이 등장한다.
1-1-4-2. 한글의 표현
유니코드를 설명하기 전에 한글의 표현에 대해 설명하겠다. 먼저 한글은 영어와는 다르게 초성, 중성, 종성의 조합으로 문자가 만들어진다.
그렇기에 한글 문자를 하나하나 등록해둔 완성형 인코딩 방식과 조합형 인코딩 방식으로 나눠지게 된다.
완성형 인코딩 방식
말 그대로 문자 하나하나를 등록해두고, 이에 대한 코드 포인트를 부여한다. 당연히 ASCII에 비해 코드 포인트의 크기가 커질 것이고, 이는 조합형 인코딩 방식에서도 마찬가지다.
'가'
, '나'
, '다'
… '힣'
등의 거의 모든 글자를 등록한다. 여기서 ‘거의’라는 의미는 존재하지 않는 글자 또한 존재한다는 의미고, 이는 곧 완성형 인코딩의 단점이 된다.
조합형 인코딩 방식
완성형과는 다르게 글자 하나를 조합하는 방식이며, 예를 들어 간
이라는 글자가 있다면 ㄱ + ㅏ + ㄴ
으로 조합된다.
EUC-KR
KS X 1001, KS X 1003 문자 집합 기반으로, 완성형 인코딩 방식인 EUC-KR이 등장한다. 글자 하나 당 2바이트 크기의 코드 포인트가 부여된다.
EUC-KR은 약 2000개 이상의 한글을 표현할 수 있으나, 그럼에도 복잡한 글자는 표현할 수 없다. 이러한 이유로 유니코드가 자주 사용된다.
1-1-4-3. 유니코드
유니코드는 전세계의 모든 문자를 일괄되게 표현할 수 있게 하는 표준 문자 집합이다. 유니코드의 코드 포인트를 표현할땐 U+[16진수]
형식으로 표현된다.
유니코드엔 UTF-8
, UTF-16
, UTF-32
와 같이 다양한 문자 집합이 있다.
1-2. 데이터의 단위
앞서 열심히 2진법에 설명한 이유는 알겠지만 컴퓨터는 2진수로만 처리할 수 있기 때문이다. 즉 지금 보고있는 글자도, 사진도, 유튜브의 동영상도 모두 2진수로 되어있다는 의미이다.
여기서 0
과 1
, 그 최소한의 단위를 비트(bit)
라고 하며, 8비트라 하면 8자리의 이진수를 뜻한다. (예: 10011100₂
)
허나 비트로만 표현하면 단위로 쓰기엔 너무 클때가 있고, 추후에 설명하겠지만 컴퓨터에서 처리할 수 있는 최소 단위는 바이트(Byte)다.
1바이트는 8비트이며, 추후에 설명할 ASCII 또한 문자 하나당 1바이트로 할당된다.
그 이상의 단위는 다음과 같다:
단위 | 크기 |
---|---|
1바이트(byte) | 8비트(bit) |
1킬로바이트(kB) | 1000바이트 |
1메가바이트(MB) | 1000킬로바이트 |
1기가바이트(GB) | 1000메가바이트 |
1테라바이트(TB) | 1000기가바이트 |
단위의 크기를 나타낼 때 1024 라는 수가 많이 나오기도 하는데, 들의 단위는 kiB, MiB 등의 단위로 나타낸다. 1000단위로 나타내는것을 SI 단위계, 1024씩 나타내는 단위를 이진 단위라 한다.
1-2-1. 워드
마지막으로 워드(Word)라는 단위가 자주 등장할 예정인데, 이는 CPU가 한번에 처리할 수 있는 데이터의 크기를 의미한다.
만약 CPU가 64비트 CPU라면 1 word = 64
이며, 아래와 같은 워드 단위가 존재한다.
- 하프(half) 워드: 워드의 절반
- 풀(full) 워드: 워드를 의미함
- 더블(double) 워드: 워드의 두배
1-3. 명령어
메모리 간단 설명
이 내용은 “메모리”1 목차에 넣으려 했으나, 이 개념을 모른다면 제대로된 설명이 어려워서 미리 설명하려고 한다.
우리가 컴퓨터에서 어떠한 프로그램을 실행했을 때, 그 프로그램의 명령어와 그 프로그램의 데이터는 메모리에 적재된다.2 그리고 CPU에서 프로그램의 명령어를 실행하기 위해, 또는 데이터 읽기/쓰기를 위해 메모리에 접근할 땐 이 메모리에 접근한다.3
메모리에 어떤 데이터가 적재될 때, 어떠한 공간에 적재되는데 이 공간엔 주소라는 개념이 존재한다. 메모리에 적재된 데이터엔 모두 주소가 할당되어있고, 이 주소를 통해 메모리에 적재된 데이터에 접근하게 된다.
이 주소라는 개념때문에 미리 설명을 하였고, 이제부터 “주소에 접근한다” 라는 문장은 메모리에 저장된 데이터 또는 명령어를 가져오기 위해 주소를 사용하여 가져온다(접근한다) 라는 의미가 된다.[$mem4]
추가적으로 레지스터는 추후 설명하겠지만 CPU 내부의 임시적으로 데이터를 저장 할 빠른 메모리라고 생각하면 된다. CPU가 메모리에 접근하는 속도보다 CPU 내부의 레지스터에 접근하는것이 더욱 빠르기 때문에, 연산 등에선 레지스터를 사용하게 된다.
목차 상 명령어를 먼저 설명하나, 원활한 이해를 위해 후반의 컴퓨터 구조(메모리, 레지스터 등)를 먼저 보고 오거나, 오고가며 병행하는 것도 좋은 선택이다.
우리가 사용하는 모든 컴퓨터의 프로그램은 명령어의 집합이다.
프로그램, program
- (명사) 진행 계획이나 순서. 또는, 그 목록. 순화어는
계획(표)
,차례(표)
.∼을 짜다
- (컴퓨터) 어떤 문제를 해결하기 위해 컴퓨터에게 주어지는 처리 방법과 순서를 기술한 일련의 명령문의 집합체.
정의 출처: Oxford Languages
위 사전과 같이 프로그램은 어떠한 명령어들이 순서대로 짜여져있는것을 의미하며, 컴퓨터 또한 이 순서로 프로그램의 명령어를 실행하게 된다.
1-3-1. 명령어의 구조
우리가 누군가에게 명령을 할때, 일반적으로 ~을 대상으로 ~무엇을 해라
식으로 말한다. 이처럼 컴퓨터의 명령어 또한 ‘무엇’에 해당하는 수행할 연산과 ‘대상’엔 연산에 사용될 데이터(또는 주소)가 해당된다.
예를 들어, '1과 2를 더해라'
라는 명령문은 컴퓨터에선 명령어 add
와 연산에 사용될 1
과 2
로 나뉠 수 있다.
컴퓨터에선 무엇(명령)
을 연산 코드(opcode, operation code), 연산에 사용될 데이터(또는 주소) 대상을 피연산자(operand)라 칭한다.
즉 컴퓨터의 명령어는 opcode와 operand의 조합인것이다.
또한 이러한 명령어는 instruction이라고 하고, 이러한 명령어의 집합을 instruction set이라고 한다.
해당 글에선 명칭의 가독성을 위해 opcode와 operand로 통일한다.
또한, 아래에서 설명하는 명령어는 이해를 돕기 위한 가상의 명령어이며, CPU 아키텍쳐4, 종류에 따라 재각각 다르다.
1-3-1-1. opcode
opcode엔 다양한 역할의 opcode가 존재하는데, 크게 아래와 같은 종류의 opcode가 존재한다.
- 데이터(또는 주소) 전송
- 데이터 연산 (산술 논리)
- 흐름 제어
- 입출력 제어
각 종류에 대해 아래와 같은 명령어들이 존재하며, 여러 CPU 아키텍쳐에 따라 다르나 대부분 비슷한 계열의 명령어들이 존재한다.
데이터 전송엔 데이터를 옮기는
MOVE
, 메모리에 저장하는STORE
, 메모리에서 꺼내오는LOAD
, 이 밖에 스택과 관련된PUSH
,POP
과 같은 명령어가 존재한다.데이터 연산엔 ALU가 담당하는 연산들의 명령어가 존재하며,
ADD
(덧셈),SUB
(뺄셈),MUL
(곱셈),DIV
(나눗셈) 등과 같은 기본적인 사칙연산 명령어들과 논리 연산자(AND
,OR
등)와 같은 명령어가 존재한다.흐름 제어에 대해 배우기 전에 프로그램 카운터(PC)에 대해 알면 좋으나, 이는 추후에 설명하고 지금은 ‘프로그램의 명령어들은 프로그램 카운터(PC)에 따라 실행된다’ 라고 생각하자. 여기서 흐름 제어는 이러한 프로그램 카운터를 제어하여 명령어들의 순서 등을 옮기는 명령어다.
여기엔 특정 주소로 순서(PC)를 옮기는
JUMP
, 프로시저와 관련된CALL
,RETURN
등의 명령어가 존재한다. 여기서HALT
와 같은 프로그램을 종료하는 명령어도 흐름 제어에 속한다.입출력 제어엔 추후에 설명하겠지만, 외부 장치와 소통하는것을 일반적으로 칭한다. 즉 외부 장치와 소통하기 위한 인터럽트와 관련된 기능과 같은 명령어들이 존재한다.
1-3-1-2. operand
operand(피연산자)엔 데이터가 직접 들어가거나 주소(또는 레지스터)가 들어간다. 즉, 해당 주소에 접근한다는건 해당 주소 공간에 있는 데이터에 접근한다는 의미이다.
여기서 몇개의 주소를 넣을것인가에 따라 n주소 명령어라고 한다. 만약 PUSH와 같이 operand가 없는 경우 0주소 명령이라고 한다.
다시 돌아와서 피연산자에 주소가 들어갈 수 있다고 했는데, ‘그냥 데이터를 직접 넣으면 되지 않나’라고 생각할 수 있다. 만약 32비트의 명령어에 opcode가 1바이트(8비트)라 한다면 operand의 크기는 3바이트(24비트)가 된다. 하지만 만약 데이터가 32비트 이상이라면 명령어에 직접 넣기엔 공간이 부족하다.
이 처럼 32비트의 데이터를 메모리에 저장하고, 그 메모리 주소를 명령어에 넣음으로써 32비트 데이터를 사용할 수 있게 된다. 연산에 사용되는 데이터가 저장된 메모리 주소 위치를 유효 주소(effective address)라고 한다.
명령어에 주소를 넣을땐 다양한 방식이 있는데, 이러한 방식을 메모리 주소 지정 방식이라고 한다.
1-3-1-3. 메모리 주소 지정 방식
주소 지정 방식엔 설명하는 글 또는 아키텍쳐마다 이름이 다를 수 있다. 다만 그 방식에 대해선 대부분 같은 역할이므로 방식에 대해서 이해만 해도 좋다.
메모리 주소 지정 방식(Addressing Mode)엔 크게 아래와 같은 방식이 존재한다.
- 묵시적 주소 지정(Implied Addressing)
- 명령어에서 operand가 없을 경우, 묵시적 주소 지정이라고 한다. PUSH, POP과 같은 0주소 명령에서 사용되는 방식이다.
- 즉시 주소 지정(Immediate Addressing)
- 연산에 사용할 데이터를 직접 명시하는 방식으로, 데이터를 메모리나 레지스터에서 찾는 과정이 없기 때문에 빠르나 데이터 크기의 제한이 있다.
- 직접 주소 지정(Direct Addressing)
연산에 사용할 유효 주소를 명시하는 방식으로, 데이터를 직접 넣는 방식에 비해 데이터의 크기가 커지나 메모리에 접근해야되기 때문에 속도가 느려진다.
또한 32비트 명령어에서 opcode가 8비트라면 operand의 크기는 24비트, 유효 주소의 크기 또한 24비트로 제한된다. 즉 주소가 32비트라면 명시가 불가하다.
- 간접 주소 지정(Indirect Addressing)
- 직접 주소 지정 방식의 단점 중 하나인 유효 주소의 크기가 24비트로 제한된다는 점을 보완하기 위함이며, 데이터가 저장된 유효 주소가 다른 주소에 저장이 되어있고, 이 다른 주소를 명시함으로써 단점을 보완한다. 하지만 메모리 접근을 2번 이상 접근해야된다는 단점이 있다.
- 레지스터 주소 지정(Register Addressing)
- 직접 주소 지정과 방식은 같으나, 메모리 접근이 아닌 레지스터 접근을 통해 데이터를 접근하는 방식이다. 레지스터에 대해선 추후 설명할 예정이나, 미리 보고와도 좋다.
레지스터에 접근하는것이 메모리에 접근하는 것 보다 빠르므로 직접 주소 지정 방식보다 빠르다.
- 레지스터 간접 주소 지정(Register Indirect Addressing)
- 간접 주소 지정 방식과 비슷하나, 레지스터에 저장된 유효 주소를 레지스터로 통해 접근하는 방식이다. 레지스터의 특징으론 메모리 접근보다 빠르다는 특징이 있으므로, 이 또한 간접 주소 지정 방식보다 빠르다.
이 밖에도 주소를 어떠한 값(레지스터 등)과 계산하여 구하는 방식인 계산에 의한 주소 지정(Calculated Addressing) (또는 변위 주소 지정, Displacement Addressing)이 존재한다.
변위 주소 지정 방식은 명령어에 두개 이상의 operand가 있고, 이들을 사용하여 계산한다.
- 상대 주소 지정(Relative Addressing)
- 프로그램 카운터(현재 명령어의 위치)에 operand의 값을 더해서 유효 주소를 구한다.
- 인덱스 레지스터 주소 지정(Indexed Register Addressing)
- 인덱스 레지스터(추후에 설명할 예정)와 operand의 값을 더해서 유효한 주소를 구한다.
- 베이스 레지스터 주소 지정(Base Register Addressing)
- 베이스 레지스터(추후에 설명할 예정)와 operand의 값을 더해서 유효한 주소를 구한다.
이 밖에도 아키텍쳐에 따라 다양한 주소 지정 방식이 존재한다. 허나 위에서 설명한 주소 지정 방식은 대부분 존재하므로 알아두는것이 좋다.
추가적으로 주소 지정 방식의 속도를 빠른 순으로 정리하면 다음과 같다.
1
묵시적 > 즉시 > 레지스터 > 직접 > 레지스터 간접 > 간접 > 변위
1-3-2. 프로그래밍 언어론
여기까지 열심히 읽었다면 의문이 들 수 있는 내용인데, 우리가 프로그래밍을 할 때 컴퓨터는 ‘이 소스 코드5를 어떻게 해석할까?’ 라는 내용이다. 컴퓨터는 0과 1만 이해할 수 있는데 프로그래밍 언어는 문자로 이루어져 있기 때문이다.
1-3-2-1. 고급 언어와 저급 언어
먼저 아래 내용을 설명하기 전에 고급 언어(High Level)와 저급 언어(Low Level)로 나뉘게 된다. 우리가 일반적으로 쓰는 프로그래밍 언어는 대부분 고급 언어일것이다. 간단히 사람이 이해하기 쉽게 만들어진 언어이다.
저급 언어엔 두가지가 있는데, 어셈블리어(Assemble language)와 기계어(Machine Code)이다. 기계어는 말 그대로 기계가 이해할 수 있는, 0과 1로만 이루어진 코드이다. 이는 컴퓨터가 직접 이해할 수 있는 반면, 0과 1로만 이루어져 있기 때문에 사람이 읽기가 매우 어렵다. 때문에 이러한 기계어의 명령어를 1:1 대응 문자6로 대응시킨것이 어셈블리어이다.
어셈블리어는 아래와 같은 형식으로, 다음은 고급 언어를 어셈블리어로 번역시킨 것이다7.
1
2
3
int a = 1;
int b = 2;
int c = a + b;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
section .data
a dd 1 ; a = 1
b dd 2 ; b = 2
c dd 0 ; c = 0 (초기화)
section .text
global _start
_start:
; a와 b를 더하여 c에 저장
mov eax, [a] ; a의 값을 레지스터 eax로 로드
add eax, [b] ; eax에 b의 값을 더함
mov [c], eax ; 결과를 c에 저장
; 프로그램 종료
mov rax, 60 ; 시스템 호출: exit
xor rdi, rdi ; 종료 코드 0
syscall
이 처럼 어셈블리어는 기계어와 1:1 대응으로 명령어와 피연산자로 이루어져있다. 하지만 지금도 비전공자가 읽기엔 어려움이 있고, 때문에 고급 언어가 등장하게 된다. 고급 언어는 일반적으로 어셈블리어로 번역되고, 그 어셈블리어가 1:1 대응되어 기계어로 번역된다.
이는 즉 소스 코드를 0과 1로 바꿔(번역되어) 실행이 된다는 얘기인데, 이 과정은 크게 컴파일(Compile) 또는 인터프리트(Interpret) 과정을 통해 이루어진다.
1-3-2-2. 컴파일
컴파일 과정은 소스코드를 실행 전 전부 0과 1[^binary]로 이루어진 기계어(명령어의 집합)로 번역된다.
대표적으로 C 언어를 예를 들 수 있는데, C 언어의 컴파일 과정은 크게 다음과 같다:
1
전처리 -> 컴파일 -> 어셈블 -> 링크
- 전처리(preprocess)
- C에서
#include
는 외부 라이브러리나 파일을 가져오는 전처리기다. 내부적으로 어떻게 작동할까? 정답은 가져온 파일의 내용을 그대로8 복사하여 가져오는 방식이다. 이 과정은 보이지 않는데, 이를 전처리 과정을 통해 처리하게 된다. 즉 컴파일하기 전 처리하는 과정을 의미한다. 파일 확장자는.i
로 출력된다.
- C에서
- 컴파일(compile)
- 전처리기에서 전처리된 코드를 어셈블리어로 번역하는 과정이다. 예시는 위 고급 언어와 저급 언어에서 서술하였다. 파일 확자자는
.s
로 출력된다.
- 전처리기에서 전처리된 코드를 어셈블리어로 번역하는 과정이다. 예시는 위 고급 언어와 저급 언어에서 서술하였다. 파일 확자자는
- 어셈블(assemble)
- 이제 어셈블리어로된 코드를 기계어로 번역하는 과정이다. 하지만 이 상태로는 바로 실행되기가 어려운데, 이유는 외부 파일이나 라이브러리들이 기계어에 완벽히 포함되지 않았기 때문이다. 그래서 다른 파일이나 라이브러리를 합치는 과정을 다음 과정인 링킹이다. 아직 실행파일(exe 등)이 아니고, 목적 코드(Object Code)라고하며, 파일 확장자는
.o
이다.
- 이제 어셈블리어로된 코드를 기계어로 번역하는 과정이다. 하지만 이 상태로는 바로 실행되기가 어려운데, 이유는 외부 파일이나 라이브러리들이 기계어에 완벽히 포함되지 않았기 때문이다. 그래서 다른 파일이나 라이브러리를 합치는 과정을 다음 과정인 링킹이다. 아직 실행파일(exe 등)이 아니고, 목적 코드(Object Code)라고하며, 파일 확장자는
- 링크(link)
- 마지막으로 여러 목적 코드를 합치는 과정으로, 이 과정을 링킹(Linking)이라고 한다. 이 과정을 마지막으로 실행 파일이 생성되며, 윈도우에선
.exe
등의 확장자가 대표적이다.
- 마지막으로 여러 목적 코드를 합치는 과정으로, 이 과정을 링킹(Linking)이라고 한다. 이 과정을 마지막으로 실행 파일이 생성되며, 윈도우에선
미리 기계어로 번역(컴파일)이 되기때문에 실행 시 실행 속도가 추후 설명할 인터프리터보다 빠르다는 장점이 있다. 또한, 컴파일 과정 중에 오류가 검출되기 때문에 오류가 발생할 가능이 비교적 적다.
다만 컴파일하는 과정에서 메모리와 시간이 소비되고, 실행 파일의 크기가 클 수 있다는 단점이 존재한다.
컴파일 과정에 대해 자세한 내용은 이 글을 참고하면 좋을 듯 하다.
1-3-2-3. 인터프리트
인터프리트 방식은 컴파일 방식과는 다르게, 소스 코드에서 한번에 한 문장(코드)씩 해석하고 실행된다. 예를 들어, 아래와 같은 코드가 있을 때 인터프리트 방식은 다음과 같다.
1
2
3
a = 10
b = 20
print(a + b)
- 메모리 공간
a
에10
을 저장한다. - 메모리 공간
b
에20
을 저장한다. a
와b
를 더해서 출력한다.
인터프리트 과정은 이 과정을 프로그램을 실행하는 동안 번역하며 한줄한줄 실행한다. 만약 컴파일 방식의 언어였다면 위 실행 단계를 모두 컴파일하여 실행 파일을 만들고, 그 실행 파일엔 위 실행 단계들이 기계어로 번역되어 저장되었을 것이다.
이 처럼 인터프리터 언어는 한줄한줄 실행하며 번역되기 때문에 컴파일러 언어의 장단점과 반대된다. 컴파일 방식보다 실행 속도는 느리지만 컴파일 과정이 없기 때문에 실행 시 메모리 및 시간 소비가 없고9, 실행 파일이 없기 때문에 실행 파일의 크기를 고려할 필요는 없다.
단점으론 오류가 실행 중 발생할 가능성이 있다. 예를 들어, 정의되지 않은 변수가 있을 때 컴파일 방식에선 컴파일 전 오류를 띄우지만, 인터프리터 방식에선 대부분 실행 중 오류가 발생한다.
또한 이식성도 생각을 해야한다. 컴파일 방식에서 실행 파일은 CPU의 아키텍쳐마다, 운영체제 마다 구조가 다르기 때문에 각 환경에 맞춰 컴파일(빌드)를 해야한다. 하지만 인터프리트 방식은 소스 코드를 실행하는 인터프리터만 있으면 되기에 각 환경에 맞춰 컴파일하여 배포할 필요가 없다.
1-3-2-4. 하이브리드 컴파일러
하이브리드 방식의 경우 컴파일과 인터프리트 방식을 혼합한것이며, 소스 코드를 미리 바이트 코드(Bytecode)라는 코드로 컴파일하며, 이 바이트 코드를 가상 머신에서 인터프리트하여 실행한다.
하이브리드 방식 언어인 자바로 예를 들자면, 자바는 실행 전 바이트 코드 파일인 .class
파일로 컴파일된다. 이 과정은 실행파일 컴파일 과정보단 메모리/시간 소비가 적으며, 이러한 바이트코드는 JVM이라는 자바 가상 머신에서 실행된다.
때문에 컴파일과 인터프리트 방식의 중간이며, 각 장점을 합친 방식이다. 자세한 내용은 프로그래밍 언어론 (PLT)라는 이론을 참고하길 바란다.
2. 컴퓨터 구조
서론이 너무 길었다. 사실 글 전체를 1시간만에 끝내려고 했는데, 거의 450줄 가량을 써버렸다. 아무튼 이제부터 본격적으로 컴퓨터 구조론에 대해 들어가보려고 한다.
“컴퓨터 구조”라 하면 보통 크기 2가지의 파트로 나뉘게 된다. 하나는 앞서 설명한 “컴퓨터가 이해하는 데이터”와 이 파트에서 서술할 “컴퓨터의 부품”이다.
컴퓨터는 크게 4가지의 부품으로 구분할 수 있다.
- CPU(중앙처리장치)
- 메모리(주기억장치)10
- 보조기억장치(SSD, HDD 등)
- 입출력장치(외부 키보드, 마우스, 모니터 등)
그리고 이들을 서로 이어주는 통로인 버스라는 개념도 추후 설명한다.
메모리에 대해선 너무나 중요한 개념이고, 이 개념을 모른다면 제대로된 설명이 불가능하기 때문에, 메모리에 대해 잊어버렸다면 “명령어” 절에서 설명한 메모리 간단 설명을 보고 오는걸 추천한다.
2-1. CPU
CPU는 Central Processing Unit의 약자로, 직역하면 중앙 처리 장치이라 부른다. 이름 그대로 컴퓨터에서 가장 중요한 핵심 부품이며, 이 장치에서 데이터의 연산/처리, 제어 등을 하게 된다.
CPU의 구조는 크게 아래와 같이 구분된다.
- 산술 논리 연산 장치(ALU, Arithmetic and Logical Unit)
- 제어장치(Control Unit)
- 레지스터(Register)
- 캐시 메모리(Cache Memory)
2-1-1. ALU
ALU는 이름 그대로 연산 장치로, 덧셈, 뺄셈, 곱셈부터 논리 연산, 시프트 연산 등의 컴퓨터가 데이터를 연산할 때 필요한 연산 장치이다. 사실 큰 그림만 본다면 위 설명으로 끝낼 수 있다. 더욱 간단히 말하자면 계산기에 비유할 수 있다.
ALU는 아래와 같은 입력과 출력을 가진다.
- 입력
- 제어장치로 부터 -> 제어신호
- 레지스터로 부터 -> 피연산자(연산할 데이터)
- 출력
- 결과값 -> 레지스터
- 플래그 -> 플래그 레지스터
ALU의 입력엔 단순히 “계산을 하기 위한 필요한 정보”가 입력된다. 우리가 연산을 할 때 “OO과 OO을 더해라” 라고 생각하는 것 처럼, 입력에도 레지스터로 부터 연산할 데이터(피연산자)와 제어장치로 부터 어떤 연산을 할 지 수행할 연산을 입력받는다.
그리고 출력으론 결과값을 레지스터로 내보낸다. 그리고 결과값이 담긴 레지스터를 메모리에 저장하거나, 다시 연산등을 하게 된다.
그 다음으론 플래그(Flag)라는 용어가 등장한다. 이는 연산한 결과에 대한 부가적인 정보가 담긴다. 예를 들어, 연산 결과가 0이라면 Zero 플래그가 1이 될것이고, 앞서 설명했다 싶이 음수가 되면 부호 플래그가 1이 될것이다. 또한 연산 결과가 제한된 크기를 넘을 때 발생하는 오버로드 플래그 등도 여기에 저장되며, 이러한 플래그가 저장되는 레지스터를 플래그 레지스터라고 한다.
2-1-2. 제어장치
제어장치는 제어신호를 발생시키거나 받아서 제어하고, 명령어를 해석하는 장치이다. 제어장치의 입력과 출력엔 아래와 같다:
- 입력
- 플래그 레지스터로 부터 -> 플래그
- 명령어 레지스터로 부터 -> 해석할 명령어
- 다른 장치로 부터 -> 제어신호
- 출력
- 제어신호 -> 레지스터 또는 ALU 등의 CPU 내부
- 제어신호 -> 메모리 또는 입출력장치 등의 CPU 외부 (제어 버스)
자주 등장하는 용어인 제어 신호부터 설명을 하자면, 제어 신호는 이름 그대로 제어를 위한 특별한 신호를 의미한다. CPU 내부로 전달되는 제어 신호는 레지스터 간 데이터 전송, ALU엔 어떠한 연산을 처리하라 등의 제어 신호이며, CPU 외부로 전달되는 제어 신호는 메모리 접근, 입출력 장치 인터럽트 등의 제어 신호를 보낸다. 외부로 전달될 땐 제어 버스라는 통로로 이동되는데, 이는 추후에 따로 설명할 예정이다.
또한 추후 설명할 외부에서 인터럽트 등의 제어 신호가 제어장치로 들어오는 경우도 있고, 이 또한 제어 버스를 통한다.
이 처럼 주요 기능중 하나인 제어 신호를 통한 제어가 있고, 다른 주요 기능인 명령어 해석 또한 제어 장치에서 처리된다.
메모리에서 명령어를 가져올때(인출할때) 메모리 레지스터라는 레지스터에 우선 가져오고, 제어장치에서 명령어를 해석할때 이 레지스터에서 인출한다. 또한, 플래그 레지스터에서 부가적인 정보 또한 사용하여 명령어를 해석하고 실행한다.
명령어의 구조는 앞서 설명했으며, 해석 과정은 추후 설명할 예정이다.
2-1-3. 레지스터
위에서도 자주 언급을 하여 대략 무엇인지 유추해볼 수 있으나, 레지스터는 CPU 내부의 임시적으로 데이터를 저장할 수 있는 메모리 공간이다. 그럼 굳이 왜 메모리(RAM)11에 접근하지 않고 CPU 내부의 임시 메로리에 접근하는가? 라는 의문이 들 수도 있다.
먼저 메모리별 속도 순위는 다음과 같다.
1
레지스터 > 캐시 메모리 > 메모리(RAM)
여기서 힌트를 얻을 수 있는데, 메모리에 접근하는 것 보다 레지스터에 접근하는것이 더 빠르기 때문이다. 즉 계산 등에서 임시로 사용할 데이터를 메모리에 저장, 인출을 하게 된다면 꽤나 비효율적이게 된다.
물론 레지스터에 임시 데이터만 저장되는것은 아니고, 주소나 명령어, 그 외 특수한 레지스터들이 존재한다.
아래에서 설명할 레지스터는 통상적으로 불리는 이름이며, 각 아키텍쳐에 따라 조금씩 차이가 나거나 EAX (Extended Accumulator Register)와 같이 다른 이름으로 불릴 수 있다.
여러 레지스터를 설명하기 앞서, 레지스터의 종류에는 범용 레지스터(General Purpose Register)와 특수 레지스터(Special Register)로 구분될 수 있다.
범용 레지스터(또는 일반 레지스터)는 연산에 직접적으로 사용되는 데이터나 주소 등이 범용적으로 저장되는 레지스터로, 프로그램의 명령어에 따라 임의로 값을 변경하거나 그 용도를 변경할 수 있는 레지스터이다.
특수 레지스터는 그 밖에 특수한 목적으로 사용되는 레지스터이며, 프로그래머가 임의로 값을 변경하거나 용도를 변경할 수 없는 레지스터이다.
2-1-3-1. 프로그램 카운터
CPU가 메모리에 올아와있는 프로그램의 명령어를 실행할 때 그 명령어가 있는 메모리 주소를 어떻게 찾을까? 정답은 프로그램 카운터(PC, Program Counter)에 있다. 명령어가 한번 인출될때마다 프로그램 카운터가 하나씩 증가하고, 어떠한 프로그램의 시작점을 바탕으로 접근하여 메모리에 저장된 명령어를 인출하게 된다.
프로그램 카운터는 하나의 레지스터이며, 명령어 포인터(Instruction Pointer)라고도 불리며, 특수 레지스터에 포함된다.
2-1-3-2. 데이터 레지스터
데이터 레지스터(DR, Data Register)는 범용 레지스터 중 하나로, 이름 그대로 데이터를 저장할 수 있는 레지스터이다. 주로 메모리에서 가져온 데이터를 임시로 저장하는 레지스터이다.
2-1-3-3. 명령어 레지스터
명령어 레지스터(IR, Instruction Register)는 PC를 바탕으로 가져온 명령어를 저장하며, 다른말로 현재 실행할 명령어가 저장되는 레지스터이다. 이 레지스터에 있는 명령어를 제어장치가 해석하고 실행하게 된다.
2-1-3-4. 메모리 주소 레지스터
메모리 주소 레지스터(MAR, Memory Address Register)는 CPU가 메모리에 접근하기 위해 메모리 주소를 저장하는 레지스터이다.
2-1-3-5. 메모리 버퍼 레지스터
버퍼(Buffer)는 어떤 장치(또는 메모리)에서 다른 장치로 데이터를 전달하거나 받을 때 시간의 차이를 맞추기 위해 데이터를 일시적으로 저장하는데, 이 공간을 버퍼라 한다.
위 버퍼의 설명대로 CPU에서 주기억장치에 접근할 때 물리적으로 속도차이가 날 수 밖에 없는데, 이 속도 차이를 보완하기 위해 전달하고자 하는 데이터를 메모리 버퍼 레지스터(MBR, Memory Buffer Register)에 저장해두고 시간 차이를 줄인다음 전달하게 된다.
2-1-3-6. 플래그 레지스터
플래그 레지스터(Flag Register 또는 PSR, Program Status Register)는 ALU 파트에서 설명한 플래그가 저장되는 레지스터이다.
2-1-3-7. 누산기
누산기(AC, Accumulator)는 ALU의 저장 결과가 저장되는 레지스터로, CPU에 따라 데이터 레지스터에 포함되기도 한다.
이 밖에도 입출력장치에 관련한 IO 주소/버퍼 레지스터 등의 다른 레지스터가 존재하나 이는 추후 서술할 예정이다.
2-1-4. 명령어 사이클
CPU의 제어장치에서 하나의 명령어를 처리할 때 마다 각각의 과정이 있는데, CPU는 그 흐름을 반복하여 명령어들을 처리한다. 이 흐름 또는 과정을 명령어 사이클이라 하며, 명령어들이 일정하게 반복되는 주기를 의미한다.
가장 먼저 명령어를 메모리에서 인출하는 인출(Fetch) 사이클, 명령어를 실행하는 실행(Execute) 사이클이 기본적이며 그 사이 인터럽트(Interrupt)과 간접(Indirect) 사이클이 발생하기도 한다.
인출 사이클은 메모리에 있는 명령어를 PC(Program Counter)를 참조하여 가져온다. 가져왔다면 PC += 1
이 된다.
실행 사이클은 명령어를 실행하는 단계로, 아래의 세부적인 단계로 나눌 수 있다.
- 명령어 해독(Instruction Decode) - Fetch하여 명령어를 해독한다.
- 데이터 인출(Data Fetch) - 피연산자(operand)의 데이터를 메모리 등에서 가져온다. 이 과정에서 후술할 간접 사이클이 발생하기도 한다. 0주소 명령어일 경우 이 과정이 생략될 수 있다.
- 데이터 처리(Data Process) - 본격적인 연산을 하는 단계로, 피연산자를 연산한다.
- 데이터 저장(Data Store) - 연산 후 그 결과를 저장한다. 경우에 따라 이 과정은 생략될 수 있다.
여기서 간접(Indirect) 사이클은 메모리 주소 지정 방식에 따라 발생할 수 있다. 예를 들어, 간접 주소 지정(Indirect Addressing)의 경우 메모리 접근을 2번 하게 되는데, 이 처럼 데이터 인출을 위해 메모리에 2번 이상 접근하게(메모리 접근이 추가적으로 필요하면) 되면 간접 사이클이 발생하게 된다.
즉, 명령어 사이클를 다이어그램으로 표현하면 아래와 같다.
1
2
3
4
5
6
7
8
9
|-------------|
/--> | Fetch Cycle | -->\
/ |-------------| \
|-----------------| ^ | |----------------|
| Interrupt Cycle | | | | Indirect Cycle |
|-----------------| | v |----------------|
\ |---------------| /
\<-- | Execute Cycle | <--/
|---------------|
2-1-5. 인터럽트
위 명령어 사이클에서 인터럽트(Interrupt)라는 용어가 등장하는데, 인터럽트는 끼어들기, 가로막기 등의 의미를 가지고 있다. 의미 그대로 명령어 사이클 중 예외 등이 발생하여 CPU가 급하게 일을 처리해야 할때, 현재 작업을 중단하고 다른 작업을 먼저 처리하는것을 의미한다.
인터럽트엔 2가지로 분류되는데, 동기 인터럽트와 비동기 인터럽트(또는 하드웨어 인터럽트)로 분류된다.
2-1-5-1. 동기 인터럽트
예외 인터럽트라고도 불리는 동기 인터럽트는 명령어 실행 과정중에 발생하는 인터럽트이다. 예를 들어, 0으로 나누는 연산을 수행하려고 하면 Division by zero 예외가 발생하는데, 이 또한 동기 인터럽트이다.
동기 인터럽트엔 아래와 같은 종류가 존재한다.
- 폴트(Fault) - 잘못된 명령어나 잘못된 데이터 접근, 불가한 연산 등이 발생할 때 오류가 폴트에 해당한다. 이는 예외 처리 등으로 방지할 수 있다.
- 트랩(Trap) - 프로그래머가 의도적으로 발생시키는 인터럽트로, 디버깅 등에서 의도적으로 트랩을 발생시키는 경우가 존재한다.
- 중단(Abort) - 프로그램을 강제로 중단시키는 인터럽트이다.
- 소프트웨어 인터럽트 - 프로그램이 코드 상 명시적으로 요청한 인터럽트로, 시스템콜 등이 해당된다.
2-1-5-2. 비동기 인터럽트
비동기 인터럽트는 하드웨어 인터럽트 등으로도 불리며, 이름 그대로 입출력 장치 등에서 발생하는 인터럽트이다. 예를 들어, 키보드의 어떤 키를 눌렀을 때 키보드라는 입출력장치는 CPU에게 인터럽트를 발생시킨다.
하지만 키보드라는 물리적인 스위치를 가진 장치는 CPU보다 훨씬 느릴 수 밖에 없는데, CPU가 이 처리 속도를 기다리는것은 매우 비효율적이다.
이를 해결하기 위해 인터럽트를 발생시켜 이러한 작업을 효율적으로 처리할 수 있다. 키보드는 언제 눌릴 지 모르기 때문에 이러한 인터럽트는 비동기 인터럽트로 분류한다.
인터럽트는 아래와 같은 순서로 처리된다.
- CPU에게 인터럽트 요청 신호를 보낸다.
- CPU는 하나의 명령어 사이클이 끝나고, 명령어를 인출하기 전에 인터럽트 요청 신호를 확인한다.
- 만약 인터럽트 요청이 확인됐다면, 인터럽트 플래그 레지스터를 통해 인터럽트를 받아들일지 말지 결정한다.
- 만약 인터럽트를 받아들일 수 있다면, 지금까지의 작업을 스택에 백업하고, 인터럽트 벡터를 참조하여 인터럽트 서비스 루틴을 실행한다.
- 인터럽트 서비스 루틴이 끝나면, 백업한 작업을 복구하고 다시 실행한다.
인터럽트 요청 신호, 인터럽트 플래그
인터럽트 요청 신호는 이름 그대로 인터럽트를 요청하는 신호이다.
CPU가 인터럽트 요청을 받아들이기 위해선 플래그 레지스터 중 하나인 인터럽트 플래그에서 인터럽트를 받아들일 수 있다고 해야 인터럽트를 받아들일 수 있다.
다만 일부 인터럽트(컴퓨터 강제 종료 등)는 인터럽트 플래그를 무시하고 실행될 수 있다. 이 처럼 막을 수 없는 인터럽트는 non-maskable interrupt라고 부르기도 한다.
인터럽트 서비스 루틴, 인터럽트 벡터
인터럽트 서비스 루틴은 인터럽트가 발생했을 때, 해당 인터럽트를 어떻게 처리해야 될지 정의한 코드이다. 인터럽트 서비스 루틴 또한 하나의 프로그램이기 때문에, 메모리에 저장되어 있다.
만약 어떠한 인터럽트가 발생했을 때, 인터럽트 서비스 루틴을 실행하기 위해 PC(Program Counter)를 해당 메모리 주소으로 이동해야 하는데, 이러한 인터럽트 서비스 루틴의 정보(메모리 주소 등)가 저장된 것이 인터럽트 벡터이다.
인터럽트 서비스 루틴으로 이동하기 위해선 현재 작업(레지스터 등에 저장된 데이터 포함)을 어딘가에 백업해둬야 하는데, 이는 메모리 상의 스택 영역에 저장된다. 인터럽트를 마치면 다시 백업된 메모리 주소로 돌아온 다음 레지스터 등의 데이터를 복구한다.
그렇다면 키보드를 눌렀을 때를 예를 들어 과정을 설명해보자.
어떠한 키를 눌렀을 때 CPU에게 키를 눌렀다는 인터럽트 요청 신호를 보낸다. CPU는 하나의 명령어를 처리하고 인터럽트 요청 신호를 확인하는데, 인터럽트 요청 신호가 있다면 인터럽트 플래그를 확인하여 처리가 가능한지 여부를 결정한다.
만약 가능하다면 현재 작업을 스택에 백업해두고, 인터럽트 벡터에서 키보드 입력과 관련한 인터럽트 서비스 루틴을 찾는다. 이 인터럽트 서비스 루틴엔 키보드 키를 눌렀을 때 어떤 작업을 처리할것인지가 저장되어있다.
처리가 완료되면 다시 원래 주소로 돌아와 복구한 다음 명령어를 계속 이어나간다.
2-1-6. 코어와 스레드
이 파트부터 아래의 명령어 집합 구조까지 CPU의 속도/성능과 관련한 내용이며, 이 파트를 끝으로 CPU에 대한 내용은 끝난다.
먼저 원초적으로 돌아가서, 일종의 하드웨어 관점에서 생각해보자. CPU는 일정한 주기를 이뤄 움직이는데, 이 일정한 주기를 주는 부품이 크리스탈(Crystal)이라는 오실레이터(Oscillator) 부품이다. 전기/전자 공학은 설명하지 않으니 간략하게 설명한다면 일정한 주기의 주파수(전기 신호)를 발생시키는 부품이다.
이 부품을 바탕으로 움직이는 일정한 주기를 클럭(Clock)이라 한다.12 그럼 당연히 이 클럭 속도가 빠르면 CPU 속도 또한 빨라질것이다.
하지만 전기의 특성 중 하나로, 열손실이 발생한다는 문제가 존재한다. 이는 CPU가 매우 뜨거워진다는 의미인데, 이를 식히기 위해 별도의 장치가 필요하게 된다.
그래서 CPU 클럭 속도를 무지성으로 늘릴 순 없으며, 다른 방법을 통해 성능을 향상시켜야 한다.
2-1-6-1. 코어
다시 원초적으로 돌아가서, 속도를 늘리려면 클럭 속도를 제외하고 어떤 방법이 있을까? 바로 단순하게 CPU의 부품(ALU, 제어장치, 레지스터, 캐시 등)을 여러개로 늘리면 된다. 이러한 CPU의 주요 구성 부품이 담긴 유닛을 코어(Core)라고 한다.
코어가 하나일 경우 단일코어, 여러개의 경우 멀티코어라 부른다. 하지만 코어만 마냥 늘린다고 속도가 꼭 빨라지는것은 아니다.
예를 들어, 어떠한 명령어를 처리하는데 코어가 1개만 있어도 충분하게 처리할 수 있으나 여러개인 경우 그 외의 코어가 필요하지 않게된다.
2-1-6-2. 스레드
스레드(Thread)는 어떠한 프로세스13이나 코어 등에서 실행되는 흐름의 단위이다. 스레드는 하드웨어적 스레드와 소프트웨어적 스레드로 구분된다.
하드웨어적 스레드
하드웨어적 스레드는 하나의 코어에서 동시에 몇개의 작업을 처리할 수 있는지를 의미한다. 예를 들어, PC 레지스터와 중요한 레지스터14들을 여러개가 존재한다면 코어 내에서 실행되는 작업 또한 그만큼 증가하게 된다.
즉, 코어에서 몇개의 작업을 동시에 처리할 수 있는지를 나타낸 것이 하드웨어적 스레드이다.
소프트웨어적 스레드
소프트웨어적 스레드는 하나의 프로그램에서 독립적으로 실행되는 흐름을 의미한다. 예를 들어 브라우저에서 두 개 이상의 인터넷 창을 켜두고 동시에 작업할 수 있는데, 이처럼 하나의 프로그램에서 독립적으로 실행되는것을 소프트웨어적 스레드라고 한다.
자세한 내용은 운영체제 글의 “동시성”에서 설명할 예정이며, 지금은 이런게 있다 정도로만 알아도 충분하다.
2-1-7. 병렬 처리(파이프라인)
지금까지 잘 이해했다면 한가지 의문이 들 수 있다.
“명령어 하나를 처리할 때 단계가 있는데, 그 단계가 각각 겹치지만 않으면 명령어 처리를 더 빠르게 할 수 있지 않을까?”
이러한 생각으로 고안한것이 명령어 파이프라인(Instruction Pipeline)이다. 하나의 명령어가 처리되면 다른 명령어가 처리되는것이 아닌, 하나의 명령어가 실행 도중 다른 명령어가 실행되는 식으로 명령어를 동시에 여러개를 실행하는 기법이다.
먼저 명령어의 실행 단계는 크게 인출(Fetch)
-> 해석(Decode)
-> 실행(Execute)
-> 저장(Store)
순으로 볼 수 있다. 여러 명령어를 처리할 때 아래와 같이 표기할 수 있다.
t₁ | t₂ | t₃ | t₄ | t₅ | t₆ | t₇ | t₈ | t₉ | t₁₀ | |
---|---|---|---|---|---|---|---|---|---|---|
I1 | Fetch | Decode | Execute | Store | ||||||
I2 | Fetch | Decode | Execute | Store | ||||||
I3 | Fetch | … | ||||||||
I4 |
(T = 시간 흐름, I = 명령어 n)
우리가 전에 배운 내용이라면 위와 같이 명령어 처리 과정이 다 끝나야 다음 명령어를 처리할 수 있다고 배웠다. 하지만 파이프라이닝을 통해 아래와 같이 동시 처리가 가능해진다.
t₁ | t₂ | t₃ | t₄ | t₅ | t₆ | t₇ | t₈ | t₉ | t₁₀ | |
---|---|---|---|---|---|---|---|---|---|---|
I1 | Fetch | Decode | Execute | Store | ||||||
I2 | Fetch | Decode | Execute | Store | ||||||
I3 | Fetch | Decode | Execute | Store | ||||||
I4 | Fetch | Decode | Execute | Store |
2-1-7-1. 해저드
이렇게 하나의 시간동안 여러개의 작업을 처리할 수 있으며, t₄
처럼 하나의 시간동안 4단계의 과정을 모두 처리할 수 도 있다. 하지만 과연 모든 명령어에 파이프라이닝이 적용될 수 있을까? 아래의 예시를 보자.
1
2
1 M1 = 1 + 2
2 M2 = M1 + 3
이러한 경우 첫번째 줄에서 M1에 저장까지 끝나야 둘번째 줄의 M1 + 3
을 처리할 수 있으며, 이 경우엔 파이프라이닝이 불가하다. 이처럼 파이프라이닝이 불가능한 경우를 해저드(Hazard)라고 하며 데이터 해저드, 구조적 해저드, 컨트롤(제어) 해저드로 나눌 수 있다.
데이터 해저드
데이터 해저드는 위 예시와 같이 어떠한 명령어가 끝까지 처리돼야 다음 명령어를 처리할 수 있는 경우로15, 이 경우엔 파이프라이닝이 불가하다.
구조적 해저드
구조적 해저드는 동시에 처리되는 명령어가 같은 ALU나 레지스터 등을 사용하려고 할 때 발생한다. 명령어가 동시에 실행된다 하더라도 다른 일부 부품은 하나씩 처리할 수 밖에 없기 때문에 누군가는 기다려야 한다.
컨트롤 해저드
컨트롤 해저드는 명령어들이 동시에 처리 도중 어떤 명령어가 다른 분기16로 jump
등으로 이동할때 생기는 문제이다.
t₁ | t₂ | t₃ | t₄ | t₅ | t₆ | t₇ | t₈ | t₉ | t₁₀ | |
---|---|---|---|---|---|---|---|---|---|---|
I1 | Fetch | Decode | Execute | Store | ||||||
I2 | Fetch | Decode | Execute (JUMP) | Store | ||||||
I3 | Fetch | Decode | Execute | Store | ||||||
I4 | Fetch | Decode | Execute | Store |
만약 위 경우처럼 I2
에서 다른 위치(분기)로 JUMP하게 된다면 그 뒤의 I3
와 I4
에서 해석, 인출되던 과정은 그대로 버려지게 된다. 이러한 점을 막기 위해 분기 예측(Branch Prediction) 기법이 등장하게 된다.
2-1-7-2. 슈퍼스칼라
슈퍼 스칼라(Superscalar)는 하나의 코어 내에 파이프라인을 여러개 두는 기법으로, 해저드가 발생하지 않는다면 파이프라인의 수에 비례하여 명령어 처리 속도가 증가하게 된다. 하지만 파이프라인이 여러개인 만큼 그만큼의 해저드 발생 위험이 높으며, 현실적으로 파이프라인에 따라 명령어 처리 속도가 비례하여 증가하지 않는다는것을 의미한다.
2-1-7-3. 비순차적 실행
앞서 데이터 해저드에 대해 설명했다. 이는 명령어끼리의 의존성 때문인데, 그럼 의존성이 없는 명령어의 순서를 바꾸면 되지 않을까? 라는 생각을 할 수 있다. 이 생각이 바로 비순차적 실행(Out-of-Order Execution, OoOE)이라고 한다. 아래의 예시 의사코드를 보자.
1
2
3
4
1 M1 = 1 + 2
2 M2 = M1 + 3
3 M3 = 3 + 4
4 M4 = M3 + 5
이는 아래와 같이 표현될 수 있다.
t₁ | t₂ | t₃ | t₄ | t₅ | t₆ | t₇ | t₈ | t₉ | t₁₀ | |
---|---|---|---|---|---|---|---|---|---|---|
I1 | Fetch | Decode | Execute | Store | ||||||
I2 | Fetch | Decode | Execute | Store | ||||||
I3 | Fetch | Decode | Execute | Store | ||||||
I4 | Fetch | Decode | Execute | Store |
I2
는 I1
이 처리된 후 실행될 수 있고, 그 이후 I3
가 처리되기 때문에 그 중간은 비어있게 된다. 그럼 아래와 같이 바꾸면 어떨까?
1
2
3
4
1 M1 = 1 + 2
3 M3 = 3 + 4
4 M4 = M3 + 5
2 M2 = M1 + 3
t₁ | t₂ | t₃ | t₄ | t₅ | t₆ | t₇ | t₈ | t₉ | t₁₀ | |
---|---|---|---|---|---|---|---|---|---|---|
I1 | Fetch | Decode | Execute | Store | ||||||
I3 | Fetch | Decode | Execute | Store | ||||||
I4 | Fetch | Decode | Execute | Store | ||||||
I2 | Fetch | Decode | Execute | Store |
이렇게 순서를 바꾸도 결과엔 문제가 없다. I1 I2
와 I3 I4
는 서로 의존성이 없고, I1
과 I2
사이에 들어와도 작동엔 문제가 없기 때문에 순서를 바꾸는것이 더욱 효율적이다.
2-1-8. 명령어 집합 구조
CPU마다 명령어의 종류와 그 사용법, 구조가 제각각 다르고, CPU가 이해할 수 있는 명령어들의 모음을 명령어 집합(Instruction Set)이라고 한다. 이렇게 CPU마다 명령어가 다르면 그에 따른 레지스터와 명령어의 처리 과정 등이 모두 달라진다.
이러한 CPU의 구조를 CPU 아키텍쳐(CPU Architecture)라고 하며, 여기엔 x86-64, ARM 등의 여러 아키텍쳐가 존재한다. 각 아키텍쳐마다 명령어와 구조가 다르기 때문에 서로 다른 어셈블리어가 존재하며, 컴파일 언어에서 컴파일 시 해당 환경의 아키텍쳐에 맞춰 컴파일된다.
명령어 집합 구조(Instruction Set Architecture, ISA)17엔 크게 2가지가 있는데, CISC(Complex Instruction Set Computer)와 RISC(Reduced Instruction Set Computer)로 나눠진다.
2-1-8-1. CISC
CISC는 Complex Instruction Set Computer의 약자로, 직역하면 “복잡 명령어 집합 컴퓨터” 라고 해석할 수 있다. 뜻 그대로 복잡한 형태의 명령어를 가지며, 최초의 명령어 집합 구조이다. 인텔의 x86-64가 CISC이며18, 명령어의 종류가 다양하고 복잡하다.
한가지 특징은 명령어의 크기가 바뀔 수 있는 가변 길이 명령어를 사용한다는 점이다19. 간단하게 말하면 명령어마다 길이가 제가각이라는 뜻이다.
이는 하나의 명령어로 다양한 처리가 가능하며, 명령어를 작성할 때 더욱 간결하고 적은 명령어로도 작성이 가능하다는 얘기이다.
하지만 이는 해석하는데 비효율적이며, 파이프라이닝에 대해 상대적으로 효율적이지 않다는 단점이 있다. 명령어가 적기때문에 호환성이 좋기도 하지만 속도가 느리고, 오히려 더욱 많은 클럭을 사용할 수 있다는 의미이다.
2-1-8-2. RISC
CISC의 단점을 보완하기 위해 RISC(Reduced Instruction Set Computer)가 등장하였다. 직역하면 “축소 명령어 집합 컴퓨터”로 CISC와는 다르게 명령어가 적고20, 단순하며 고정 길이 명령어를 사용한다.
때문에 프로그램을 작성할 때 비교적 많은 명령어가 사용되지만, 하나의 클럭(사이클)에 하나의 명령어를 처리하기 때문에 CISC보다 효율적이다. 또한 파이프라이닝에 대해서도 더욱 용의하다.
명령어 호환성이 떨어진다는 단점이 있으나, 비교적 전력 소모가 적고21 위와 같이 효율적이기 때문에 현대의 명령어 집합 구조에서 주로 사용되는 구조이다.
ARM 등이 RISC에 해당되며, 임베디드 시스템에 사용되는 프로세서도 대부분 RISC이다.
2-2. 메모리(주기억장치)
여태 CPU에 대한 내용을 다뤘다. 사실 CPU에 대한 동작 과정과 구조만 알고있어도 컴퓨터를 다루는데 무리가 전혀 없다. 하지만 컴퓨터는 CPU로만 이루어져있진 않다. 예컨대 CPU 외에 메모리, 입출력장치 등도 있어야 컴퓨터가 동작한다.
이번 파트에선 그중 하나인 “메모리”에 대해 다뤄볼 예정으로, 크게 RAM과 ROM으로 구분하여 설명할 예정이다.
2-2-1. RAM
우리가 앞서 “메모리”라고 하면 대부분 RAM을 의미한다고 설명하였다. ROM은 나중에 설명하고, RAM에 대해 먼저 설명하겠다.
CPU는 메모리로 부터 데이터를 가져와 명령어를 실행한다. 또한 데이터를 메모리에 저장하기도 한다.
사실 위 설명만으로도 RAM이 어떤 역할을 하는지 대략적으로 알 수 있다. 어떠한 프로그램이 실행되면 보조기억장치(HDD, SSD 등)에서 프로그램의 데이터22를 메모리로 가져와 CPU가 실행하게 된다.
만약 RAM의 크기가 상대적으로 작다면, RAM에 올라갈 수 있는 프로그램의 크기도 제한되므로 성능이 떨어지게 된다. 추가적으로 프로그램의 크기가 너무 커 올라가지 못할 경우, 프로그램의 실행되는 일부만 올라가고 나머지는 가상 메모리에 저장된다. 가상메모리는 보조기억장치를 RAM처럼 사용하는 기술인데, 추후 설명하겠다.
반대로 RAM의 크기가 상대적으로 크면 올라갈 수 있는 프로그램의 크기와 개수가 많아지므로 성능이 향상된다.
또한 RAM은 전원이 꺼지면 데이터가 사라지기 때문에 휘발성 메모리(Volatile Memory)이다.
2-2-1-1. RAM의 종류
RAM엔 여러 종류가 있는데, 크게 DRAM(Dynamic RAM)과 SRAM(Static RAM)으로 나눌 수 있다.
DRAM(Dynamic RAM)
흔히 말하는 “RAM”은 대부분 DRAM을 의미한다. DRAM은 동적 램이라는 뜻으로, 데이터를 저장하는 방식이 동적이라는 뜻이다. 이게 무슨말이냐, 쉽게 말해 일정한 주기마다 데이터를 갱신(refresh)해야 한다는 뜻으로, 갱신을 하지 않으면 데이터가 사라진다.
근데 왜 DRAM을 쓰냐, IT의 세계에선 그 무엇보다 중요한건 비용이다. 아무리 좋아도 비용이 비싸면 그만큼 안쓰이기 마련인데, DRAM은 비용이 저렴하고 상대적으로 소비전력도 낮으며, 밀도가 높아 용량이 크다는 장점이 있다.
추가적으로 SDRAM(Synchronous DRAM), DDR SDRAM(Double Data Rate Synchronous DRAM) 등의 DRAM이 존재하며, 이는 DRAM의 성능을 향상시킨 버전이다.
SDRAM은 동기 DRAM이라는 뜻으로, CPU와 메모리의 속도를 맞추어 동작한다는 뜻이다. 즉 CPU 클럭과 메모리 클럭이 동일하게 동작한다.
DDR SDRAM의 Double Data Rate는 데이터 전송 속도를 늘렸다는 의미다. 다른말로 설명하면 대역폭을 늘렸다는 의미인데, 그럼 한번에 전송되는 데이터의 양이 늘어나므로 성능이 향상된다. 오늘날 대부분의 컴퓨터는 DDR SDRAM을 사용하며, DDR2, DDR3, DDR4, DDR5 등의 버전이 존재한다. 뒤의 숫자는 2^n의 의미로, DDR4는 16개의 대역폭을 가지고 있다.
SRAM(Static RAM)
Static은 정적이라는 뜻으로, 데이터를 저장하는 방식이 정적이라는 뜻이다. 컴퓨터에 전원이 인가되어있는 동안 데이터가 유지되며, DRAM과는 다르게 주기적인 갱신이 필요없다. 또한 대부분 DRAM보다 속도가 빠르나, 비싸고 소비전력이 높다는 단점이 있다.
또한 밀도가 낮아 용량이 작다는 단점이 있으나, 속도가 빨라 캐시 메모리에 사용되는 경우가 많다.
2-2-2. ROM
ROM은 Read-Only Memory의 약자로, 읽기 전용 메모리라는 뜻이다. 즉, 데이터를 읽기만 가능하고 쓰기가 불가능하다. 사실 불가능한건 아니며, 쓰기가 가능한 ROM도 존재한다. 이를 PROM(Programmable ROM)이라 하는데 곧 설명하겠다.
ROM은 이름 뜻 그대로 첫 프로그래밍 이후 데이터를 다시 쓸 필요가 없는 부트로더, 펌웨어 등에 사용된다. 물론 현재에 와선 펌웨어 역시 플래시 메모리에 저장되는 경우도 있다. 플래시 메모리는 추후 설명하겠다. 크게 마스크 ROM(Mask ROM)과 PROM(Programmable ROM)으로, PROM의 하위로 EPROM(Erasable Programmable ROM)과 EEPROM(Electrically Erasable Programmable ROM) 등이 존재한다.
추가적으로 ROM은 전원이 꺼져도 데이터가 유지되는 비휘발성 메모리(Non-Volatile Memory)이다.
2-2-2-1. ROM의 종류
ROM은 원래 Read Only, 즉 읽기 전용이였으나 오늘날엔 다시 쓸 수 있게 만들어져가고 있다. 초기의 진짜 Read Only의 ROM을 마스크 ROM이라 한다.
Mask ROM(마스크 ROM)
마스크 ROM은 처음에 데이터를 쓰고 나면 다시 쓸 수 없는 ROM이다. 공장에서 만들어질때부터 데이터가 적히고, 이는 변경할 수 없기 때문에 오늘날엔 거의 쓰이지 않고 있다.
PROM(Programmable ROM)
PROM은 처음에 데이터를 쓸 수 있지만, 한번 쓰면 다시 쓸 수 없는 ROM이다. 이는 사용자가 데이터를 쓸 수 있게 만들어져서 Programmable이라는 이름이 붙었다. 마스크 ROM은 공장에서 부터 들어갈 내용을 패턴화하여 써넣는 반면, PROM은 아무것도 없는 상태로 사용자에게 공급되어 사용자가 직접 데이터를 쓸 수 있다.
하지만 한번 쓴 이후엔 마스크 ROM과 마찬가지로 다시 쓸 수 없다는 단점이 있다.
EPROM(Erasable Programmable ROM)
EPROM은 PROM과 다르게 데이터를 지울 수 있는 ROM이다. 칩셋에 동그란 창이 있는데, 그 위로 자외선을 비추면 데이터가 지워진다. 그 이후 PROM Writer를 이용해 다시 데이터를 쓸 수 있다. 제조사 등에서 버그 수정 등을 위해 다시 프로그래밍 시 유용하다.
EEPROM(Electrically Erasable Programmable ROM)
EEPROM은 EPROM과 다르게 자외선이 아닌 전기적인 방법으로 데이터를 지울 수 있는 ROM이다. 이는 EPROM보다 훨씬 편리하며, 현재의 플래시 메모리의 원형이 된다. 전기적으로 쓰고 지우기 때문에 자외선이나 PROM Writer가 필요없으며, 컴퓨터의 BIOS 등에 사용된다. 자외선을 사용하여 지우는 UVEPROM에 비해 EEPROM은 훨씬 편리하지만, 가격히 훨씬 비싸며 쓰기/지우기 속도가 느리다.
플래시 메모리(Flash Memory)
플래시 메모리는 EEPROM의 일종으로, EEPROM보다 더욱 편리하고 빠르며, 대용량의 저장이 가능하다. 굳이 따지자면 RAM과 ROM의 중간쯤에 위치하는데, ROM과 같이 비휘발성인데 반해 RAM과 같이 쓰기/읽기 속도가 빠르다.
물론 RAM에 비교하면 월등히 느려 RAM 대용으론 부적합하고, 쓰기에 제한이 있어 몇십만~몇백만번 정도의 쓰기 이후엔 데이터를 더이상 쓸 수 없다.
플래시 메모리는 주로 USB 메모리, SSD 등에 사용되며, 최근엔 컴퓨터의 BIOS에도 사용된다. 덕분에 사용자가 쉽게 BIOS를 업데이트 할 수 있게 되었다.
2-2-3. 캐시 메모리
먼저, CPU와 메모리(RAM) 사이에는 속도 차이가 존재한다. CPU가 메모리에 접근하는 CPU의 연산 속도보다 느리다. 때문에 메모리 버퍼 레지스터 같은 버퍼를 활용하여 속도 차이를 줄이는 방법이 있기도 한데, 다른 방법으론 캐시 메모리(Cache Memory)를 사용하는 방법이 있다.
앞서 설명했지만 일반적으로 메모리(RAM)은 DRAM을 사용한다. 이는 속도가 느리다는 단점이 있지만, 대용량의 저장이 가능하다는 장점이 있다. 캐시 메모리는 SRAM을 사용하는데, SRAM은 용량이 작고 가격이 비싸지만 속도가 빠르다는 장점이 있다.
이처럼 CPU로 부터 가까운 레지스터 -> 캐시 메모리 -> 메모리 -> 보조기억장치 순으로 접근 속도가 느려지는데, 이를 메모리 계층 구조(Memory Hierarchy)라고 한다.
레지스터는 크기도 워낙 작고, RAM과 같은 역할로 쓰기엔 적절하지 않기 때문에 메모리(RAM)과 레지스터 사이에 캐시 메모리를 두어 속도 차이를 줄이는것이다.
캐시(Cache)는 사전적으로 “데이터나 값을 미리 저장해두는 임시 장소”라는 의미를 가지고 있다. 간혹 크롬 등의 웹브라우저에서 캐시를 지우라는 말을 들어본적이 있을텐데, 여기서 나오는 캐시도 웹페이지의 데이터를 미리 저장해두는 임시 장소라는 의미이다.
CPU가 메모리(RAM)에 자주 접근하면 그만큼 성능이 저하되나, 자주 접근하는 데이터를 캐시 메모리에 저장해두면 CPU가 빠르게 접근할 수 있어 성능이 향상된다. 또한 CPU가 다음에 사용할 데이터를 예측하여 캐시 메모리에 저장하기도 한다.
물리적인 CPU 구조로 보면 캐시도 계층적으로 구성되어있는데, 레벨로 구분하여 크게 L1 캐시, L2 캐시, L3 캐시로 나눌 수 있다. 일반적으로 L1 캐시가 가장 빠르고 용량이 작고, L3 캐시가 가장 느리고 용량이 크다.
2-2-3-1. 지역성
캐시 메모리는 RAM보다 크기가 훨씬 작다. 때문에 캐시 메모리에 어떤 데이터를 저장하는게 효율적인지를 판단해야 하는데, 이때 지역성(Loacality)이라는 개념이 중요하다.
지역성은 데이터 접근이 시간적, 혹은 공간적으로 가깝게 발생하는걸 의미한다. 예를 들어 12번지의 명령어를 실행한다면 13번지의 명령어를 실행할 확률이 높다는 것도 지역성의 일종이다. (공간 지역성)
지역성에는 크게 시간 지역성(Temporal Locality)과 공간 지역성(Spatial Locality)이 있다.
- 시간 지역성: 어느 메모리의 데이터를 접근했을 때, 가까운 시간 내에 다시 접근할 확률이 높다.
- 공간 지역성: 특정 메모리의 데이터를 접근했을 때, 그 주변 메모리의 데이터를 접근할 확률이 높다.
만약 예측하여 캐시 메모리에 저장한 데이터가 CPU에 의해 사용된다면, 즉 예측이 맞다면 이를 캐시 히트(Cache Hit)라고 하며, 그 반대인 예측에 실패한 경우를 캐시 미스(Cache Miss)라고 한다. 캐시 미스가 많을수록 오히려 성능이 떨어지게 되므로, 이를 줄이는것이 성능 향상에 중요하다.
캐시 적중(예측) 횟수를 전체 메모리 접근 수로 나눈 비율을 캐시 적중률(Cache Hit Rate)이라고 한다.
$\text{캐시 적중률(%)} = \left( \frac{\text{캐시 히트 수}}{\text{전체 메모리 접근 수}} \right) \times 100$
2-2-4. 논리 주소와 물리 주소
앞서 메모리에 접근한다 하면 컴퓨터 내에 하나의 RAM에 접근하는것이라고 설명했다. 하지만 컴퓨터는 두가지의 주소 체계를 사용하는데, 바로 논리 주소(Logical Address)와 물리 주소(Physical Address)이다.
먼저 물리 주소는 물리적인 RAM의 주소를 의미한다. 즉, RAM의 실제 주소를 의미한다. 여태 “12번지의 데이터”라고 했을때, 이 12번지가 바로 RAM의 물리주소를 의미했었다.
하지만 프로그램 내에선 논리 주소를 사용하는데, 논리 주소는 CPU와 프로그램 내에서 사용되는 주소 체계다. 실행되는 프로그램엔 각각 0번지 부터 시작하는 주소가 할당된다. 반면 물리 주소는 이러한 구분 없이 0번지부터 시작한다.
때문에 어떤 프로그램 내에서 “12번지” 라고 하면 이는 논리 주소를 의미하며, 물리 주소에선 다른 주소일 수도 있다.
이렇게 논리 주소를 물리 주소로 변환해야 하는데, 이를 변환하는 장치를 메모리 관리 장치(Memory Management Unit, MMU)라고 한다. 간단하게 설명하면 논리 주소 “12번지”를 물리 주소로 변환하려면 물리 주소에서의 프로그램의 시작점을 더한다.
예를 들어 어떠한 프로그램이 물리 주소 100번지부터 시작한다고 하면, 이 프로그램의 논리 주소 12번지는 물리 주소 112번지가 된다.23
논리 주소와 물리 주소로 나누는 이유는 여러가지가 있는데, 가장 큰 이유는 프로그램의 주소 변동과 메모리 보호 때문이다.
프로그램이 실행될 때마다 매번 메모리 주소가 변동하는데, 이때 논리 주소를 사용하면 프로그램의 주소 변동에 따라 물리 주소를 MMU를 통해 변환해주기만 하면 되기 때문에 편리하다.
또한 논리 주소를 사용하면 메모리 보호도 쉽게 가능하다. 예를 들어, 프로그램 A는 물리 주소 100번지 ~ 150번지를 사용하고, 프로그램 B는 200번지 ~ 250번지를 사용한다고 하자.
만약 프로그램 A가 70번지 데이터를 사용하려고 한다면, 이는 논리 주소이기 때문에 MMU를 통해 물리 주소로 변환되어 물리 주소 170번지로 변환된다. 하지만 프로그램 A는 물리 주소 150번지까지만 사용하므로, 170번지에 접근하려고 하면 Segmentation Fault 에러가 발생한다.
이처럼 논리 주소를 사용하여 메모리 보호를 할 수 있으며, 프로그램의 주소 변동에도 유연하게 대처할 수 있다.
2-3. 보조기억장치
2-4. 입출력장치(I/O)
2-5. 버스
일반적으로 “메모리”라 하면 주기억장치(RAM)을 의미한다. ↩
프로그램의 모든 명령어와 데이터가 메모리에 올라가는건 아니다. ↩
메모리뿐만 아니라 레지스터, 캐시 메모리에서 접근하기도 한다. ↩
아키텍쳐란 컴퓨터 시스템의 하드웨어 구조를 의미한다. x86-64, ARM 등의 다양한 아키텍쳐가 존재한다. ↩
소스 코드(Source Code)는 소프트웨어 제작에 사용되는 코드로, 다른 말로 원시 코드라고도 한다. ↩
니모닉(mnemonic) 기호라고 부른다. ↩
이해하지 않아도 좋다. 어셈블리어가 저렇게 생겼다는 것만 알면 된다. ↩
자세히 들어가면 “그대로” 복사하는건 아니다. 최적화 등의 과정으로 내용이 바뀔 수 있다. ↩
다만 인터프리트 과정에서 추가적인 메모리 소비(오버헤드)가 발생하기 때문에 이 점을 고려해야 한다. ↩
추후 설명할 예정이지만, “메모리”라 칭하는건 대부분 주기억장치(RAM)을 의미한다. ↩
여기서 말한 “메모리”는 주기억장치(RAM)을 의미한다. ↩
단위는 Hz(헤르츠)를 사용하며, 1초에 몇번 반복되는가 나타낸다. 현재 존재하는 CPU의 경우 GHz 단위가 대부분이며, 3GHz의 경우 1초에 클럭 신호가 30억번을 반복된다는 의미이다. ↩
프로그램과 프로세스는 다른 개념이다. 프로그램은 어떠한 명령어들의 집합을 의미하고, 프로세스는 그 프로그램이 실행되고 있는 상태의 프로그램을 의미한다. ↩
명령어를 처리하기 위해 필요한 레지스터들의 집합을 레지스터 세트라 부른다. ↩
이를 명령어 의존성이라 한다. ↩
분기란 명령어 실행이 순차적으로 실행되던 도중, 어느 명령어(JUMP 등)가 실행되어 다른 위치(포인터)로 이동되는것을 의미한다. ↩
명령어 집합 구조(ISA)와 CPU 아키텍쳐는 비슷하지만 약간의 차이가 존재한다. 전자는 CPU가 처리할 수 있는 명령어들의 구조와 레지스터, 주소 지정 방식, 데이터 형식 등의 명령어 처리에 중점을 둔것에 비해, 후자는 CPU의 하드웨어적인 구조 및 설계에 가깝다. ↩
x86-64에선 CISC 형식의 명령어를 처리하지만, 사실 현재의 x86-64는 내부적으로 RISC 처럼 처리된다. 즉 CISC와 RISC가 혼용되어 사용되며, 최근에 들어선 CISC는 거의 사용되지 않고 있다. ↩
RISC에서도 가변 길이 명령어를 사용하기도 하기 때문에, CISC에서만 가변 길이 명령어를 사용한다는 특징도 사라지고 있는 추세이다. ↩
실제론 RISC의 명령어가 더 적거나 하진 않을 수 있다. 하지만 주소 지정 방식이 CISC보다 단순하여 명령어 처리에 효율적이다. ↩
실제로 전력 소모면에선 임베디드를 제외하면 큰 차이가 없긴 하다. ↩
프로그램의 명령어들과 프로그램이 가지고있는 데이터를 의미한다. ↩
프로그램의 시작점의 물리 주소를 가지고 있는 레지스터가 베이스 레지스터이다. ↩