Skip to content

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

|

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

들어가며

컴퓨터 구조(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배로 제한합니다. 이것이 순차 병목을 제거하는 것이 병렬 최적화의 핵심인 이유입니다.


참고 자료

Computer Architecture Complete Guide: From ISA to GPU Parallel Architecture

Introduction

Computer Architecture is the foundational discipline that bridges hardware and software. Whether you are an electronics engineer, computer science student, or systems programmer, a deep understanding of how processors work is essential for writing high-performance software, designing chips, and reasoning about system behavior.

This guide is structured around Patterson & Hennessy's Computer Organization and Design and Computer Architecture: A Quantitative Approach, covering everything from ISA design to modern GPU parallel architectures.


1. Overview of Computer Architecture

Von Neumann Architecture

The Von Neumann architecture, proposed by John von Neumann in 1945, forms the foundation of nearly all modern computers. Its defining characteristic is that program instructions and data share the same memory space.

Components:

  • CPU (Central Processing Unit): ALU + Control Unit + Registers
  • Memory: Stores both program code and data
  • I/O Devices: Keyboard, display, disk, network
  • Bus: Carries data, address, and control signals

Von Neumann Bottleneck: The bandwidth between the CPU and memory limits overall performance. The modern cache hierarchy was designed to mitigate this bottleneck.

Harvard Architecture

The Harvard architecture uses separate instruction and data memories, enabling simultaneous access to both. It is used in DSPs, microcontrollers (AVR, PIC), and in the L1 cache split (I-Cache / D-Cache) of modern processors.

AspectVon NeumannHarvard
MemoryUnifiedSeparate
BandwidthLimitedHigher
ComplexityLowerHigher
Use caseGeneral-purpose CPUDSP, microcontroller

Levels of Abstraction

Computer systems are organized in layers of abstraction:

Application Software
Operating System
ISA (Instruction Set Architecture)Hardware/Software boundary
Microarchitecture
Logic Gates
Transistors

The ISA is the contract between hardware and software. Any processor that implements the same ISA can run the same software, regardless of its internal microarchitecture.

Performance Metrics

CPU Execution Time:

CPU Time = Instruction Count × CPI × Clock Cycle Time
         = Instruction Count × CPI / Clock Rate
  • CPI (Cycles Per Instruction): Average clock cycles per instruction
  • Clock Rate: Measured in Hz (e.g., 3.5 GHz)
  • MIPS (Millions of Instructions Per Second): Throughput in instructions
  • FLOPS (Floating Point Operations Per Second): Floating-point throughput

Amdahl's Law (Limits of Parallel Speedup):

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

Where f is the fraction of work that can be parallelized, and s is the speedup of the parallelized part. If only 40% of a program can be parallelized, the theoretical maximum speedup is 1.67x regardless of core count.


2. Instruction Set Architecture (ISA)

RISC vs CISC

RISC (Reduced Instruction Set Computer):

  • Simple, fixed-length instructions
  • Register-centric operations (Load/Store architecture)
  • Favors hardware pipelining
  • Examples: ARM, RISC-V, MIPS, PowerPC

CISC (Complex Instruction Set Computer):

  • Complex, variable-length instructions
  • Can operate directly on memory
  • Higher code density (more work per instruction)
  • Examples: x86-64, VAX

Modern x86-64 processors internally decode CISC instructions into RISC-like micro-ops, blurring the original distinction.

Instruction Formats (RISC-V)

RISC-V defines six instruction formats:

R-type (Register operations):
[funct7 | rs2 | rs1 | funct3 | rd | opcode]
  7-bit   5-bit 5-bit  3-bit  5-bit  7-bit

I-type (Immediate / Load):
[imm[11:0] | rs1 | funct3 | rd | opcode]
  12-bit    5-bit   3-bit  5-bit  7-bit

S-type (Store):
[imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode]

B-type (Branch):
[imm[12|10:5] | rs2 | rs1 | funct3 | imm[4:1|11] | opcode]

U-type (Upper immediate):
[imm[31:12] | rd | opcode]

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

RISC-V Assembly Examples

RISC-V is an open-source ISA developed at UC Berkeley, growing rapidly in both education and industry.

# RISC-V Assembly Examples
# Basic arithmetic
add  a0, a0, a1      # a0 = a0 + a1
sub  t0, t1, t2      # t0 = t1 - t2
addi t0, t0, 10      # t0 = t0 + 10 (immediate)
mul  a0, a1, a2      # a0 = a1 * a2

# Memory access (Load/Store)
lw   t0, 0(a0)       # t0 = Memory[a0 + 0]  (word load)
lh   t1, 2(a0)       # t1 = Memory[a0 + 2]  (halfword load)
lb   t2, 1(a0)       # t2 = Memory[a0 + 1]  (byte load)
sw   t0, 4(a0)       # Memory[a0 + 4] = t0  (word store)

# Logical operations
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 (logical left shift)
srl  t0, t1, t2      # t0 = t1 >> t2 (logical right shift)

# Branches and jumps
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 address in t0

# RISC-V Register Conventions
# x0  (zero): hardwired zero
# x1  (ra):   return address
# x2  (sp):   stack pointer
# x10-x17 (a0-a7): function arguments / return values
# x5-x7, x28-x31 (t0-t6): temporaries
# x8-x9, x18-x27 (s0-s11): saved registers

Translating C to RISC-V Assembly

// C code
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
# RISC-V Assembly (recursive factorial)
factorial:
    addi sp, sp, -16     # Allocate stack frame
    sw   ra, 12(sp)      # Save return address
    sw   a0, 8(sp)       # Save n

    addi t0, zero, 1
    bgt  a0, t0, recurse # if n > 1, recurse
    addi a0, zero, 1     # return 1
    j    done

recurse:
    addi a0, a0, -1      # n - 1
    jal  ra, factorial   # recursive call
    lw   t0, 8(sp)       # restore n
    mul  a0, t0, a0      # n * factorial(n-1)

done:
    lw   ra, 12(sp)      # restore return address
    addi sp, sp, 16      # free stack frame
    jalr zero, 0(ra)     # return

3. ALU and Datapath

ALU Design

The ALU (Arithmetic Logic Unit) is the computational core of the CPU:

ADD:  result = A + B
SUB:  result = A + (~B + 1) = A - B  (two's complement)
AND:  result = A AND B
OR:   result = A OR B
XOR:  result = A XOR B
SLT:  result = (A < B) ? 1 : 0  (Set Less Than)
SLL:  result = A << B

Ripple Carry Adder:

FA0: S0 = A0 XOR B0 XOR Cin;  C1 = carry out
FA1: S1 = A1 XOR B1 XOR C1;  C2 = carry out
...

For an N-bit ripple carry adder, delay is O(N). A 64-bit addition requires 64 gate delays.

Carry Lookahead Adder (CLA):

Define Generate and Propagate:

Gi = Ai AND Bi       (carry generate)
Pi = Ai XOR Bi       (carry propagate)
Ci+1 = Gi OR (Pi AND Ci)

By computing carries for 4-bit groups in parallel, total delay drops to O(log N).

Single-Cycle Datapath

In a single-cycle implementation, every instruction completes in exactly one clock cycle.

Instruction flow (R-type ADD):
1. IF:  PC → instruction memory → fetch instruction
2. ID:  Read rs1, rs2 from register file; generate control signals
3. EX:  ALU computes rs1 + rs2
4. MEM: (R-type has no memory access)
5. WB:  Write ALU result to rd

Problem: The slowest instruction (e.g., memory load) determines the clock period. Fast instructions are penalized by the slow clock.


4. Pipelining

Pipelining overlaps the execution of multiple instructions, just like running a washer, dryer, and iron simultaneously for a load of laundry.

Five-Stage Pipeline

Stage  Abbr  Function
------+------+--------------------------------------------
Fetch  IF    Fetch instruction from memory using PC
Decode ID    Decode instruction, read registers, gen control
Execute EX   Perform ALU operation
Memory MEM   Read/write data memory
Writeback WB Write result back to register file

Timing diagram:

Cycle:  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

Ideally, a 5-stage pipeline provides up to 5x throughput improvement over single-cycle.

Pipeline Hazards

1. Structural Hazard

Two instructions require the same hardware resource at the same time.

  • Solution: Duplicate resources (separate I-Cache and D-Cache), add register file ports.

2. Data Hazard

An instruction needs a result that a previous instruction has not yet produced.

add t0, t1, t2    # write t0 (WB: cycle 5)
sub t3, t0, t4    # read t0  (ID: cycle 3) → RAW hazard!

Types:

  • RAW (Read After Write): Most common. Next instruction reads before previous writes.
  • WAR (Write After Read): Occurs in out-of-order execution.
  • WAW (Write After Write): Two instructions write the same register.

Solutions:

  • Forwarding (Bypassing): Route EX/MEM stage result directly to next EX stage input — no stall.
  • Stall (Bubble): Insert NOP cycles. Reduces performance.
  • Code Reordering: Compiler inserts independent instructions between dependent ones.
# Load-use hazard: 1 stall cycle unavoidable
lw  t0, 0(a0)      # load from memory
add t1, t0, t2     # use t0 immediately → MEM→EX forwarding impossible
# Solution: compiler inserts an independent instruction
lw  t0, 0(a0)
add t3, t4, t5     # independent instruction fills the slot
add t1, t0, t2     # t0 is now ready

3. Control Hazard

A branch instruction creates uncertainty about which instruction to fetch next.

beq t0, t1, label  # branch decision made in EX stage
# Instructions already fetched in IF/ID may be wrong

Solutions:

  • Flush: Discard incorrectly fetched instructions (2-3 cycle penalty).
  • Branch Prediction:
    • Static: always Not-Taken or always Taken
    • Dynamic: 1-bit/2-bit predictors, Branch Target Buffer (BTB)
    • Modern CPUs achieve 95%+ prediction accuracy
  • Delayed Branch: Always execute the instruction after the branch (MIPS approach).

Superscalar Pipelines

Modern CPUs run multiple pipelines in parallel:

Intel Core: 6-way out-of-order execution (OoO)
AMD Zen 4:  4-way decode + OoO execution
ARM Cortex-X4: 5-way decode

Out-of-Order Execution:

  1. Instructions fetched and decoded in order
  2. Instructions dispatched as soon as their operands are ready (Tomasulo's algorithm)
  3. Results committed in original program order (Reorder Buffer)

5. Memory Hierarchy

The Memory Hierarchy

Registers
  Size: ~1KB  |  Latency: 1 cycle       |  Cost: very high

L1 Cache (on-chip)
  Size: 32-64KB  |  Latency: 4-5 cycles  |  Cost: high

L2 Cache (on-chip)
  Size: 256KB-1MB  |  Latency: 12-15 cycles  |  Cost: medium

L3 Cache (on-chip, shared)
  Size: 8-64MB  |  Latency: 30-40 cycles  |  Cost: low

DRAM (Main Memory)
  Size: 8-256GB  |  Latency: 200-300 cycles  |  Cost: very low

SSD / NVMe
  Size: 1-4TB  |  Latency: 10,000+ cycles  |  Cost: extremely low

Principle of Locality

  • Temporal Locality: Recently accessed data will likely be accessed again soon. (Loop variables, counters)
  • Spatial Locality: Data near recently accessed locations will likely be accessed soon. (Array traversal)
// Spatial locality optimization
// Bad: column-major access (frequent cache misses)
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]... (non-contiguous)

// Good: row-major access (cache-friendly)
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]... (contiguous memory)

Cache Organization

Direct-Mapped Cache: Each memory block maps to exactly one cache slot.

  • Pro: Simple hardware, fast lookup
  • Con: Conflict misses (two blocks competing for the same slot)

N-way Set-Associative Cache: Each memory block can go into any of N slots in a set.

  • Most common in real CPUs (L1: 4-way, L2: 8-way, L3: 16-way)

Fully Associative Cache: A block can go anywhere in the cache.

  • Pro: No conflict misses
  • Con: Expensive parallel lookup (used for TLBs)

Cache address breakdown (32-bit address, 4KB cache, 64B block, 4-way):

[Tag (20 bits) | Index (6 bits) | Offset (6 bits)]

Replacement Policies

  • LRU (Least Recently Used): Evict the block unused for the longest time. Best performance, complex hardware.
  • FIFO (First In First Out): Evict the oldest-inserted block. Simple.
  • Random: Evict a random block. Simple hardware, performance close to LRU in practice.

Write Policies

  • Write-through: Update both cache and memory on every write. Simple, high bandwidth usage.
  • Write-back: Update cache only; write to memory when the block is evicted (Dirty bit). Better performance, more complex coherence.

6. Virtual Memory

Overview

Virtual memory gives each process its own address space, providing isolation, protection, and the ability to overcommit physical memory.

Virtual Address (VA): Address used by the process (0x0000 to 0xFFFFFFFF)
Physical Address (PA): Actual DRAM address
Page: Fixed-size block of virtual/physical memory (typically 4KB)

Page Table Translation

Virtual Address [VPN | Page Offset]
             Page table lookup
Physical Address [PFN | Page Offset]

64-bit systems use a 4-level page table (x86-64):

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

TLB (Translation Lookaside Buffer)

Every page table lookup requires memory accesses. The TLB is a fully associative cache for recent virtual-to-physical translations.

TLB Hit:  VATLB lookup → PA  (1-2 cycles)
TLB Miss: VA → page table walk → PA  (100+ cycles)

Typical TLB size: 64-1024 entries. Must maintain 99%+ hit rate for good performance.

Page Replacement Algorithms

When physical memory is full, the OS must evict a page to disk.

  • Optimal: Evict the page that won't be used for the longest time (theoretical best, not implementable)
  • LRU: Evict the least recently used page (good performance, expensive hardware)
  • Clock (FIFO + second chance): LRU approximation used by most OS kernels

7. I/O Systems

I/O Control Methods

Polling: The CPU repeatedly checks device status registers.

  • Pro: Simple, low latency
  • Con: Wastes CPU cycles (busy-wait)

Interrupts: The device signals the CPU when ready.

  • Pro: CPU free to do other work
  • Con: Interrupt handling overhead (context save/restore)

DMA (Direct Memory Access): A DMA controller transfers data between memory and device without CPU involvement.

  • Essential for high-throughput transfers (disk, network, GPU)
  • CPU only initiates and acknowledges completion

Bus Architecture

PCIe 5.0:   x16 = 128 GB/s bidirectional
NVMe (PCIe): up to 7 GB/s sequential read (Gen4)
USB 4.0:    up to 40 Gbps
DDR5-6400:  51.2 GB/s per channel

8. Parallel Architecture

Flynn's Taxonomy

ClassInstruction streamsData streamsExamples
SISDSingleSingleSingle-core CPU
SIMDSingleMultipleGPU, AVX vector ops
MISDMultipleSingleFault-tolerant systems
MIMDMultipleMultipleMulti-core CPU, clusters

Multicore and Cache Coherence

When multiple cores hold copies of the same cache line, coherence must be maintained.

MESI Protocol (Modified, Exclusive, Shared, Invalid):

Modified (M):  This core holds the only (modified) copy
Exclusive (E): This core holds the only copy, matches memory
Shared (S):    Multiple cores hold read-only copies
Invalid (I):   This copy is stale (another core modified it)

State transitions:

  • Core A writes to a Shared line → A transitions to M, all others go to I (invalidation)
  • Core B reads an Invalid line → bus snooping causes A to supply the line

OpenMP Parallel Programming

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

// Parallel array summation
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 clause ensures thread-safe accumulation
    #pragma omp parallel for reduction(+:sum) schedule(static)
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }

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

    // Print thread IDs
    #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;
}
// Cache-friendly parallel matrix multiplication
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 Architecture

