[CS300 #087] 명령어 집합 구조 — RISC 와 CISC, 하드웨어와 소프트웨어의 계약서
컴퓨터공학 300 주제 시리즈의 087번째 글이다. 전체 지도는 여기.
한 줄 요약
명령어 집합 구조(ISA)는 CPU 가 이해하는 명령어, 레지스터, 메모리 접근 방식, 비트 인코딩을 정한 계약서다. RISC 는 단순하고 고정된 명령어로 하드웨어를 빠르게 만들자는 철학, CISC 는 명령어 하나가 많은 일을 하게 하자는 철학이었고, 오늘날 둘의 경계는 상당히 흐려졌다.
왜 필요한가
같은 C 코드라도 x86-64 서버와 ARM 맥북, 라즈베리 파이에서 서로 다른 기계어로 컴파일된다. 컨테이너 이미지를 빌드할 때 linux/amd64, linux/arm64 를 따로 만드는 이유가 ISA 가 다르기 때문이다.
ISA 는 소프트웨어와 하드웨어가 만나는 경계다. 위쪽(컴파일러, OS)은 ISA 만 믿고 코드를 만들고, 아래쪽(마이크로아키텍처)은 ISA 를 지키는 한 내부를 마음대로 바꿀 수 있다. 같은 x86-64 프로그램이 여러 세대의 CPU 에서 다시 컴파일하지 않고 돌아가는 것이 이 계약 덕분이다.
핵심 개념
ISA 가 정하는 것
| 항목 | 예시 |
|---|---|
| 명령어 종류 | 산술·논리, 적재·저장, 분기, 시스템 호출 |
| 레지스터 | 개수, 폭(32/64비트), 특수 레지스터(PC, 스택 포인터, 플래그) |
| 주소 지정 방식 | 레지스터, 즉시값, 베이스+오프셋, 인덱스 스케일 |
| 인코딩 | 명령어 길이, 비트 필드 배치 |
| 메모리 모델 | 바이트 순서, 정렬 요구, 메모리 순서 규칙 |
| 특권 수준 | 사용자 모드·커널 모드, 예외·인터럽트 처리 |
ISA 와 마이크로아키텍처를 구분해야 한다. ISA 는 “무엇을” 약속하고, 마이크로아키텍처는 그것을 “어떻게” 구현하는지(파이프라인 단수, 캐시 크기, 분기 예측기)다. 인텔과 AMD 는 같은 x86-64 ISA 를 서로 다른 마이크로아키텍처로 구현한다.
CISC 와 RISC 의 원래 주장
| CISC (예: x86) | RISC (예: RISC-V, AArch64) | |
|---|---|---|
| 명령어 길이 | 가변 (x86 은 1~15바이트) | 고정 (주로 4바이트) |
| 메모리 접근 | 산술 명령이 메모리 피연산자를 직접 다룸 | 적재(load)·저장(store) 명령만 메모리 접근 |
| 레지스터 수 | 상대적으로 적음 (x86-64 범용 16개) | 많음 (RISC-V, AArch64 모두 범용 31~32개) |
| 명령어 수와 복잡도 | 많고 복잡한 명령 포함 | 적고 단순 |
| 해독 | 복잡 | 단순, 파이프라인에 유리 |
1970년대에는 메모리가 비싸고 컴파일러가 단순했다. 명령어 하나가 많은 일을 하면 프로그램이 작아지고 어셈블리 작성도 편했다. 이것이 CISC 의 배경이다.
1980년 패터슨과 디첼은 실제 컴파일러가 복잡한 명령어를 거의 쓰지 않는다는 관찰을 바탕으로, 자주 쓰는 단순한 명령어만 빠르게 만들자는 RISC 를 주장했다. 고정 길이와 적재/저장 구조는 해독을 단순하게 하고 파이프라이닝(#090)을 쉽게 만든다.
같은 일, 다른 기계어
p[3] += x; 를 생각해 보자. x86-64 는 메모리 피연산자를 직접 고치는 add 하나로 끝난다(아래 “직접 해 보기” 에서 실측). RISC-V 는 적재–연산–저장 세 명령이 필요하다. 아래는 RISC-V 64비트 기준으로 손으로 쓴 대응 코드다(실행 확인은 하지 않았다).
# RISC-V (a0 = p, a1 = x)
ld t0, 24(a0) # t0 = p[3]
add t0, t0, a1 # t0 += x
sd t0, 24(a0) # p[3] = t0
ret
명령어 수는 RISC 가 많지만 각 명령어가 단순하고 모두 4바이트라 해독과 파이프라인이 쉽다. 반대로 CISC 는 코드 밀도가 높다.
경계가 흐려진 이유
- x86 의 내부는 RISC 처럼 동작한다. 현대 x86 CPU 는 복잡한 명령어를 내부에서 단순한 마이크로 연산(micro-op)으로 쪼개 실행한다. CISC 의 겉모습을 유지하면서 RISC 식 파이프라인을 쓰는 셈이다.
- RISC 도 복잡해졌다. SIMD, 암호화, 원자적 연산 같은 명령이 추가됐다. RISC-V 는 16비트 압축 명령(C 확장)으로 코드 밀도를 보완한다.
- 결국 성능은 마이크로아키텍처가 정한다. 캐시, 분기 예측, 비순차 실행이 ISA 차이보다 성능에 더 큰 영향을 주는 경우가 많다. ISA 차이는 해독기 복잡도, 전력, 코드 크기 같은 곳에서 남는다.
RISC-V: 열린 ISA
RISC-V 는 공개 사양으로 누구나 구현할 수 있는 ISA 다. 작은 기본 정수 명령어 집합(RV32I, RV64I) 위에 곱셈(M), 원자 연산(A), 부동소수점(F, D), 압축(C), 벡터(V) 같은 확장을 골라 붙이는 모듈 구조가 특징이다. 교육용으로도 인기가 높아 최근 교과서 상당수가 RISC-V 로 설명한다.
인코딩 엿보기: RISC-V I 형식
31 20 19 15 14 12 11 7 6 0
┌───────────────┬───────┬──────┬───────┬─────────┐
│ imm[11:0] │ rs1 │funct3│ rd │ opcode │
└───────────────┴───────┴──────┴───────┴─────────┘
12비트 5비트 3비트 5비트 7비트
필드 위치가 고정돼 있어서 해독기는 명령어를 다 읽기 전에도 레지스터 번호를 꺼낼 수 있다. 가변 길이인 x86 은 명령어의 시작과 끝을 찾는 것부터가 일이다.
직접 해 보기
1. x86-64 가변 길이 명령어 확인
x86-64 리눅스, gcc 13.3 에서 확인했다.
/* add.c */
void add_mem(long *p, long x) {
p[3] += x;
}
gcc -O2 -fcf-protection=none -c add.c -o add.o
objdump -d add.o
0000000000000000 <add_mem>:
0: 48 01 77 18 add %rsi,0x18(%rdi)
4: c3 ret
메모리 피연산자(0x18(%rdi), 곧 p + 24바이트)를 직접 고치는 add 가 4바이트, ret 은 1바이트다. 길이가 제각각이다. -fcf-protection=none 은 출력에 endbr64 명령이 끼지 않게 하려고 붙였다.
2. RISC-V 명령어를 손으로 인코딩
def rv_addi(rd, rs1, imm):
# RISC-V I-type: imm[11:0] | rs1 | funct3 | rd | opcode
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (0b000 << 12) | (rd << 7) | 0b0010011
def rv_decode_itype(word):
return {
"opcode": word & 0x7F,
"rd": (word >> 7) & 0x1F,
"funct3": (word >> 12) & 0x7,
"rs1": (word >> 15) & 0x1F,
"imm": (word >> 20) if not (word >> 31) else (word >> 20) - 4096,
}
w = rv_addi(5, 6, 10) # addi x5, x6, 10
print(f"{w:08x}", rv_decode_itype(w))
w = rv_addi(5, 6, -1) # addi x5, x6, -1
print(f"{w:08x}", rv_decode_itype(w))
출력(python3 로 확인):
00a30293 {'opcode': 19, 'rd': 5, 'funct3': 0, 'rs1': 6, 'imm': 10}
fff30293 {'opcode': 19, 'rd': 5, 'funct3': 0, 'rs1': 6, 'imm': -1}
즉시값 −1 은 12비트 2의 보수 0xFFF 로 들어간다(#082). 해독할 때는 최상위 비트로 부호를 확장한다.
현업에서는
- 멀티 아키텍처 이미지: x86 노드와 ARM 노드가 섞인 쿠버네티스 클러스터에서는 이미지가 해당 ISA 용으로 빌드돼 있지 않으면 파드가
exec format error로 죽는다.docker buildx로 여러 플랫폼을 함께 빌드하고 매니페스트 리스트로 묶는다. 노드의 ISA 는kubernetes.io/arch레이블로 확인하고 스케줄링에 쓸 수 있다. - ABI: ISA 위에는 호출 규약(인자를 어느 레지스터로 넘기는지)을 정한 ABI 가 있다. x86-64 리눅스는 System V psABI 를 따른다. C 확장 모듈이나 FFI 가 깨질 때 이 문서를 본다.
- 컴파일 대상 선택:
-march=native로 빌드한 바이너리는 빌드한 CPU 의 확장 명령(AVX2 등)을 쓸 수 있지만, 그 확장이 없는 다른 노드에서illegal instruction으로 죽을 수 있다. 배포 바이너리는 대상 범위를 명시해 빌드한다.
확인 문제
- ISA 와 마이크로아키텍처의 차이를 설명하라.
- 적재/저장 구조란 무엇인가.
- 고정 길이 명령어가 파이프라인에 유리한 이유는.
- 현대 x86 CPU 가 “겉은 CISC, 속은 RISC” 라고 불리는 이유는.
풀이
- ISA 는 소프트웨어가 볼 수 있는 명령어·레지스터·인코딩의 약속이고, 마이크로아키텍처는 그 약속을 구현하는 내부 설계(파이프라인, 캐시 등)다.
- 메모리에는 적재·저장 명령으로만 접근하고 산술 연산은 레지스터끼리만 하는 구조다.
- 다음 명령어의 시작 위치를 바로 알 수 있고 필드 위치가 고정돼 해독이 단순하고 빠르기 때문이다.
- 복잡한 x86 명령어를 내부에서 단순한 마이크로 연산으로 쪼개 RISC 식 파이프라인으로 실행하기 때문이다.
더 읽을거리 (References)
- RISC-V International, RISC-V Ratified Specifications
- RISC-V ISA Manual 소스 저장소
- System V Application Binary Interface — x86-64 psABI
- David A. Patterson, David R. Ditzel, “The Case for the Reduced Instruction Set Computer”, ACM SIGARCH Computer Architecture News 8(6), 1980.