Skip to content
Published on

컴퓨터 구조 완전 가이드: ISA부터 GPU 병렬 아키텍처까지

Authors

들어가며

컴퓨터 구조(Computer Architecture)는 하드웨어와 소프트웨어의 경계를 이해하는 핵심 학문입니다. 전자/컴퓨터공학 전공자라면 반드시 깊이 이해해야 하는 분야이며, 고성능 소프트웨어 개발자, 시스템 프로그래머, 반도체 설계자 모두에게 필수 지식입니다.

이 가이드는 Patterson & Hennessy의 Computer Organization and DesignComputer Architecture: A Quantitative Approach를 기반으로, ISA 설계부터 현대 GPU 병렬 아키텍처까지 체계적으로 다룹니다.


1. 컴퓨터 구조 개요

폰 노이만 아키텍처 (Von Neumann Architecture)

현대 컴퓨터의 기반인 폰 노이만 아키텍처는 1945년 John von Neumann이 제안했습니다. 핵심 특징은 프로그램과 데이터를 같은 메모리 공간에 저장한다는 것입니다.

구성 요소:

  • CPU (Central Processing Unit): ALU + 제어 유닛 + 레지스터
  • 메모리: 프로그램 코드 + 데이터 저장
  • 입출력 장치: 키보드, 디스플레이, 디스크 등
  • 버스: 데이터/주소/제어 신호 전달

폰 노이만 병목(Von Neumann Bottleneck): CPU와 메모리 사이의 버스 대역폭이 성능을 제한합니다. 현대 캐시 계층 구조는 이 문제를 완화하기 위해 설계되었습니다.

하버드 아키텍처 (Harvard Architecture)

하버드 아키텍처는 명령어 메모리와 데이터 메모리를 분리하여 동시 접근을 허용합니다. DSP, 마이크로컨트롤러(AVR, PIC), 그리고 현대 CPU의 L1 캐시(I-Cache/D-Cache 분리)에 사용됩니다.

구분폰 노이만하버드
메모리통합분리
대역폭제한적높음
복잡도낮음높음
사용처범용 CPUDSP, 마이크로컨트롤러

컴퓨터 추상화 계층

컴퓨터 시스템은 여러 추상화 계층으로 구성됩니다:

응용 프로그램 (Application)
운영체제 (Operating System)
ISA (Instruction Set Architecture)  ← 하드웨어/소프트웨어 경계
마이크로아키텍처 (Microarchitecture)
논리 게이트 (Logic Gates)
트랜지스터 (Transistors)

ISA는 하드웨어와 소프트웨어의 계약(contract)입니다. 같은 ISA를 구현하는 프로세서라면 어떤 마이크로아키텍처를 사용하든 동일한 소프트웨어가 동작합니다.

성능 측정 지표

CPU 실행 시간 공식:

CPU Time = Instruction Count × CPI × Clock Cycle Time
         = Instruction Count × CPI / Clock Rate
  • CPI (Cycles Per Instruction): 명령어 1개 실행에 걸리는 평균 클럭 사이클 수
  • Clock Rate: Hz 단위, 초당 클럭 사이클 수 (예: 3.5 GHz)
  • MIPS (Millions of Instructions Per Second): 초당 실행 명령어 수 (백만 단위)
  • FLOPS (Floating Point Operations Per Second): 부동소수점 연산 성능

Amdahl의 법칙 (성능 개선 한계):

Speedup = 1 / ((1 - f) + f/s)

여기서 f는 병렬화 가능한 비율, s는 병렬화된 부분의 속도 향상 배율입니다. 전체의 40%만 병렬화 가능하다면, 아무리 많은 코어를 사용해도 최대 1.67배 속도 향상이 한계입니다.


2. 명령어 집합 구조 (ISA)

RISC vs CISC

RISC (Reduced Instruction Set Computer):

  • 단순하고 고정 길이 명령어
  • 레지스터 중심 연산 (Load/Store 아키텍처)
  • 하드웨어 파이프라이닝에 유리
  • 대표: ARM, RISC-V, MIPS, PowerPC

CISC (Complex Instruction Set Computer):

  • 복잡하고 가변 길이 명령어
  • 메모리 직접 연산 가능
  • 코드 밀도 높음 (적은 명령어로 복잡한 작업)
  • 대표: x86-64, VAX

