Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

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

이 가이드는 Patterson & Hennessy의 *Computer Organization and Design*과 *Computer 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 분리)에 사용됩니다.

| 구분 | 폰 노이만 | 하버드 |

| ------ | --------- | --------------------- |

| 메모리 | 통합 | 분리 |

| 대역폭 | 제한적 | 높음 |

| 복잡도 | 낮음 | 높음 |

| 사용처 | 범용 CPU | DSP, 마이크로컨트롤러 |

컴퓨터 추상화 계층

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

응용 프로그램 (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 설계 철학

| 항목 | CPU | GPU |

| ----------- | ------------------------- | ------------------------- |

| 코어 수 | 수십 개 | 수천-수만 개 |

| 코어 복잡도 | 매우 복잡 (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. 퀴즈

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

**해결 방법**:

- 전방전달(Forwarding): EX/MEM 단계 결과를 바로 다음 명령어 EX 입력으로 직접 전달하여 스톨 없이 해결.

- 스톨(Stall): NOP(버블)을 삽입하여 파이프라인을 일시 중단. 성능 저하.

- 코드 재배치: 컴파일러가 독립적인 명령어를 사이에 끼워 지연 없이 처리.

- 로드-유스 해저드의 경우 1사이클 스톨이 불가피합니다 (MEM 단계 결과를 EX 입력으로 전달 불가).

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

**핵심 차이**:

- Direct-mapped: 충돌 미스(Conflict Miss) 발생 가능. 구현 단순, 비용 낮음.

- 4-way SA: 충돌 미스 감소. LRU 등 교체 정책 필요. 비용 증가.

- 실제 CPU L1 캐시는 보통 4-way 또는 8-way를 사용하며, 완전 연관(Fully-associative)은 TLB에만 사용됩니다.

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

**TLB 미스 처리**:

1. TLB에 해당 가상 페이지 번호(VPN)가 없음

2. 하드웨어(x86) 또는 소프트웨어(RISC-V, MIPS) 페이지 테이블 워크 수행

3. 다단계 페이지 테이블을 순서대로 조회 (PML4 → PDPT → PD → PT)

4. 최종 물리 프레임 번호(PFN) 획득

5. TLB에 새 항목 추가 (기존 항목 교체 필요 시 LRU 적용)

6. 원래 메모리 접근 재시도

- 전체 과정: 수백 사이클 소요 → TLB 히트율(99%+) 유지가 중요

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

**동작 방식**:

- SIMT 모델에서 워프의 모든 스레드는 같은 명령어를 실행해야 합니다.

- if 블록 실행 시: if를 취한 스레드만 활성화, else 스레드는 마스킹(비활성)

- else 블록 실행 시: else를 취한 스레드만 활성화, if 스레드는 마스킹

- 두 경로를 직렬로 실행하므로 최악 2배 시간 소요

**해결 방법**:

- 워프 내 스레드들이 같은 분기를 취하도록 데이터 정렬

- 분기 대신 수학적 연산으로 대체 (조건부 선택)

- 쿠다 쉐이더의 분기 최소화 설계

**정답**: 무한히 많은 프로세서를 사용한다고 가정하면 최대 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배로 제한합니다. 이것이 순차 병목을 제거하는 것이 병렬 최적화의 핵심인 이유입니다.

참고 자료

- Patterson, D. & Hennessy, J. (2020). _Computer Organization and Design: RISC-V Edition_ (2nd ed.). Morgan Kaufmann.

- Hennessy, J. & Patterson, D. (2019). _Computer Architecture: A Quantitative Approach_ (6th ed.). Morgan Kaufmann.

- RISC-V International. (2024). _The RISC-V Instruction Set Manual_. https://riscv.org/technical/specifications/

- NVIDIA. (2023). _CUDA C++ Programming Guide_. https://docs.nvidia.com/cuda/cuda-c-programming-guide/

- ARM Ltd. (2024). _ARM Architecture Reference Manual_. https://developer.arm.com/documentation/

- Intel. (2024). _Intel 64 and IA-32 Architectures Software Developer's Manual_.

현재 단락 (1/490)

컴퓨터 구조(Computer Architecture)는 하드웨어와 소프트웨어의 경계를 이해하는 핵심 학문입니다. 전자/컴퓨터공학 전공자라면 반드시 깊이 이해해야 하는 분야이며, 고...

작성 글자: 0원문 글자: 15,194작성 단락: 0/490