NUMA (Non-Uniform Memory Access) assigns local memory to each CPU socket.

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

Socket 0 accessing Socket 1 memory: ~160ns (2x latency)

Use numactl to pin processes to specific NUMA nodes for best performance.


9. GPU Architecture

GPU vs CPU Design Philosophy

AspectCPUGPU
Core countDozensThousands to tens of thousands
Core complexityVery high (OoO, branch prediction)Simple
Cache sizeLarge, multi-levelSmall, minimal
Design goalMinimize single-thread latencyMaximize throughput
Use caseGeneral-purpose serial workMassively parallel computation

SIMT Execution Model

SIMT (Single Instruction, Multiple Thread) is the GPU's core execution model.

Warp: 32 threads executing the same instruction (NVIDIA)
All 32 threads execute the same instruction simultaneously
Each thread operates on different data

Warp scheduler: On memory stall, immediately switch to another warp
Thousands of warps rotate rapidly, hiding memory latency

NVIDIA GPU Hierarchy

GPU
├── GPC (Graphics Processing Cluster) x 8
│   └── SM (Streaming Multiprocessor) x 7-12
│       ├── CUDA Core x 128 (FP32 operations)
│       ├── Tensor Core x 4 (matrix multiply, AI)
│       ├── RT Core x 1 (ray tracing)
│       ├── Warp Scheduler x 4
│       ├── Register File (256KB)
│       └── Shared Memory / L1 Cache (128-256KB)
└── L2 Cache (shared, tens of MB)