현대 x86-64 프로세서는 내부적으로 RISC 마이크로연산(micro-ops)으로 변환하여 실행하므로, 실제 경계는 모호해졌습니다.

명령어 형식 (RISC-V 기준)

RISC-V는 6가지 명령어 형식을 사용합니다:

R-형식 (레지스터 연산):
[funct7|rs2|rs1|funct3|rd|opcode]
 7비트  5비트 5비트 3비트 5비트 7비트

I-형식 (즉시값/로드):
[imm[11:0]|rs1|funct3|rd|opcode]
  12비트   5비트 3비트 5비트 7비트

S-형식 (스토어):
[imm[11:5]|rs2|rs1|funct3|imm[4:0]|opcode]

B-형식 (분기):
[imm[12|10:5]|rs2|rs1|funct3|imm[4:1|11]|opcode]

U-형식 (상위 즉시값):
[imm[31:12]|rd|opcode]

J-형식 (JAL):
[imm[20|10:1|11|19:12]|rd|opcode]

RISC-V ISA 예제 코드

RISC-V는 UC Berkeley에서 개발한 오픈소스 ISA로, 교육 및 산업 모두에서 급성장 중입니다.

# RISC-V 어셈블리 예제
# 기본 산술 연산
add  a0, a0, a1      # a0 = a0 + a1
sub  t0, t1, t2      # t0 = t1 - t2
addi t0, t0, 10      # t0 = t0 + 10 (즉시값)
mul  a0, a1, a2      # a0 = a1 * a2

# 메모리 접근 (Load/Store)
lw   t0, 0(a0)       # t0 = Memory[a0 + 0]   (워드 로드)
lh   t1, 2(a0)       # t1 = Memory[a0 + 2]   (하프워드 로드)
lb   t2, 1(a0)       # t2 = Memory[a0 + 1]   (바이트 로드)
sw   t0, 4(a0)       # Memory[a0 + 4] = t0   (워드 스토어)

# 논리 연산
and  t0, t1, t2      # t0 = t1 & t2
or   t0, t1, t2      # t0 = t1 | t2
xor  t0, t1, t2      # t0 = t1 ^ t2
sll  t0, t1, t2      # t0 = t1 << t2 (논리 왼쪽 시프트)
srl  t0, t1, t2      # t0 = t1 >> t2 (논리 오른쪽 시프트)

# 분기 및 점프
beq  t0, t1, label   # if t0 == t1, jump to label
bne  t0, t1, label   # if t0 != t1, jump to label
blt  t0, t1, label   # if t0 < t1,  jump to label
bge  t0, t1, label   # if t0 >= t1, jump to label
jal  ra, func        # ra = PC+4; jump to func
jalr ra, 0(t0)       # ra = PC+4; jump to t0+0

# RISC-V 레지스터 규약
# x0 (zero): 항상 0
# x1 (ra):   반환 주소 (Return Address)
# x2 (sp):   스택 포인터 (Stack Pointer)
# x10-x17 (a0-a7): 함수 인자/반환값
# x5-x7, x28-x31 (t0-t6): 임시 레지스터
# x8-x9, x18-x27 (s0-s11): 저장 레지스터

C 함수를 RISC-V 어셈블리로 변환

// C 코드
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
# RISC-V 어셈블리 (재귀 팩토리얼)
factorial:
    addi sp, sp, -16     # 스택 프레임 할당
    sw   ra, 12(sp)      # 반환 주소 저장
    sw   a0, 8(sp)       # n 저장

    addi t0, zero, 1
    bgt  a0, t0, recurse # n > 1이면 재귀
    addi a0, zero, 1     # return 1
    j    done

recurse:
    addi a0, a0, -1      # n - 1
    jal  ra, factorial   # 재귀 호출
    lw   t0, 8(sp)       # n 복원
    mul  a0, t0, a0      # n * factorial(n-1)

done:
    lw   ra, 12(sp)      # 반환 주소 복원
    addi sp, sp, 16      # 스택 프레임 해제
    jalr zero, 0(ra)     # 반환

3. ALU와 데이터패스

ALU 설계

ALU(Arithmetic Logic Unit)는 CPU의 핵심 연산 장치입니다. 기본 연산:

ADD:  결과 = A + B
SUB:  결과 = A + (~B + 1) = A - B  (2의 보수)
AND:  결과 = A AND B
OR:   결과 = A OR B
XOR:  결과 = A XOR B
SLT:  결과 = (A < B) ? 1 : 0  (Set Less Than)
SLL:  결과 = A << B (시프트)

