컴퓨터 구조 03 - 명령어 집합 아키텍처 (ISA)
ISA의 역할과 CISC/RISC 철학, x86과 ARM의 설계 차이
하드웨어와 소프트웨어 사이의 계약
프로그래머가 작성한 코드는 결국 CPU가 이해하는 명령어로 변환된다. 그런데 CPU마다 이해하는 명령어가 다르다면 어떻게 될까? 컴파일러는 어떤 명령어를 생성해야 할지 알 수 없고, 운영체제는 하드웨어를 제어할 수 없을 것이다.
이 문제를 해결하는 것이 명령어 집합 아키텍처(Instruction Set Architecture, ISA)이다. ISA는 하드웨어와 소프트웨어 사이의 인터페이스를 정의하는 명세이다. 어떤 명령어가 존재하는지, 각 명령어의 형식은 어떠한지, 레지스터는 몇 개이고 어떤 역할을 하는지, 메모리 주소 지정은 어떤 방식으로 하는지를 규정한다. ISA가 동일하다면, 내부 구현이 완전히 다른 두 프로세서에서도 같은 프로그램이 동일하게 실행된다.
이것이 왜 중요한가? ISA 덕분에 하드웨어 설계자는 내부 마이크로아키텍처를 자유롭게 개선할 수 있고, 소프트웨어 개발자는 특정 하드웨어 구현에 종속되지 않는 프로그램을 작성할 수 있다. x86 ISA를 지원하는 프로세서라면 인텔이 만들든 AMD가 만들든 같은 바이너리가 실행된다. 이 추상화가 수십 년간 하드웨어와 소프트웨어가 독립적으로 발전할 수 있었던 기반인 셈이다.
명령어 형식
ISA에서 각 명령어는 고정된 비트 패턴으로 인코딩된다. 이 비트 패턴에는 어떤 연산을 수행할지(오피코드), 어떤 피연산자를 사용할지(레지스터 번호, 즉시값, 주소 등)가 포함된다.
MIPS나 RISC-V 같은 RISC 아키텍처에서는 명령어 형식이 몇 가지 유형으로 깔끔하게 정리되어 있다. RISC-V를 예로 들면 다음과 같다.
RISC-V 기본 명령어 형식 (32비트 고정)
R-type (레지스터 간 연산: add, sub, and, or 등)
┌─────────┬─────┬─────┬──────┬─────┬─────────┐
│ funct7 │ rs2 │ rs1 │funct3│ rd │ opcode │
│ 7비트 │5비트│5비트│ 3비트│5비트│ 7비트 │
└─────────┴─────┴─────┴──────┴─────┴─────────┘
I-type (즉시값 연산, 로드: addi, lw 등)
┌──────────────┬─────┬──────┬─────┬─────────┐
│ imm[11:0] │ rs1 │funct3│ rd │ opcode │
│ 12비트 │5비트│ 3비트│5비트│ 7비트 │
└──────────────┴─────┴──────┴─────┴─────────┘
S-type (스토어: sw, sb 등)
┌─────────┬─────┬─────┬──────┬────────┬─────────┐
│imm[11:5]│ rs2 │ rs1 │funct3│imm[4:0]│ opcode │
│ 7비트 │5비트│5비트│ 3비트│ 5비트 │ 7비트 │
└─────────┴─────┴─────┴──────┴────────┴─────────┘
B-type (분기: beq, bne 등)
┌────┬────────┬─────┬─────┬──────┬────────┬───┬─────────┐
│[12]│[10:5] │ rs2 │ rs1 │funct3│ [4:1] │[11]│ opcode │
└────┴────────┴─────┴─────┴──────┴────────┴───┴─────────┘
이 형식들의 공통점을 주목할 필요가 있다. 오피코드의 위치가 항상 하위 7비트에 고정되어 있고, 소스 레지스터(rs1, rs2)와 목적지 레지스터(rd)의 위치도 형식에 관계없이 일정하다. 이런 규칙성 덕분에 하드웨어 디코더가 단순해진다. 명령어를 해석하기 위해 복잡한 분기 로직이 필요 없이, 정해진 비트 위치에서 필요한 필드를 바로 추출할 수 있다.
주소 지정 방식
명령어의 피연산자가 어디에 있는지를 지정하는 방식을 주소 지정 방식(addressing mode)이라 한다. ISA마다 지원하는 주소 지정 방식이 다르며, 이 차이가 프로그래밍 모델과 컴파일러 설계에 큰 영향을 미친다.
| 주소 지정 방식 | 설명 | 예시 |
|---|---|---|
| 즉시(Immediate) | 피연산자가 명령어 자체에 포함 | addi r1, r2, 5 |
| 레지스터(Register) | 피연산자가 레지스터에 위치 | add r1, r2, r3 |
| 직접(Direct) | 메모리 주소가 명령어에 명시 | load r1, [0x1000] |
| 간접(Indirect) | 레지스터가 메모리 주소를 보유 | load r1, [r2] |
| 변위(Displacement) | 레지스터 + 오프셋으로 주소 계산 | load r1, [r2 + 16] |
| 인덱스(Indexed) | 베이스 + 인덱스 레지스터로 주소 계산 | load r1, [r2 + r3] |
RISC 아키텍처는 주소 지정 방식을 최소한으로 제한하는 경향이 있다. RISC-V에서는 메모리 접근이 로드와 스토어 명령어에서만 허용되며, 주소 지정은 변위 방식 하나만 사용한다. 이러한 제약이 오히려 하드웨어를 단순하게 만들고, 파이프라이닝을 용이하게 한다.
반면 x86은 매우 다양한 주소 지정 방식을 지원한다. 하나의 산술 명령어가 직접 메모리에서 읽어 연산하고 결과를 메모리에 쓸 수도 있다. 프로그래머 입장에서는 편리하지만, 하드웨어 구현은 그만큼 복잡해지는 셈이다.
CISC vs RISC
ISA 설계 철학에서 가장 근본적인 갈림길은 CISC(Complex Instruction Set Computer)와 RISC(Reduced Instruction Set Computer)의 대립이다.
CISC의 철학은 하나의 명령어가 복잡한 작업을 수행하도록 한다. 메모리에서 값을 읽어 연산하고 결과를 다시 메모리에 저장하는 것을 한 명령어로 처리할 수 있다. 명령어의 길이가 가변적이고, 종류가 매우 많으며, 하나의 명령어가 여러 클럭 사이클에 걸쳐 실행될 수 있다. x86이 대표적인 CISC 아키텍처이다.
RISC의 철학은 정반대이다. 각 명령어는 하나의 단순한 연산만 수행하고, 명령어 길이를 고정하며, 대부분의 명령어가 한 클럭 사이클에 실행되도록 설계한다. 메모리 접근은 로드/스토어 명령어로만 가능하고, 산술 연산은 레지스터 사이에서만 이루어진다. ARM, MIPS, RISC-V가 이 범주에 속한다.
과연 어느 쪽이 더 우수한 접근인가? 1980년대에 RISC가 등장했을 때, CISC 지지자들은 프로그램이 더 많은 명령어를 필요로 하므로 코드 크기가 커지고 메모리 대역폭이 낭비된다고 주장했다. RISC 지지자들은 단순한 명령어가 파이프라이닝에 유리하고, 각 명령어의 실행 시간이 예측 가능하여 하드웨어 최적화가 쉽다고 반박했다.
역사는 양쪽 모두에게 일부 타당성을 부여했다. 현대 x86 프로세서는 외부적으로는 CISC 명령어 집합을 유지하지만, 내부적으로는 복잡한 명령어를 마이크로옵(micro-op)이라는 RISC 유사 명령어로 분해하여 실행한다. 즉, 소프트웨어 호환성을 위해 CISC 인터페이스를 유지하면서도, 성능을 위해 RISC의 실행 원리를 차용하고 있는 것이다.
x86 vs ARM
CISC와 RISC의 대립은 x86과 ARM이라는 두 거대 ISA의 경쟁으로 구체화된다.
x86은 1978년 인텔 8086에서 시작하여 거의 50년간 하위 호환성을 유지해 온 아키텍처이다. 가변 길이 명령어(1바이트에서 15바이트까지), 복잡한 주소 지정 방식, 풍부한 명령어 집합이 특징이다. 데스크톱과 서버 시장을 오랫동안 지배해 왔으며, 그 최대 강점은 방대한 소프트웨어 생태계이다.
ARM은 1985년 Acorn에서 시작된 RISC 아키텍처이다. 32비트 고정 길이 명령어(ARM 모드), 조건부 실행, 로드/스토어 아키텍처가 특징이다. 단순한 명령어 구조 덕분에 전력 효율이 높아, 모바일과 임베디드 시장을 장악하고 있다. 최근에는 Apple Silicon과 AWS Graviton 등을 통해 데스크톱과 서버 영역으로도 진출하고 있다.
| 특성 | x86-64 | ARMv8 (AArch64) |
|---|---|---|
| 설계 철학 | CISC | RISC |
| 명령어 길이 | 가변 (1~15바이트) | 고정 (4바이트) |
| 범용 레지스터 | 16개 | 31개 |
| 메모리 접근 | 대부분의 명령어에서 가능 | 로드/스토어만 가능 |
| 명령어 인코딩 | 복잡, 프리픽스 다수 | 규칙적, 디코딩 용이 |
| 전력 효율 | 상대적으로 낮음 | 상대적으로 높음 |
| 소프트웨어 생태계 | 데스크톱/서버에서 압도적 | 모바일에서 압도적 |
과거에는 x86이 성능에서, ARM이 전력 효율에서 각각 우위를 점했지만, 이 구도는 변화하고 있다. Apple M 시리즈 프로세서는 ARM 기반이면서도 데스크톱급 성능을 보여주었고, 인텔과 AMD도 전력 효율 개선에 많은 투자를 하고 있다. ISA 자체보다는 마이크로아키텍처와 제조 공정이 성능과 효율을 좌우하는 시대가 된 셈이다.
RISC-V가 주목받는 이유
x86과 ARM은 모두 상용 ISA이다. 이 ISA를 사용하여 프로세서를 설계하려면 인텔이나 ARM Holdings에 라이선스 비용을 지불해야 한다. RISC-V는 이 구도에 도전하는 오픈소스 ISA이다.
2010년 UC 버클리에서 시작된 RISC-V는 누구나 자유롭게 구현할 수 있는 ISA를 목표로 설계되었다. 기본 명령어 집합은 극도로 간결하며(47개의 기본 명령어), 필요에 따라 확장을 추가하는 모듈식 구조를 채택하고 있다.
RISC-V가 주목받는 이유는 단순히 무료라서만은 아니다. ISA 설계의 교훈을 반영하여, x86이나 ARM이 역사적 이유로 안고 있는 설계상의 비효율을 피한 깔끔한 설계가 특징이다. 또한 모듈식 구조 덕분에 IoT 장치용 최소 구성부터 고성능 서버용 풀 구성까지 동일한 ISA 기반으로 구현할 수 있다.
현재 RISC-V는 임베디드와 IoT 분야에서 빠르게 채택되고 있으며, 장기적으로는 ARM의 영역을 일부 대체할 것으로 전망된다. 물론 소프트웨어 생태계라는 거대한 장벽이 남아 있지만, 오픈소스의 저력은 Linux가 증명한 바 있다.
명령어 인코딩의 실제
구체적인 예를 통해 명령어 인코딩이 어떻게 이루어지는지 확인해 보자. RISC-V에서 add x1, x2, x3 명령어를 인코딩하면 다음과 같다.
add x1, x2, x3 → R-type 형식
funct7 rs2 rs1 funct3 rd opcode
0000000 00011 00010 000 00001 0110011
│ │ │ │ │ │
│ x3 x2 ADD x1 OP(정수연산)
│
ADD를 나타냄
32비트 기계어: 0000000_00011_00010_000_00001_0110011
16진수: 0x003100B3
이 인코딩에서 각 필드의 위치가 고정되어 있다는 점이 중요하다. 하드웨어 디코더는 명령어의 종류를 파악하기 전에도 rs1, rs2, rd 필드를 미리 추출하여 레지스터 파일에 접근을 시작할 수 있다. 이것이 파이프라이닝 효율을 높이는 데 기여한다.
반면 x86에서 같은 의미의 명령어는 상황에 따라 인코딩 길이가 달라질 수 있으며, 프리픽스 바이트에 따라 해석이 바뀌기도 한다. 디코더 설계가 복잡해질 수밖에 없는 이유이다.
다음 글에서는 파이프라인과 병렬 처리를 다룬다.