게임 개발이나 일반적인 프로그래밍을 하다 보면 어느 순간 이런 질문을 만나게 된다.
왜 이 코드는 빠르고 저 코드는 느린가?
왜 메모리를 많이 사용하는가?
CPU와 GPU는 정확히 무슨 차이가 있는가?
캐시가 왜 필요한가?
코드만 보고 있으면 이런 문제의 원인을 찾기 어려울 때가 있다.
결국 프로그램은 CPU에서 실행되고, 데이터는 메모리에 저장되며, CPU와 메모리 사이에서 데이터를 계속 주고받는다.
컴퓨터 구조를 공부하는 이유는 이 실행 과정을 이해하기 위해서다.
컴퓨터는 크게 어떻게 구성되어 있는가
프로그램이 실행되는 과정을 단순하게 보면 다음과 같다.
프로그램-> 저장장치-> 메모리-> CPU-> 실행
여기에 입력과 출력, GPU 같은 장치들이 연결된다.
조금 더 구조적으로 보면

각 부품이 완전히 독립적으로 동작하는 것이 아니라 서로 데이터를 주고받으면서 프로그램을 실행한다.
CPU
CPU는 프로그램의 명령어를 읽고 실행하는 장치다.
코드가 작성되어 있다고 해서 CPU가 C#이나 C++ 코드를 직접 이해하는 것은 아니다.
소스 코드는 컴파일 과정을 거쳐 CPU가 실행할 수 있는 기계어 명령어로 변환된다.
실행 과정은 단순화하면 다음과 같다.
명령어 가져오기-> 명령어 해석-> 필요한 데이터 가져오기-> 연산-> 결과 저장-> 다음 명령어
이 과정을 반복하면서 프로그램이 실행된다.
CPU 내부에서도 여러 구성요소가 역할을 나눈다.
ALU
ALU는 Arithmetic Logic Unit의 약자로 산술 연산과 논리 연산을 담당한다.
예를 들어
10 + 20
10 > 5
A == B
A AND B
같은 연산이 여기에 해당한다.
실제 CPU는 단순히 덧셈만 하는 것이 아니라 다양한 연산을 수행하지만, 기본적인 역할은 데이터를 받아 연산 결과를 만드는 것이다.
Register
Register는 CPU 내부에 있는 아주 작은 저장 공간이다.
CPU가 현재 계산에 사용할 값을 빠르게 보관하는 데 사용한다.
예를 들어 어떤 계산을 한다면
Memory-> Register-> ALU-> Register
같은 흐름으로 생각할 수 있다.
Register는 매우 빠르지만 용량이 극히 작다.
그래서 모든 데이터를 Register에 저장할 수는 없다.
이 지점에서 CPU Cache와 RAM이 등장한다.
메모리 계층
컴퓨터의 저장 공간은 하나의 종류로 되어 있지 않다.
대략 다음과 같은 계층으로 생각할 수 있다.

일반적으로 위로 갈수록 빠르고 용량이 작으며, 아래로 갈수록 느리고 용량이 크다.
여기서 중요한 것은 CPU가 모든 데이터를 같은 속도로 접근할 수 있는 것이 아니라는 것이다.
CPU 입장에서는 계산 자체보다 데이터를 가져오는 과정이 병목이 될 수도 있다.
RAM
RAM은 프로그램이 실행되는 동안 사용하는 주기억장치다.
예를 들어 게임을 실행하면 게임 코드와 필요한 데이터가 저장장치에서 RAM으로 올라온다.
SSD-> RAM-> CPU
CPU가 필요한 데이터를 RAM에서 가져와 처리한다.
문제는 CPU의 발전 속도에 비해 메모리 접근 속도가 상대적으로 느리다는 것이다.
CPU가 아무리 빠르게 계산할 수 있어도 데이터를 기다리는 시간이 길어지면 성능이 제대로 나오지 않는다.
그래서 CPU 내부에 Cache가 들어간다.
CPU Cache
Cache는 CPU가 자주 사용하는 데이터를 가까운 곳에 임시로 저장하는 메모리다.
대표적으로
L1 Cache
L2 Cache
L3 Cache
가 있다.
일반적으로 L1이 가장 작고 빠르며, L3로 갈수록 용량은 커지고 접근 비용도 증가한다.
개념적으로 보면