리플 캐리 가산기 (Ripple Carry Adder):

FA0: S0 = A0 XOR B0 XOR Cin;  C1 = (A0 AND B0) OR ...
FA1: S1 = A1 XOR B1 XOR C1;  C2 = ...
...
FAn: Sn = An XOR Bn XOR Cn;  Cout = ...

N비트 리플 캐리 가산기의 지연: O(N). 64비트 가산에 64단계 게이트 지연 발생.

선행 올림 가산기 (Carry Lookahead Adder, CLA):

생성(Generate)과 전파(Propagate)를 정의합니다:

Gi = Ai AND Bi       (올림 생성)
Pi = Ai XOR Bi       (올림 전파)
Ci+1 = Gi OR (Pi AND Ci)

4비트 그룹 단위로 올림을 병렬 계산하면 O(log N) 지연으로 줄어듭니다.

단일 사이클 데이터패스

단일 사이클(Single-cycle) 구현에서는 모든 명령어가 한 클럭 사이클에 실행됩니다.

명령어 흐름 (R-형식 ADD):
1. IF: PC → 명령어 메모리 → 명령어 인출
2. ID: 레지스터 파일에서 rs1, rs2 읽기 + 제어 신호 생성
3. EX: ALU에서 rs1 + rs2 계산
4. MEM: (R-형식은 메모리 접근 없음)
5. WB: ALU 결과를 rd에 기록

단일 사이클의 문제: 가장 느린 명령어(예: 메모리 접근)가 클럭 주기를 결정 → 빠른 명령어도 느린 클럭에 맞춰야 함.


4. 파이프라이닝 (Pipelining)

파이프라이닝은 세탁기-건조기-다리미를 동시에 사용하는 것처럼, 여러 명령어의 서로 다른 단계를 겹쳐 실행하는 기법입니다.

5단계 파이프라인

단계   약어  역할
------+-----+----------------------------------------
인출   IF   PC로부터 명령어를 메모리에서 가져옴
해독   ID   명령어 해독, 레지스터 읽기, 제어 신호 생성
실행   EX   ALU 연산 수행
메모리 MEM  데이터 메모리 읽기/쓰기
기록   WB   레지스터 파일에 결과 저장

타이밍 다이어그램:

사이클:  1    2    3    4    5    6    7    8
Inst 1: IF   ID   EX  MEM   WB
Inst 2:      IF   ID   EX  MEM   WB
Inst 3:           IF   ID   EX  MEM   WB
Inst 4:                IF   ID   EX  MEM   WB

이상적으로 5단계 파이프라인은 단일 사이클 대비 최대 5배 처리량 향상을 제공합니다.

파이프라인 해저드 (Pipeline Hazards)

1. 구조적 해저드 (Structural Hazard)

동일한 하드웨어 자원을 두 명령어가 동시에 사용하려 할 때 발생합니다.

  • 해결: 자원 분리(I-Cache와 D-Cache 분리), 레지스터 포트 추가

2. 데이터 해저드 (Data Hazard)

이전 명령어의 결과가 준비되기 전에 다음 명령어가 그 값을 필요로 할 때 발생합니다.

add t0, t1, t2    # t0 쓰기 (WB: 5사이클)
sub t3, t0, t4    # t0 읽기 (ID: 3사이클) → RAW 해저드!

종류:

  • RAW (Read After Write): 가장 흔함. 이전 명령어가 쓰기 전에 읽기 시도.
  • WAR (Write After Read): 순서 바뀐 실행(OoO)에서 발생.
  • WAW (Write After Write): 두 명령어가 같은 레지스터에 쓸 때.

해결 방법:

  • 전방전달 (Forwarding/Bypassing): EX/MEM 단계 결과를 바로 다음 EX 단계 입력으로 전달.
  • 스톨 (Stall/Bubble): 파이프라인을 멈추고 NOP(No Operation) 삽입. 성능 저하.
  • 코드 재배치: 컴파일러가 독립적인 명령어를 사이에 삽입.