NVIDIA H100 (Hopper, 2022):

  • 132 SMs, 16,896 CUDA Cores
  • 528 Tensor Cores (4th generation, FP8 support)
  • 80GB HBM3, 3.35 TB/s memory bandwidth
  • 4 PetaFLOPS FP8 Tensor Core performance

CUDA Programming Basics

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

// GPU kernel: vector addition
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
    // Global thread index
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

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

    // Host (CPU) memory
    float *h_a = (float*)malloc(size);
    float *h_b = (float*)malloc(size);
    float *h_c = (float*)malloc(size);

    // Device (GPU) memory
    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, size);
    cudaMalloc(&d_b, size);
    cudaMalloc(&d_c, size);

    // Host → Device transfer
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);

    // Launch kernel: 256 threads per block
    int threadsPerBlock = 256;
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);

    // Device → Host transfer
    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 Memory Optimization with Shared Memory

// Tiled matrix multiplication using shared memory
#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++) {
        // Load tiles into shared memory
        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();  // Wait for all threads to finish loading

        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 Cores and AI Acceleration

Tensor Cores accelerate the matrix multiply-accumulate (MMA) operation in hardware.

Regular CUDA Core: 1 FP32 MAC per cycle
4th-gen Tensor Core: 16x16x16 matrix multiply = 4096 FP16 MACs per cycle