형태다.
CPU가 데이터를 요청했을 때 Cache에 원하는 데이터가 있으면 Cache Hit가 발생한다.
반대로 없으면 Cache Miss가 발생하고 더 느린 메모리 계층에서 데이터를 가져와야 한다.
Cache가 게임 개발에서 중요한 이유
게임은 매 프레임 많은 데이터를 처리한다.
예를 들어 수천 개의 Entity를 순회한다고 하자.
foreach (var enemy in enemies)
{
enemy.Update();
}
단순히 코드만 보면 별 문제가 없어 보인다.
하지만 실제 CPU에서는 enemy 데이터가 메모리 여기저기에 흩어져 있다면 데이터를 가져오는 과정에서 Cache Miss가 많이 발생할 수 있다.
반대로 필요한 데이터를 연속적으로 배치하면 CPU가 한 번에 주변 데이터를 가져오기 때문에 Cache 효율이 좋아질 수 있다.
이런 이유 때문에 게임 엔진이나 ECS(Entity Component System) 같은 구조에서 데이터의 배치와 접근 패턴을 중요하게 다룬다.
자료구조에서 이야기했던 성능 문제가 컴퓨터 구조와 연결되는 부분이다.
Locality
Cache가 효과적으로 작동하려면 프로그램이 데이터를 일정한 패턴으로 접근하는 경향이 있어야 한다.
대표적으로 두 가지가 있다.
Temporal Locality
최근에 사용한 데이터를 다시 사용할 가능성이 높다는 개념이다.
A 사용
-> B 사용
-> A 다시 사용
같은 경우다.
Spatial Locality
현재 사용한 데이터 주변의 데이터도 곧 사용할 가능성이 높다는 개념이다.
Array
[A][B][C][D][E]
^
현재 접근
A를 사용했다면 가까운 위치의 B, C도 사용할 가능성이 높다고 보는 것이다.
그래서 배열처럼 데이터가 연속적으로 배치된 구조가 Cache 친화적인 경우가 많다.
CPU의 명령어 실행
CPU는 프로그램의 명령어를 순서대로 처리한다.
아주 단순화하면
Fetch-> Decode-> Execute
과정으로 볼 수 있다.
Fetch
실행할 명령어를 가져온다.
Decode
가져온 명령어가 무엇을 의미하는지 해석한다.
Execute
실제 연산을 수행한다.
현대 CPU에서는 실제 실행 과정이 이것보다 훨씬 복잡하다.
여러 명령어를 동시에 처리하기 위해 Pipeline, Out-of-Order Execution, Branch Prediction 같은 기술을 사용한다.
Pipeline
CPU가 명령어 하나를 완전히 끝낸 뒤 다음 명령어를 시작한다면 일부 하드웨어가 놀고 있을 수 있다.
그래서 명령어 처리 과정을 여러 단계로 나누고 서로 겹쳐서 실행한다.
개념적으로