# 로드-유스 해저드 (1 사이클 스톨 불가피)
lw  t0, 0(a0)      # 메모리에서 로드
add t1, t0, t2     # t0 바로 사용 → MEM→EX 전방전달 불가
# 해결: 컴파일러가 독립 명령어 삽입
lw  t0, 0(a0)
add t3, t4, t5     # 독립 명령어 삽입
add t1, t0, t2     # 이제 t0 준비됨

3. 제어 해저드 (Control Hazard)

분기(branch) 명령어로 인해 다음에 실행할 명령어가 불확실할 때 발생합니다.

beq t0, t1, label  # 분기 여부 EX 단계에서 결정
# 그 동안 IF, ID에서 인출된 명령어들은?

해결 방법:

  • 플러시 (Flush): 잘못 인출된 명령어 버리기 (2-3 사이클 페널티)
  • 분기 예측 (Branch Prediction):
    • 정적 예측: 항상 Not-Taken 또는 항상 Taken
    • 동적 예측: 1비트/2비트 예측기, BTB(Branch Target Buffer)
    • 현대 CPU는 95%+ 예측 정확도
  • 지연 분기 (Delayed Branch): 분기 명령어 직후 슬롯에 항상 유용한 명령어 배치 (MIPS)

슈퍼스칼라 파이프라인

현대 CPU는 여러 파이프라인을 병렬로 운영합니다:

Intel Core: 6-way 비순서 실행 (Out-of-Order Execution)
AMD Zen 4:  4-way 디코드 + OoO 실행
ARM Cortex-X4: 5-way 디코드

비순서 실행 (Out-of-Order Execution):

  1. 명령어를 순서대로 인출/해독
  2. 준비된 명령어부터 실행 (토마술로 알고리즘)
  3. 결과는 순서대로 기록 (Reorder Buffer 사용)

5. 메모리 계층 구조

메모리 계층

레지스터 (Register)
  용량: ~1KB  |  속도: 1 사이클  |  비용: 매우 높음

L1 캐시 (On-chip)
  용량: 32-64KB  |  속도: 4-5 사이클  |  비용: 높음

L2 캐시 (On-chip)
  용량: 256KB-1MB  |  속도: 12-15 사이클  |  비용: 중간

L3 캐시 (On-chip, 공유)
  용량: 8-64MB  |  속도: 30-40 사이클  |  비용: 낮음

DRAM (Main Memory)
  용량: 8-256GB  |  속도: 200-300 사이클  |  비용: 매우 낮음

SSD/NVMe
  용량: 1-4TB  |  속도: 10,000+ 사이클  |  비용: 아주 낮음

지역성 원리 (Principle of Locality)

  • 시간적 지역성 (Temporal Locality): 최근 접근한 데이터는 곧 다시 접근될 가능성이 높다. (루프 변수, 카운터)
  • 공간적 지역성 (Spatial Locality): 접근한 데이터 근처의 데이터도 곧 접근될 가능성이 높다. (배열 순차 접근)
// 공간적 지역성 최적화 예시
// 나쁜 예: 열 우선 접근 (캐시 미스 빈번)
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += A[i][j];  // A[0][0], A[1][0], A[2][0]... (행 건너뜀)

// 좋은 예: 행 우선 접근 (캐시 친화적)
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += A[i][j];  // A[0][0], A[0][1], A[0][2]... (연속 메모리)

캐시 구조

Direct-mapped 캐시: 메모리의 각 블록이 캐시의 딱 한 곳에만 저장될 수 있습니다.

  • 장점: 구현 간단, 빠른 접근
  • 단점: 충돌 미스(Conflict Miss) 발생 가능

N-way Set-associative 캐시: 메모리 블록이 N개 슬롯 중 하나에 저장됩니다.

  • 실제 CPU에서 가장 많이 사용 (L1: 4-way, L2: 8-way, L3: 16-way)

Fully-associative 캐시: 메모리 블록이 어느 슬롯에나 저장 가능.

  • 장점: 충돌 미스 없음
  • 단점: 탐색 비용 큼 (TLB에 사용)

캐시 주소 분해 (32비트 주소, 4KB 캐시, 64B 블록, 4-way):

[태그(20비트) | 인덱스(6비트) | 오프셋(6비트)]

캐시 교체 정책

  • LRU (Least Recently Used): 가장 오래 사용되지 않은 블록 교체. 성능 최고, 구현 복잡.
  • FIFO (First In First Out): 가장 먼저 들어온 블록 교체. 구현 간단.
  • Random: 무작위 교체. 하드웨어 구현 간단, LRU와 유사한 성능.