NVIDIA cuBLAS, cuDNN, and TensorRT automatically leverage Tensor Cores when the data sizes align.


Chiplet Design

Instead of one large monolithic die, chiplets connect multiple smaller dies on an interposer.

  • AMD EPYC (Genoa): 12 CCDs (Core Complex Die, 5nm) + 1 IOD (I/O Die, 6nm)
  • Intel Meteor Lake: separate CPU, GPU, SoC tiles
  • Benefits: Higher yield, process optimization per function, cost reduction
  • Technologies: TSMC CoWoS, Intel EMIB, UCIe standard

HBM (High Bandwidth Memory)

DRAM dies are stacked in 3D to achieve extreme memory bandwidth.

HBM3E (2024): 9.6 Gbps/pin, 8-stack = 1.2 TB/s per package
GDDR6X (RTX 4090): 21 Gbps/pin, 384-bit = 1 TB/s
DDR5-6400: 51.2 GB/s per channel (CPU)

NPU/TPU: AI-Dedicated Accelerators

  • Google TPU v5p: 459 TFLOPS BF16, mesh interconnect
  • Apple Neural Engine: iPhone 15 Pro, 35 TOPS INT8
  • Qualcomm Hexagon: Smartphone NPU, 75 TOPS
  • Intel Gaudi 3: 1835 TFLOPS BF16