처럼 동작할 수 있다.
공장의 생산 라인과 비슷한 구조다.
Pipeline이 있다고 해서 하나의 명령어가 무조건 더 빨리 끝나는 것은 아니다.
여러 명령어를 겹쳐 처리해서 전체적인 처리량을 높이는 것이 핵심이다.
Branch Prediction
CPU 코드에는 조건문이 많다.
if (player.IsAlive)
{
UpdatePlayer();
}
else
{
RemovePlayer();
}
CPU 입장에서는 다음에 어떤 명령어를 실행해야 할지 조건을 확인하기 전까지 확정하기 어렵다.
그래서 CPU는 과거 실행 패턴 등을 바탕으로 어느 쪽으로 분기할지 예측한다.
예측이 맞으면 Pipeline을 계속 효율적으로 사용할 수 있다.
틀리면 이미 가져온 명령어를 버리고 올바른 경로를 다시 처리해야 한다.
이것을 Branch Misprediction이라고 한다.
게임 코드에서 조건문을 무조건 없애야 한다는 의미는 아니다.
다만 아주 빈번하게 실행되는 코드에서는 분기 패턴과 데이터 접근 패턴도 성능에 영향을 줄 수 있다.
CPU와 GPU
게임 개발에서는 CPU와 GPU의 역할을 구분하는 것이 중요하다.
CPU는 다양한 종류의 작업을 순차적으로 빠르게 처리하는 데 강하다.
게임 로직
AI
물리 계산
입력 처리
시스템 관리
GPU는 대량의 데이터를 병렬적으로 처리하는 데 강하다.
Vertex Processing
Pixel Processing
Shader
Lighting
Post Processing
개념적으로 보면
CPU-> 적은 수의 강력한 Core-> 복잡한 작업 처리
GPU-> 매우 많은 병렬 처리 자원-> 대량의 유사한 작업 처리
그래서 게임에서는 CPU와 GPU가 각각 다른 역할을 맡는다.
CPU-> Game Logic-> Physics-> AI-> Rendering Command
GPU-> 실제 그래픽 연산-> Rasterization-> Shader-> 화면 출력
CPU Bound와 GPU Bound
게임 성능을 분석할 때 자주 나오는 개념이다.
CPU에서 처리해야 하는 작업이 너무 많으면 CPU Bound 상태가 된다.
CPU 작업-> Game Logic-> Physics-> AI-> Rendering Preparation-> 병목
반대로 GPU가 화면을 렌더링하는 데 너무 많은 시간이 걸리면 GPU Bound가 된다.
GPU 작업-> Rendering-> Shader-> Lighting-> Post Processing-> 병목
따라서 FPS가 낮다고 무조건 GPU 문제라고 생각하면 안 된다.
어느 쪽이 병목인지 먼저 확인해야 한다.
저장장치
SSD나 HDD는 데이터를 장기간 저장하는 장치다.
RAM과 달리 전원이 꺼져도 데이터가 유지된다.
게임을 설치하면 게임 데이터가 SSD에 저장된다.
게임을 실행하면 필요한 데이터가 SSD에서 RAM으로 로드된다.
SSD-> RAM-> CPU / GPU
그래서 게임 로딩 시간에는 저장장치의 성능뿐 아니라 파일 구조, 압축 해제, 데이터 처리 등 여러 요소가 영향을 준다.
최근 게임에서는 Asset Streaming처럼 플레이 중 필요한 데이터를 저장장치에서 조금씩 가져오는 방식도 사용한다.
Virtual Memory
프로그램은 실제 RAM보다 더 큰 메모리 공간을 사용하는 것처럼 동작할 수 있다.
운영체제는 프로세스마다 가상 주소 공간을 제공하고 실제 물리 메모리와 연결해서 관리한다.
개념적으로 보면
Program-> Virtual Address-> Page Table-> Physical Memory
형태다.
이 덕분에 각 프로세스가 자신의 독립된 메모리 공간을 사용하는 것처럼 만들 수 있다.
또한 RAM이 부족하면 일부 데이터를 저장장치로 옮기는 방식도 사용할 수 있다.
다만 SSD는 RAM보다 훨씬 느리기 때문에 메모리 부족으로 Paging이 과도하게 발생하면 성능이 크게 떨어질 수 있다.
Process와 Memory
프로그램 파일과 실행 중인 프로그램은 구분해야 한다.
디스크에 있는 실행 파일은 단순히 저장된 데이터다.
그것을 실행하면 운영체제가 필요한 정보를 메모리에 배치하고 CPU가 실행할 수 있는 Process가 만들어진다.
프로세스의 메모리는 일반적으로 다음과 같은 영역으로 구분해서 생각할 수 있다.
Code
Data
Heap
Stack
Code
실행할 프로그램의 명령어가 들어간다.
Data
전역 변수나 정적 데이터 등이 들어간다.
Heap
동적으로 할당되는 데이터를 관리한다.
Stack
함수 호출과 지역 변수 등의 관리에 사용된다.
실제 운영체제와 언어에 따라 세부 구조는 달라질 수 있지만 기본적인 개념을 잡을 때 유용하다.
Stack과 Heap
프로그래밍에서 Stack과 Heap을 자주 들어보게 되는 이유도 컴퓨터 구조와 관련이 있다.
함수를 호출하면 호출 정보와 지역 변수 등을 Stack을 통해 관리할 수 있다.
Main()-> Update() -> Move()
함수 호출이 끝나면 해당 호출에 대한 Stack 영역이 정리된다.
반면 Heap은 동적으로 데이터를 할당하는 데 사용된다.
C/C++에서는 직접 메모리를 할당하고 해제할 수 있고, C# 같은 언어에서는 Garbage Collector가 Heap에 있는 객체의 수명을 관리한다.
따라서
Stack-> 함수 호출 중심
Heap-> 동적 데이터 관리
정도로 이해하면 기본 구조를 잡는 데 충분하다.
메모리 주소
메모리는 결국 데이터를 저장하는 공간이고 각 위치에는 주소가 있다.
개념적으로