Write 정책

  • Write-through: 캐시와 메모리를 동시에 갱신. 일관성 보장, 대역폭 소비.
  • Write-back: 캐시만 갱신, 교체 시 메모리 기록(Dirty bit 사용). 성능 좋음, 일관성 복잡.

6. 가상 메모리

개요

가상 메모리는 각 프로세스에 독립적인 주소 공간을 제공하여 보호, 격리, 메모리 오버커밋을 가능하게 합니다.

가상 주소 (VA): 프로세스가 사용하는 주소 (0x0000 ~ 0xFFFFFFFF)
물리 주소 (PA): 실제 DRAM의 주소
페이지:         가상/물리 메모리의 고정 크기 블록 (보통 4KB)

페이지 테이블 변환

가상 주소 [VPN | Page Offset]
         페이지 테이블 조회
물리 주소 [PFN | Page Offset]

64비트 시스템에서는 4단계 페이지 테이블을 사용합니다 (x86-64):

[PML4(9비트) | PDPT(9비트) | PD(9비트) | PT(9비트) | Offset(12비트)]

TLB (Translation Lookaside Buffer)

페이지 테이블 조회는 메모리 접근을 추가로 필요로 합니다. TLB는 최근 변환 결과를 캐싱하는 완전 연관 캐시입니다.

TLB Hit:  가상 주소 → TLB 조회 → 물리 주소 (1-2 사이클)
TLB Miss: 가상 주소 → 페이지 테이블 워크 → 물리 주소 (100+ 사이클)

TLB 크기: 보통 64-1024 엔트리. 히트율 99%+ 유지.

페이지 교체 알고리즘

메모리가 가득 찼을 때 어떤 페이지를 디스크로 내보낼지 결정합니다.

  • Optimal: 미래에 가장 오랫동안 사용되지 않을 페이지 교체 (이론적 최적, 실제 불가)
  • LRU: 가장 오래 사용되지 않은 페이지 교체 (성능 좋음, 구현 비쌈)
  • Clock (FIFO + 기회): 실제 OS에서 많이 사용하는 LRU 근사

7. 입출력 시스템

I/O 제어 방법

폴링 (Polling): CPU가 장치 상태를 주기적으로 확인합니다.

  • 장점: 구현 간단, 낮은 지연
  • 단점: CPU 시간 낭비 (Busy-wait)

인터럽트 (Interrupt): 장치가 준비되면 CPU에 신호를 보냅니다.

  • 장점: CPU가 다른 작업 수행 가능
  • 단점: 인터럽트 처리 오버헤드

DMA (Direct Memory Access): DMA 컨트롤러가 CPU 없이 직접 메모리-장치 간 데이터 전송을 수행합니다.

  • 대량 데이터 전송에 필수 (디스크, 네트워크, GPU)
  • CPU는 전송 시작/완료만 처리

버스 아키텍처

PCIe 5.0:   x16 슬롯 = 128 GB/s 양방향
NVMe (PCIe):최대 7 GB/s 순차 읽기 (Gen4)
USB 4.0:    최대 40 Gbps
DDR5-6400:  최대 51.2 GB/s (채널당)

8. 병렬 아키텍처

Flynn의 분류

분류명령어 스트림데이터 스트림예시
SISD단일단일단일 코어 CPU
SIMD단일다중GPU, AVX 벡터 연산
MISD다중단일결함 허용 시스템
MIMD다중다중멀티코어 CPU, 클러스터

멀티코어와 캐시 일관성

여러 코어가 같은 데이터를 캐시에 갖고 있을 때 일관성을 유지해야 합니다.

MESI 프로토콜 (Modified, Exclusive, Shared, Invalid):

Modified (M): 이 코어만 수정된 최신 복사본 보유
Exclusive (E): 이 코어만 보유, 메모리와 동일
Shared (S):   여러 코어가 읽기 전용 복사본 보유
Invalid (I):  유효하지 않음 (다른 코어가 수정함)

상태 전환:

  • 코어 A가 S 상태 데이터 쓰기 → A는 M으로 전환, 다른 코어는 I로 무효화
  • 코어 B가 I 상태 데이터 읽기 → 버스 스누핑으로 A에서 전달

OpenMP 병렬 프로그래밍

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

