컴퓨터 구조 01 - 컴퓨터 구조 개요
폰 노이만 아키텍처의 핵심 개념과 CPU, 메모리, 버스가 어떻게 협력하는지
왜 컴퓨터 구조를 알아야 하는가?
소프트웨어 개발을 하다 보면 하드웨어를 직접 다룰 일은 거의 없다. 고수준 언어로 코드를 작성하고, 운영체제가 하드웨어를 관리하며, 컴파일러가 기계어로 번역해 준다. 그렇다면 컴퓨터의 물리적 구조를 이해할 필요가 있는 것일까?
필요가 있다. 캐시 미스가 성능 병목이 되는 이유, 분기 예측 실패가 파이프라인을 멈추는 이유, 멀티스레드 프로그램에서 메모리 가시성 문제가 발생하는 이유를 이해하려면 하드웨어 수준의 지식이 필수적이다. 소프트웨어는 결국 하드웨어 위에서 실행되며, 하드웨어의 특성을 모르면 성능 문제의 근본 원인을 파악하기 어려운 것이다.
실제 개발에서도 이런 차이는 자주 드러난다. 같은 알고리즘인데 데이터 구조를 배열 중심으로 바꾸자 갑자기 빨라지는 경우, 단순한 조건문 재배치만으로 처리량이 좋아지는 경우, 락은 없는데도 멀티스레드 코드가 기대와 다르게 동작하는 경우가 모두 컴퓨터 구조와 연결되어 있다. 하드웨어를 모르면 이런 현상을 "운이 좋았다" 수준으로만 해석하게 된다.
폰 노이만 아키텍처
현대 컴퓨터의 대부분은 폰 노이만 아키텍처를 기반으로 한다. 1945년 존 폰 노이만이 제안한 이 구조의 핵심 아이디어는 프로그램과 데이터를 같은 메모리에 저장한다는 것이다.
이것이 왜 혁신적이었는가? 이전의 컴퓨터들은 프로그램이 하드웨어에 고정되어 있었다. 다른 계산을 하려면 물리적으로 배선을 변경해야 했다. 폰 노이만 아키텍처에서는 메모리에 저장된 프로그램을 교체하기만 하면 완전히 다른 작업을 수행할 수 있다. 범용 컴퓨터가 가능해진 것이다.
┌──────────────────────────────────────┐
│ CPU │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ 제어 유닛 │ │ ALU │ │
│ │ (CU) │ │ (산술논리연산장치) │ │
│ └──────────┘ └──────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ 레지스터 파일 │ │
│ └──────────────────────────────┘ │
└────────────┬─────────────────────────┘
│ 시스템 버스
┌────────────┴─────────────────────────┐
│ 메모리 (RAM) │
│ ┌────────────┐ ┌────────────────┐ │
│ │ 프로그램 │ │ 데이터 │ │
│ │ (명령어) │ │ │ │
│ └────────────┘ └────────────────┘ │
└──────────────────────────────────────┘
│
┌────────────┴─────────────────────────┐
│ 입출력 장치 (I/O) │
└──────────────────────────────────────┘
폰 노이만 아키텍처는 세 가지 핵심 구성 요소로 이루어져 있다. CPU가 명령어를 해석하고 실행하며, 메모리가 프로그램과 데이터를 저장하고, 버스가 이들 사이의 데이터 이동을 담당한다.
CPU의 기본 동작
CPU는 매우 단순한 사이클을 반복하여 동작한다. 이를 명령어 사이클(Instruction Cycle)이라고 부르며, 인출(Fetch), 해석(Decode), 실행(Execute)의 세 단계로 구성된다.
인출 단계에서 CPU는 프로그램 카운터(PC)가 가리키는 메모리 주소에서 다음 명령어를 가져온다. 해석 단계에서 제어 유닛이 이 명령어가 어떤 연산을 수행하라는 것인지 파악한다. 실행 단계에서 ALU나 다른 기능 유닛이 실제 연산을 수행하고 결과를 저장한다.
이 사이클이 초당 수십억 번 반복되는 것이 현대 CPU의 동작 원리이다. 클럭 속도가 3GHz인 프로세서는 이 사이클을 초당 약 30억 번 수행할 수 있다는 의미이다. 물론 실제로는 파이프라이닝, 슈퍼스칼라 실행 등의 기법으로 한 사이클에 여러 명령어를 동시에 처리하므로, 실제 처리량은 이보다 훨씬 높아지는 것이다.
여기서 중요한 것은 CPU가 "한 줄의 소스 코드"를 이해하는 것이 아니라, 아주 작은 기계 명령의 흐름을 처리한다는 점이다. 고수준 언어의 추상화와 실제 하드웨어 동작 사이에는 큰 간극이 있고, 이 시리즈는 그 간극을 메우는 데 초점을 둔다.
레지스터
레지스터는 CPU 내부에 위치한 가장 빠른 저장 공간이다. 메모리에 접근하는 데는 수십에서 수백 클럭 사이클이 소요되지만, 레지스터에 접근하는 데는 보통 1 클럭 사이클이면 충분하다.
레지스터의 종류는 아키텍처마다 다르지만, 공통적으로 존재하는 것들이 있다.
| 레지스터 | 역할 |
|---|---|
| 프로그램 카운터 (PC) | 다음에 실행할 명령어의 주소 |
| 스택 포인터 (SP) | 현재 스택의 꼭대기 주소 |
| 명령어 레지스터 (IR) | 현재 실행 중인 명령어 |
| 범용 레지스터 | 데이터 저장 및 연산에 사용 |
| 상태 레지스터 (FLAGS) | 연산 결과의 상태 (제로, 캐리, 오버플로우 등) |
레지스터의 수는 한정되어 있다. x86-64 아키텍처에서 범용 레지스터는 16개이며, ARM에서는 31개이다. 컴파일러의 중요한 역할 중 하나가 이 제한된 레지스터를 효율적으로 할당한다. 레지스터에 값을 유지하면 메모리 접근을 줄여 성능이 크게 향상되기 때문이다.
메모리와 버스
메모리는 바이트 단위로 주소가 부여된 거대한 배열이다. CPU가 메모리에 접근하려면 주소 버스에 접근할 주소를 올리고, 데이터 버스를 통해 데이터를 주고받으며, 제어 버스가 읽기인지 쓰기인지를 지정한다.
여기서 폰 노이만 아키텍처의 근본적인 한계가 드러난다. 명령어와 데이터가 같은 메모리에 있으므로, 명령어를 가져오는 것과 데이터를 읽거나 쓰는 것이 동일한 버스를 공유한다. 이를 폰 노이만 병목(Von Neumann Bottleneck)이라고 부른다. CPU의 처리 속도는 매우 빠르지만, 메모리와의 데이터 전송 속도가 이를 따라가지 못한다.
이 병목을 완화하기 위해 현대 프로세서는 캐시 메모리를 도입하고, 명령어용과 데이터용 캐시를 분리하며(하버드 아키텍처의 부분적 적용), 프리페치 기법을 사용한다. 이러한 최적화 기법들은 이후 포스트에서 하나씩 살펴볼 것이다.
바이트 순서
메모리에 다중 바이트 데이터를 저장하는 방식에도 선택이 필요하다. 0x12345678이라는 4바이트 정수를 메모리에 저장할 때, 상위 바이트(0x12)를 낮은 주소에 먼저 저장하는 방식을 빅 엔디안이라 하고, 하위 바이트(0x78)를 낮은 주소에 먼저 저장하는 방식을 리틀 엔디안이라 한다.
과연 어느 방식이 더 우수한가? 기술적으로는 어느 쪽이 절대적으로 더 좋다고 할 수 없다. 다만 현실에서는 대부분의 현대 프로세서(x86, ARM)가 리틀 엔디안을 사용하고, 네트워크 프로토콜은 빅 엔디안을 표준으로 채택하고 있다. 네트워크 프로그래밍에서 바이트 순서 변환이 필요한 이유가 바로 이것이다.
이 시리즈에서 다루는 것
이 시리즈는 CPU 내부 구조에서 시작하여 명령어 집합 아키텍처, 파이프라이닝, 권한 수준, 인터럽트, 메모리 계층 구조, 가상 메모리, I/O, 그리고 현대 멀티코어 프로세서까지를 다룬다. 각 주제가 소프트웨어 성능과 시스템 설계에 어떤 영향을 미치는지를 함께 살펴볼 것이다.
다음 글에서는 CPU의 내부 구조를 더 깊이 다룬다.