The Rise of RISC-V

Open-source ISA expanding from low-power embedded to data-center servers:

  • Vendors: SiFive, StarFive, Alibaba T-Head
  • RISC-V International: 3,000+ member organizations
  • Official Linux 5.19 support, Android port complete

11. Performance Optimization in Practice

Cache-Friendly Programming

#include <stdlib.h>

#define N 4096

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

// Inefficient: column-major traversal (many cache misses)
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] strides across rows
}

// Efficient: loop tiling (reuses cache blocks)
#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 Vectorization with AVX2

#include <immintrin.h>  // AVX2 intrinsics

// Process 8 floats simultaneously with AVX2
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]);   // Load 8 floats
        __m256 vb = _mm256_loadu_ps(&b[i]);
        __m256 vc = _mm256_add_ps(va, vb);    // Add 8 floats at once
        _mm256_storeu_ps(&c[i], vc);          // Store 8 floats
    }
    // Handle remaining elements
    for (; i < n; i++)
        c[i] = a[i] + b[i];
}

12. Quiz

Q1. What causes a Data Hazard in a pipeline, and how can it be resolved?

Answer: A data hazard occurs when an instruction needs the result of a previous instruction that has not yet completed — specifically the RAW (Read After Write) hazard where the pipeline tries to read a register before a prior write has committed.

