CPU와 메모리는 둘 중 하나만 골라 사용하는 자원이 아니다. 프로그램이 실행되는 동안 CPU는 명령을 처리하고, 메모리는 그 명령과 데이터를 보관한다. 성능 문제에서도 “CPU 작업인가, 메모리 작업인가”보다 어디에서 실행하고 어디에서 기다리는가를 묻는 편이 정확하다.
CPU는 실행하고 메모리는 보관한다
| 구분 | 주된 역할 | 성능을 좌우하는 값 |
|---|---|---|
| CPU | 명령어 실행과 제어 흐름 처리 | 실행 시간, 처리량, 스케줄링 지연 |
| 레지스터·캐시 | CPU가 곧 사용할 데이터 보관 | 적중률, 접근 지연, 재사용 패턴 |
| 주 메모리(RAM) | 실행 중인 코드와 데이터 보관 | 용량, 접근 지연, 대역폭 |
| 저장장치 | 파일과 데이터를 전원 없이 보존 | IOPS, 처리량, 응답 시간 |
CPU가 하는 일은 산술 연산뿐이 아니다. 분기, 함수 호출, 메모리 주소 계산과 인터럽트 처리도 CPU 명령으로 수행된다. 메모리에는 변수와 객체뿐 아니라 프로그램 코드, 스택, 힙, 공유 라이브러리와 파일 매핑도 놓인다.
CPU가 RAM만 직접 바라보는 것도 아니다. 가장 가까운 레지스터부터 L1·L2·L3 캐시, RAM, 저장장치까지 서로 다른 지연과 용량을 가진 계층을 거친다. 같은 계산량이라도 데이터가 캐시에 잘 남는지, 메모리 대역폭을 얼마나 쓰는지에 따라 실행 시간이 달라진다.
덧셈 한 번에도 두 자원이 함께 움직인다
다음 C 코드가 실행된다고 생각해 보자.
int add(void) {
int a = 10;
int b = 20;
int c = a + b;
return c;
}
개념적으로는 다음 흐름을 거친다.
- 프로그램 코드와 데이터가 프로세스의 가상 주소 공간에 배치된다.
- CPU가 명령어와 필요한 값을 캐시나 메모리에서 가져온다.
- 값을 레지스터에 올려 덧셈 명령을 실행한다.
- 결과는 레지스터에 남거나 캐시·메모리에 기록된다.
다만 이것은 이해를 위한 모델이다. 최적화된 실제 기계어에서는 a와 b가 RAM에 별도로 저장되지 않을 수 있다. 컴파일러가 10 + 20을 미리 계산해 결과만 남길 수도 있다. “모든 변수는 항상 RAM에 저장된다”는 설명은 실제 배치를 보장하지 않는다.
가상 메모리는 RAM과 같은 말이 아니다
프로세스는 보통 연속된 가상 주소 공간을 본다. 운영체제와 MMU가 page table을 통해 가상 주소를 물리 메모리의 page에 연결한다. 아직 접근하지 않은 page는 실제 RAM을 쓰지 않을 수 있고, 파일을 mmap한 영역은 파일 page를 backing으로 삼을 수 있다.
따라서 주소 공간의 크기와 실제 물리 메모리 사용량은 다르다.
- VSZ는 매핑된 가상 주소 공간의 규모를 보여 준다.
- RSS는 현재 물리 메모리에 상주한 page를 중심으로 집계한다.
- PSS는 공유 page의 비용을 프로세스 사이에 나눠 계산한다.
- 시스템의
available은 즉시 또는 회수 후 사용할 수 있는 메모리를 추정한다.
하나의 숫자만으로 누수나 압박을 단정하면 안 되는 이유다.
CPU 사용률이 높아도 CPU 병목은 아닐 수 있다
CPU 사용률은 일정 시간 동안 CPU가 작업을 실행한 비율에 가깝다. 값이 높다는 사실만으로 코드가 비효율적이거나 CPU 증설이 답이라고 말할 수 없다.
- 계산을 계속 수행하는 작업은 CPU 사용률이 높아질 수 있다.
- 디스크, 네트워크와 lock을 기다리면 CPU 사용률은 낮아질 수 있다.
- 메모리 접근 지연 때문에 instruction 처리량이 낮아질 수 있다.
- 컨테이너가 CPU limit에 걸리면 runnable 상태여도 throttling된다.
- 스레드가 많으면 실행 큐와 context switch가 늘 수 있다.
CPU 병목을 확인하려면 사용률과 함께 실행 큐, 사용자·커널 시간, throttling, instruction과 cache miss, on-CPU profile을 본다. 요청 지연이 큰데 CPU가 낮다면 trace와 off-CPU 관측으로 I/O·lock 대기부터 확인한다.
메모리 사용량이 높아도 누수는 아닐 수 있다
운영체제와 런타임은 빈 메모리를 캐시나 allocator의 재사용 공간으로 활용한다. 사용량이 높다는 이유만으로 곧바로 누수라고 판단하면 정상적인 cache까지 제거할 수 있다.
다음 질문을 시간축으로 확인한다.
- 프로세스 RSS와 cgroup working set이 계속 증가하는가?
- 시스템의
available이 줄고 PSI memory pressure가 생기는가? - swap 입출력이나 major page fault가 늘어나는가?
- 캐시로 회수 가능한 page인가, 계속 참조되는 객체인가?
- 트래픽이 줄어든 뒤에도 working set이 회복되지 않는가?
- OOM 종료나 긴 GC pause가 같은 시점에 발생했는가?
Linux 지표는 free·page cache·slab 읽는 법, macOS 지표는 vm_stat·memory_pressure로 메모리 확인하기에서 이어서 볼 수 있다.
병목은 관찰 조합으로 구분한다
| 관찰 | 먼저 의심할 지점 | 다음 확인 |
|---|---|---|
| CPU 사용률과 실행 큐가 함께 높음 | 연산량 또는 스레드 경합 | 프로파일러, 사용자·커널 시간 |
| CPU는 낮고 요청 지연은 큼 | I/O 대기, lock, 외부 서비스 | trace, off-CPU 분석 |
| RSS가 부하와 함께 계속 증가 | 보존 객체, 캐시 상한, 누수 | heap dump, allocation profile |
| cache miss와 memory stall이 큼 | 데이터 배치와 접근 패턴 | hardware counter, 대역폭 |
| swap과 major fault가 증가 | 물리 메모리 압박 | working set, vmstat, PSI |
Linux 프로세스의 첫 확인에는 CPU와 메모리를 함께 보는 명령이 유용하다.
pidstat -u -r -p PID 1
하드웨어 카운터를 사용할 권한이 있는 테스트 환경이라면 같은 부하에서 cycles, instructions와 cache miss를 비교할 수 있다.
perf stat -e cycles,instructions,cache-references,cache-misses -p PID
도구보다 중요한 것은 부하 조건과 관찰 시간을 고정하고 CPU 시간, 메모리 증가, 처리량과 지연을 함께 기록하는 일이다. 한 지표만 보고 자원을 늘리면 병목이 다른 곳으로 이동했을 뿐인데 해결됐다고 오해하기 쉽다.
물리 코어와 논리 CPU의 관계는 Hyper-Threading과 SMT, 추측 실행의 보안 경계는 Meltdown·Spectre 차이에서 연결해 볼 수 있다.
핵심 정리
CPU는 명령을 실행하고 메모리는 실행에 필요한 코드와 데이터를 보관한다. 둘은 항상 함께 작동한다. 성능 문제에서는 사용량의 높고 낮음보다 CPU에서 실행 중인지, 메모리나 I/O를 기다리는지, 그 대기가 사용자 응답 시간으로 이어지는지를 구분해야 한다.
참고 자료
'배움과 성장 > 시스템·성능' 카테고리의 다른 글
| JIT 컴파일과 가상 머신: bytecode·ISA·profiling·deoptimization 구분 (0) | 2024.12.05 |
|---|---|
| Linux free 읽는 법: available·page cache·slab·dentry 구분하기 (1) | 2024.11.25 |
| Linux CPU 스케줄러 동작: task·run queue·dispatcher 구분하기 (6) | 2024.09.22 |
| Linux 시스템 콜이란: 사용자 모드·커널 모드와 strace 추적 (4) | 2024.09.21 |
| macOS에서 smartctl·lspci·ethtool 대신 무엇을 쓸까 (3) | 2024.09.20 |
댓글