// 병렬 배열 합산
int main() {
    int n = 1000000;
    long long sum = 0;
    int *arr = malloc(n * sizeof(int));

    for (int i = 0; i < n; i++)
        arr[i] = i + 1;

    // reduction으로 안전한 병렬 합산
    #pragma omp parallel for reduction(+:sum) schedule(static)
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }

    printf("Sum = %lld\n", sum);  // 500000500000

    // 병렬 for + 작업 분배
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        printf("Thread %d of %d\n", tid, nthreads);
    }

    free(arr);
    return 0;
}
// 행렬 곱셈 병렬화 (캐시 친화적 + OpenMP)
void matmul(float *A, float *B, float *C, int N) {
    #pragma omp parallel for collapse(2) schedule(dynamic, 64)
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            float sum = 0.0f;
            for (int k = 0; k < N; k++) {
                sum += A[i*N + k] * B[k*N + j];
            }
            C[i*N + j] = sum;
        }
    }
}

NUMA 아키텍처

NUMA(Non-Uniform Memory Access)는 각 CPU 소켓이 로컬 메모리를 갖는 구조입니다.

소켓 0 (Core 0-15) ←→ Local DRAM (64GB)  [~80ns]
QPI/Infinity Fabric
소켓 1 (Core 16-31) ←→ Local DRAM (64GB) [~80ns]

소켓 0에서 소켓 1의 메모리 접근: ~160ns (2배 지연)

numactl을 사용하면 프로세스를 특정 NUMA 노드에 고정할 수 있습니다.


9. GPU 아키텍처

GPU vs CPU 설계 철학

항목CPUGPU
코어 수수십 개수천-수만 개
코어 복잡도매우 복잡 (OoO, 분기예측)단순
캐시 크기크고 복잡작고 단순
설계 목표단일 스레드 지연 최소화처리량(Throughput) 최대화
용도범용 직렬 연산대규모 병렬 연산

SIMT 실행 모델

SIMT(Single Instruction, Multiple Thread)는 GPU의 핵심 실행 모델입니다.

워프(Warp): 32개 스레드의 묶음 (NVIDIA)
32개 스레드가 동일한 명령어를 동시에 실행
→ 각 스레드는 서로 다른 데이터에 작용

워프 스케줄러: 지연 발생 (메모리 접근) 즉시 다른 워프로 전환
→ 수천 개 워프를 빠르게 전환하여 지연을 숨김 (Latency Hiding)

NVIDIA GPU 계층 구조

GPU
├── GPC (Graphics Processing Cluster) x 8
│   └── SM (Streaming Multiprocessor) x 7-12
│       ├── CUDA Core x 128 (FP32 연산)
│       ├── Tensor Core x 4 (행렬 연산, AI)
│       ├── RT Core x 1 (레이 트레이싱)
│       ├── Warp Scheduler x 4
│       ├── Register File (256KB)
│       └── Shared Memory / L1 Cache (128-256KB)
└── L2 Cache (공유, 수십 MB)

NVIDIA H100 (Hopper, 2022):

  • 132개 SM, 16,896개 CUDA Core
  • 528개 Tensor Core (4세대, FP8 지원)
  • 80GB HBM3 메모리, 3.35 TB/s 대역폭
  • 4 PetaFLOPS FP8 Tensor Core 성능

CUDA 프로그래밍 기초

#include <cuda_runtime.h>
#include <stdio.h>

// GPU 커널: 벡터 덧셈
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
    // 스레드 글로벌 인덱스 계산
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    int n = 1 << 20;  // 1M 원소
    size_t size = n * sizeof(float);

    // 호스트(CPU) 메모리
    float *h_a = (float*)malloc(size);
    float *h_b = (float*)malloc(size);
    float *h_c = (float*)malloc(size);

    // 디바이스(GPU) 메모리
    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, size);
    cudaMalloc(&d_b, size);
    cudaMalloc(&d_c, size);

    // 호스트 → 디바이스 복사
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);

    // 커널 실행: 블록당 256 스레드
    int threadsPerBlock = 256;
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);

    // 디바이스 → 호스트 복사
    cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);

    cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
    free(h_a); free(h_b); free(h_c);
    return 0;
}

GPU 메모리 최적화

// 공유 메모리를 활용한 행렬 곱 최적화
#define TILE_SIZE 16