처럼 생각할 수 있다.
C/C++에서는 포인터를 통해 이런 메모리 주소를 직접 다룰 수 있다.
int value = 10;
int* ptr = &value;
여기서 ptr은 value가 저장된 메모리 주소를 가지고 있다.
이 개념을 이해하면 Reference, Pointer, Memory Allocation 같은 내용을 이해하기 쉬워진다.
운영체제와 컴퓨터 구조
컴퓨터 구조를 공부하다 보면 운영체제와 자연스럽게 연결된다.
하드웨어를 프로그램이 직접 제어하는 것이 아니라 운영체제가 중간에서 자원을 관리한다.
Application-> Operating System-> Hardware
예를 들어 프로그램이 파일을 읽거나 메모리를 할당하거나 Thread를 생성할 때 운영체제의 기능을 사용한다.
게임 개발에서도
Game-> Engine-> Operating System-> CPU / GPU / Memory / Storage
처럼 여러 계층이 연결되어 있다.
그래서 성능 문제를 제대로 이해하려면 코드만 보는 것으로 끝나지 않는다.
컴퓨터 구조를 왜 알아야 하는가
일반적인 애플리케이션 개발에서는 컴퓨터 구조를 깊게 몰라도 프로그램을 만들 수 있다.
하지만 성능을 다루기 시작하면 이야기가 달라진다.
예를 들어
foreach (var entity in entities)
{
Update(entity);
}
라는 코드가 있다고 하자.
문법적으로는 문제가 없다.
하지만 실제 성능을 보려면
데이터가 메모리에 어떻게 배치되어 있는가?
CPU Cache에 잘 들어가는가?
몇 개의 Entity를 처리하는가?
얼마나 자주 실행되는가?
Branch가 많은가?
CPU Bound인가?
같은 질문을 해야 한다.
결국 소스 코드 아래에는 항상 하드웨어의 실행 과정이 있다.
게임 개발에서 연결되는 개념
지금까지의 내용을 게임 개발 관점에서 연결하면 꽤 명확해진다.
자료구조-> 데이터를 어떻게 배치할 것인가
알고리즘-> 데이터를 어떻게 처리할 것인가
컴퓨터 구조-> 실제 하드웨어가 그것을 어떻게 처리하는가
예를 들어 적 AI를 처리한다고 하자.
Enemy Data-> Array / List-> A*-> CPU-> Cache-> Memory
자료구조는 데이터의 형태를 결정하고, 알고리즘은 데이터를 처리하는 방법을 결정한다.
그리고 실제 성능은 CPU와 메모리 시스템에서 결정된다.
이 세 가지를 같이 보면 최적화가 훨씬 구체적으로 보인다.
정리
컴퓨터 구조를 한 문장으로 정리하면 프로그램이 하드웨어 위에서 어떻게 실행되는지를 이해하는 것에 가깝다.
큰 흐름은 다음처럼 볼 수 있다.
Program-> Memory-> CPU-> Instruction Execution-> Result
CPU 내부에서는
Register
-> Cache
-> ALU
-> Control Unit
등이 함께 동작하고, CPU는 RAM과 계속 데이터를 주고받는다.
여기서 메모리 접근이 느리기 때문에 Cache가 필요하고, Cache 효율을 높이기 위해 데이터의 접근 패턴이 중요해진다.
게임 개발에서는 이 개념이 자료구조와 알고리즘으로 바로 연결된다.
자료구조-> 데이터 배치와 접근 방식
알고리즘-> 데이터 처리 방법
컴퓨터 구조-> 실제 하드웨어에서 실행되는 과정
그래서 성능을 이해하려면 코드만 봐서는 부족하다.
Code-> Algorithm-> Data Structure-> Memory Access-> CPU / GPU-> Actual Performance
결국 프로그램의 성능은 “코드를 얼마나 짧게 작성했는가”보다 어떤 데이터를 어떤 방식으로 처리하고, 그 과정에서 하드웨어 자원을 어떻게 사용하는가에 더 가깝다.
컴퓨터 구조를 공부하면 코드 아래에서 실제로 무슨 일이 일어나고 있는지 볼 수 있게 된다.