Solutions:

  • Forwarding (Bypassing): Route the EX/MEM stage output directly to the next EX stage input. Eliminates the stall in most cases.
  • Stall (Pipeline Bubble): Insert NOP cycles to wait for the result. Reduces throughput.
  • Code Reordering: Compiler inserts independent instructions between dependent pairs, hiding the latency.
  • Load-use hazard is a special case where one stall is unavoidable (MEM result cannot be forwarded to EX in time).
Q2. What is the difference between a Direct-Mapped cache and a 4-way Set-Associative cache?

Answer: In a direct-mapped cache, each memory block has exactly one possible location in the cache. In a 4-way set-associative cache, each block can be placed in any of 4 slots within its set.

Key differences:

  • Direct-mapped: susceptible to conflict misses (two frequently used blocks competing for one slot). Hardware is simple and fast.
  • 4-way SA: conflict misses greatly reduced. Requires a replacement policy (LRU). More hardware cost.
  • Real CPUs typically use 4-way or 8-way for L1, 16-way for L3. Fully associative is reserved for TLBs.
Q3. Explain the role of the TLB and the process that happens on a TLB miss.

Answer: The TLB (Translation Lookaside Buffer) is a fully associative cache that stores recent virtual-to-physical address translations, avoiding expensive page table walks on every memory access.

TLB miss handling:

  1. The VPN (Virtual Page Number) is not found in the TLB
  2. Hardware (x86) or software (RISC-V, MIPS) performs a page table walk
  3. Multi-level page tables are traversed in order: PML4 → PDPT → PD → PT
  4. The PFN (Physical Frame Number) is retrieved
  5. A new TLB entry is inserted, evicting an old one via LRU if necessary
  6. The original memory access is retried
  • Full miss handling takes hundreds of cycles — maintaining a 99%+ hit rate is critical.
Q4. What is warp divergence in a GPU and how does it affect performance?

Answer: Warp divergence occurs when threads within a warp (group of 32) take different branches of an if-else statement.

How it works:

  • In the SIMT model, all threads in a warp must execute the same instruction.
  • When an if-else diverges: the if-branch executes first with non-if threads masked off, then the else-branch executes with non-else threads masked off.
  • Both paths are executed serially, up to 2x slower in the worst case.

Mitigations:

  • Arrange data so threads in the same warp always take the same branch
  • Replace conditional logic with arithmetic (e.g., branchless select)
  • Minimize conditional code in kernel hot paths
Q5. Using Amdahl's Law, calculate the maximum theoretical speedup when 80% of a program is parallelized.

Answer: With an unlimited number of processors, the maximum speedup is 5x.

Calculation:

  • Parallelizable fraction: f = 0.8
  • Sequential fraction: 1 - 0.8 = 0.2
  • As s (parallel speedup) approaches infinity: f/s approaches 0
  • Speedup = 1 / ((1 - f) + f/s) = 1 / (0.2 + 0) = 1 / 0.2 = 5x

Implication: No matter how many cores are added, the 20% sequential portion caps the overall speedup at 5x. This is why eliminating sequential bottlenecks is more valuable than simply adding more parallelism.


References