__global__ void matmulShared(float *A, float *B, float *C, int N) {
    __shared__ float tileA[TILE_SIZE][TILE_SIZE];
    __shared__ float tileB[TILE_SIZE][TILE_SIZE];

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;

    for (int t = 0; t < N / TILE_SIZE; t++) {
        // 타일을 공유 메모리로 로드
        tileA[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
        tileB[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        __syncthreads();  // 모든 스레드 로드 완료 대기

        for (int k = 0; k < TILE_SIZE; k++)
            sum += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
        __syncthreads();
    }

    if (row < N && col < N)
        C[row * N + col] = sum;
}

Tensor Core와 AI 가속

Tensor Core는 행렬 곱셈-누산(MMA: Matrix Multiply-Accumulate)을 하드웨어에서 가속합니다.

일반 CUDA Core: FP32 MAC 1/사이클
Tensor Core(4세대): 16x16x16 행렬 곱 = 4096 FP16 MAC/사이클

NVIDIA cuBLAS, cuDNN, TensorRT는 Tensor Core를 자동으로 활용합니다.


10. 최신 아키텍처 트렌드

칩렛(Chiplet) 설계

단일 대형 다이(monolithic die) 대신 여러 작은 칩렛을 인터포저로 연결합니다.

  • AMD EPYC (Genoa): 12개 CCD(Core Complex Die, 5nm) + 1개 IOD(I/O Die, 6nm)
  • Intel Meteor Lake: CPU + GPU + SoC 타일 분리
  • 장점: 수율 향상, 공정 최적화, 비용 절감
  • 기술: TSMC CoWoS, Intel EMIB, UCIe 표준

HBM (High Bandwidth Memory)

DRAM을 3D로 적층하여 초고대역폭을 달성합니다.

HBM3E (2024): 9.6 Gbps/, 8-스택 = 1.2 TB/s (패키지당)
GDDR6X (RTX 4090): 21 Gbps/, 384비트 = 1 TB/s
DDR5-6400: 51.2 GB/s (채널당, CPU 기준)

NPU/TPU: AI 전용 가속기

  • Google TPU v5p: 459 TFLOPS BF16, Mesh 연결
  • Apple Neural Engine: iPhone 15 Pro, 35 TOPS INT8
  • Qualcomm Hexagon: 스마트폰 NPU, 75 TOPS
  • Intel Gaudi 3: 1835 TFLOPS BF16

RISC-V의 부상

오픈소스 ISA로 저전력 임베디드부터 데이터센터 서버까지 확장 중:

  • SiFive, StarFive, Alibaba T-Head
  • RISC-V International: 3,000+ 회원사
  • Linux 5.19 공식 지원, Android 포팅 완료

11. 성능 최적화 실전

캐시 친화적 프로그래밍

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

#define N 4096

float A[N][N], B[N][N], C[N][N];

// 비효율적: 열 우선 접근 (캐시 미스 많음)
void matmul_naive() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; k++)
                C[i][j] += A[i][k] * B[k][j];  // B[k][j] 접근이 비연속
}

// 효율적: 블록 타일링 (캐시 재사용)
#define BLOCK 64
void matmul_tiled() {
    for (int ii = 0; ii < N; ii += BLOCK)
        for (int jj = 0; jj < N; jj += BLOCK)
            for (int kk = 0; kk < N; kk += BLOCK)
                for (int i = ii; i < ii+BLOCK && i < N; i++)
                    for (int j = jj; j < jj+BLOCK && j < N; j++)
                        for (int k = kk; k < kk+BLOCK && k < N; k++)
                            C[i][j] += A[i][k] * B[k][j];
}

SIMD 벡터화 (AVX2)

#include <immintrin.h>  // AVX2

// AVX2로 float 8개를 동시에 덧셈
void vector_add_avx2(float *a, float *b, float *c, int n) {
    int i;
    for (i = 0; i <= n - 8; i += 8) {
        __m256 va = _mm256_loadu_ps(&a[i]);   // 8x float 로드
        __m256 vb = _mm256_loadu_ps(&b[i]);
        __m256 vc = _mm256_add_ps(va, vb);    // 8x 동시 덧셈
        _mm256_storeu_ps(&c[i], vc);          // 8x float 저장
    }
    // 나머지 원소 처리
    for (; i < n; i++)
        c[i] = a[i] + b[i];
}

12. 퀴즈

Q1. 파이프라이닝에서 데이터 해저드(Data Hazard)가 발생하는 원인과 해결 방법은?

정답: 이전 명령어의 쓰기(Write)가 완료되기 전에 다음 명령어가 해당 레지스터를 읽으려 할 때(RAW: Read After Write) 발생합니다.

해결 방법:

  • 전방전달(Forwarding): EX/MEM 단계 결과를 바로 다음 명령어 EX 입력으로 직접 전달하여 스톨 없이 해결.
  • 스톨(Stall): NOP(버블)을 삽입하여 파이프라인을 일시 중단. 성능 저하.
  • 코드 재배치: 컴파일러가 독립적인 명령어를 사이에 끼워 지연 없이 처리.
  • 로드-유스 해저드의 경우 1사이클 스톨이 불가피합니다 (MEM 단계 결과를 EX 입력으로 전달 불가).
Q2. Direct-mapped 캐시와 4-way Set-associative 캐시의 차이점은?

정답: Direct-mapped 캐시는 메모리의 각 블록이 캐시의 딱 한 슬롯에만 매핑되는 반면, 4-way Set-associative는 동일한 인덱스를 가진 4개의 슬롯 중 하나에 매핑될 수 있습니다.

핵심 차이:

  • Direct-mapped: 충돌 미스(Conflict Miss) 발생 가능. 구현 단순, 비용 낮음.
  • 4-way SA: 충돌 미스 감소. LRU 등 교체 정책 필요. 비용 증가.
  • 실제 CPU L1 캐시는 보통 4-way 또는 8-way를 사용하며, 완전 연관(Fully-associative)은 TLB에만 사용됩니다.
Q3. TLB(Translation Lookaside Buffer)의 역할과 TLB 미스 시 처리 과정을 설명하시오.

정답: TLB는 가상 주소를 물리 주소로 변환하는 최근 결과를 캐싱하는 완전 연관 캐시입니다. 메모리에 있는 페이지 테이블 접근 오버헤드를 줄여줍니다.

TLB 미스 처리:

  1. TLB에 해당 가상 페이지 번호(VPN)가 없음
  2. 하드웨어(x86) 또는 소프트웨어(RISC-V, MIPS) 페이지 테이블 워크 수행
  3. 다단계 페이지 테이블을 순서대로 조회 (PML4 → PDPT → PD → PT)
  4. 최종 물리 프레임 번호(PFN) 획득
  5. TLB에 새 항목 추가 (기존 항목 교체 필요 시 LRU 적용)
  6. 원래 메모리 접근 재시도
  • 전체 과정: 수백 사이클 소요 → TLB 히트율(99%+) 유지가 중요
Q4. GPU의 워프(Warp) 발산(Divergence)이란 무엇이며 성능에 어떤 영향을 주는가?

정답: 워프 내 32개 스레드가 서로 다른 분기(if-else)를 취할 때 워프 발산이 발생합니다.

동작 방식:

  • SIMT 모델에서 워프의 모든 스레드는 같은 명령어를 실행해야 합니다.
  • if 블록 실행 시: if를 취한 스레드만 활성화, else 스레드는 마스킹(비활성)
  • else 블록 실행 시: else를 취한 스레드만 활성화, if 스레드는 마스킹
  • 두 경로를 직렬로 실행하므로 최악 2배 시간 소요

해결 방법:

  • 워프 내 스레드들이 같은 분기를 취하도록 데이터 정렬
  • 분기 대신 수학적 연산으로 대체 (조건부 선택)
  • 쿠다 쉐이더의 분기 최소화 설계
Q5. Amdahl의 법칙을 이용하여, 전체 코드의 80%를 병렬화했을 때 최대 이론 속도 향상을 계산하시오.

정답: 무한히 많은 프로세서를 사용한다고 가정하면 최대 5배 속도 향상입니다.

계산:

  • 병렬화 비율 f = 0.8 (80%)
  • 순차 비율 = 1 - 0.8 = 0.2 (20%)
  • 병렬화 배율 s를 무한대로 보내면: f/s → 0
  • Speedup = 1 / ((1 - f) + f/s) = 1 / (0.2 + 0) = 1 / 0.2 = 5배

의미: 아무리 많은 코어를 추가해도, 순차 실행 부분(20%)이 전체 속도를 5배로 제한합니다. 이것이 순차 병목을 제거하는 것이 병렬 최적화의 핵심인 이유입니다.


참고 자료