본문 바로가기
TYLER SONG2026
블로그 목록
CS학습

Instruction: CPU가 이해하는 최소 실행 단위, 기계어 명령어

Instruction은 CPU가 직접 해독하고 실행할 수 있는 가장 작은 단위의 명령이며, opcode와 operand로 구성된 고정 또는 가변 길이의 이진 코드다. 명령어 집합 구조(ISA)는 소프트웨어와 하드웨어 사이의 계약이며, Fetch-Decode-Execute 사이클을 통해 실제로 실행된다. RISC와 CISC라는 서로 다른 설계 철학이 명령어

송민성7분 읽기

1. 개념

Instruction(명령어)은 CPU가 하드웨어적으로 직접 해독하고 실행할 수 있는 최소 단위의 작업 지시다. 우리가 작성하는 C, TypeScript, Python 코드는 결국 컴파일러나 인터프리터를 거쳐 이 명령어들의 나열로 변환된다.

명령어는 보통 다음 두 부분으로 구성된다.

  • opcode(연산 코드): 어떤 연산을 수행할지 지정. 예: 더하기(ADD), 저장(STORE), 분기(JUMP)
  • operand(피연산자): 연산에 사용할 데이터나 데이터의 위치(레지스터, 메모리 주소, 즉시값)

예를 들어 x86-64에서 ADD RAX, RBX라는 명령어는 "RAX 레지스터에 RBX 레지스터 값을 더하라"는 뜻이며, 실제로는 다음과 같은 이진 코드로 인코딩된다.

text
48 01 D8

이 3바이트 안에 opcode, 레지스터 지정, 연산 크기(64비트) 정보가 모두 압축되어 있다.

2. 왜 사용하는가

CPU는 고급 언어의 문법이나 의미를 전혀 알지 못한다. CPU가 이해할 수 있는 것은 오직 정해진 형식의 이진 패턴, 즉 명령어뿐이다. 따라서 어떤 프로그래밍 언어로 작성하든 결국 CPU가 지원하는 명령어 집합(Instruction Set Architecture, ISA)으로 변환되어야 실행이 가능하다.

ISA는 하드웨어 제조사(Intel, AMD, ARM 등)가 정의하는 규격이며, 이 규격을 따르는 한 소프트웨어는 하드웨어의 내부 구현(마이크로아키텍처)이 바뀌어도 그대로 실행된다. 즉 명령어는 소프트웨어와 하드웨어를 분리하는 추상화 경계 역할을 한다.

3. 동작 원리

3.1 명령어 사이클 (Instruction Cycle)

CPU는 다음 단계를 반복하며 프로그램을 실행한다.

  1. Fetch(인출): 프로그램 카운터(PC)가 가리키는 메모리 주소에서 명령어를 읽어온다.
  2. Decode(해독): 읽어온 이진 코드를 opcode와 operand로 분해하여 어떤 동작인지 판단한다.
  3. Execute(실행): ALU(산술논리연산장치) 등을 이용해 실제 연산을 수행한다.
  4. Write-back(결과 저장): 연산 결과를 레지스터나 메모리에 기록하고 PC를 다음 명령어로 이동시킨다.

3.2 명령어 형식과 주소지정방식

명령어는 고정 길이(RISC, 예: ARM은 대부분 32비트) 또는 가변 길이(CISC, 예: x86은 1~15바이트)로 인코딩된다. operand가 값을 가리키는 방식을 addressing mode라고 하며, 대표적으로 즉시 주소지정(immediate), 레지스터 주소지정(register), 메모리 직접/간접 주소지정 등이 있다.

3.3 RISC vs CISC

  • RISC(Reduced Instruction Set Computer): 명령어 종류를 단순화하고 길이를 고정해 파이프라이닝과 해독을 쉽게 만든다. ARM, RISC-V가 대표적이다.
  • CISC(Complex Instruction Set Computer): 하나의 명령어가 복잡한 동작(메모리 접근 + 연산)을 한 번에 수행할 수 있다. x86 계열이 대표적이다.

현대 x86 CPU는 내부적으로 CISC 명령어를 다시 RISC 스타일의 마이크로 연산(micro-op)으로 쪼개서 실행한다.

4. 코드 예제

4.1 x86-64 어셈블리 예제

asm
; sum.asm - NASM 문법 section .text global _start _start: mov rax, 3 ; RAX = 3 (즉시 주소지정) mov rbx, 4 ; RBX = 4 add rax, rbx ; RAX = RAX + RBX (레지스터 주소지정) ; exit(RAX) 시스템 콜 호출 mov rdi, rax mov rax, 60 syscall
shell
nasm -f elf64 sum.asm -o sum.o ld sum.o -o sum ./sum echo $? # 7 출력

4.2 Fetch-Decode-Execute 사이클을 흉내 낸 간단한 시뮬레이터

실제 CPU 동작 원리를 이해하기 위해 아주 단순화한 명령어 사이클을 Python으로 구현한다.

python
from dataclasses import dataclass, field @dataclass class CPU: registers: dict = field(default_factory=lambda: {"R0": 0, "R1": 0}) pc: int = 0 memory: list = field(default_factory=list) # 명령어 리스트 def fetch(self): instruction = self.memory[self.pc] self.pc += 1 return instruction def decode_execute(self, instruction): opcode, *operands = instruction if opcode == "LOAD": reg, value = operands self.registers[reg] = value elif opcode == "ADD": dest, src = operands self.registers[dest] += self.registers[src] elif opcode == "PRINT": reg = operands[0] print(f"{reg} = {self.registers[reg]}") elif opcode == "HALT": return False return True def run(self): running = True while running and self.pc < len(self.memory): instruction = self.fetch() running = self.decode_execute(instruction) # 프로그램: R0 = 3, R1 = 4, R0 = R0 + R1, 출력, 종료 program = [ ("LOAD", "R0", 3), ("LOAD", "R1", 4), ("ADD", "R0", "R1"), ("PRINT", "R0"), ("HALT",), ] cpu = CPU(memory=program) cpu.run() # 출력: R0 = 7

이 코드는 실제 하드웨어의 파이프라이닝, 캐시, 분기예측을 전혀 반영하지 않은 교육용 모델이지만, Fetch-Decode-Execute의 논리적 흐름 자체는 실제 CPU와 동일하다.

5. 시간 복잡도 또는 성능 특성

명령어 실행 성능은 다음 지표로 설명한다.

  • CPI(Cycles Per Instruction): 명령어 한 개를 처리하는 데 걸리는 평균 클럭 사이클 수. 단순한 RISC 명령어는 CPI가 1에 가깝고, 메모리 접근이 포함된 CISC 명령어는 CPI가 더 높다.
  • IPC(Instructions Per Cycle): 한 클럭 사이클당 처리하는 명령어 수. 슈퍼스칼라(superscalar) CPU는 파이프라인을 여러 개 두어 IPC를 1보다 크게 만든다.
  • 전체 실행 시간 = 명령어 수 × CPI × 클럭 주기(clock period)

파이프라이닝은 Fetch-Decode-Execute-Writeback 단계를 겹쳐서 처리해 이론적으로는 처리량을 단계 수만큼 늘릴 수 있지만, 분기(branch)가 있으면 파이프라인을 비워야 하는 분기 예측 실패(branch misprediction) 페널티가 발생한다. 이 페널티의 정확한 사이클 수는 마이크로아키텍처마다 다르므로 특정 수치로 단정하지 않는다.

6. 실무 사용 사례

  • 컴파일러 최적화: GCC, LLVM 같은 컴파일러는 소스 코드를 어떤 명령어 시퀀스로 변환할지 선택하며, 루프 언롤링(loop unrolling), 명령어 스케줄링(instruction scheduling)으로 파이프라인 효율을 높인다.
  • SIMD 명령어 활용: AVX, NEON 같은 벡터 명령어는 하나의 명령어로 여러 데이터를 동시에 처리해 이미지 처리나 수치 연산 성능을 크게 높인다.
  • JIT 컴파일: V8, JVM HotSpot 같은 런타임은 자주 실행되는 코드를 실시간으로 기계어 명령어로 변환해 인터프리터보다 빠르게 실행한다.
  • 프로파일링 도구: perf, Instruments 같은 도구는 실제로 어떤 명령어에서 CPU 사이클이 소모되는지, 분기 예측 실패율이 얼마인지 측정해 병목을 찾는다.

7. 주의할 점

  • 고급 언어의 반복문 하나가 겉보기엔 단순해 보여도, 조건 분기가 많으면 분기 예측 실패로 인해 예상보다 느려질 수 있다.
  • 정렬되지 않은 메모리 접근(unaligned access)이나 캐시 라인 경계를 넘나드는 데이터 구조는 추가 명령어 사이클을 유발할 수 있다.
  • ISA가 다르면(x86 vs ARM) 동일한 소스 코드라도 생성되는 명령어와 성능 특성이 달라지므로, 크로스 플랫폼 성능 최적화 시 이를 감안해야 한다.
  • 명령어 수준의 최적화(SIMD, 인라인 어셈블리 등)는 이식성을 해칠 수 있으므로 반드시 성능이 병목인 구간에서만 신중하게 적용한다.

8. 핵심 정리

Instruction은 opcode와 operand로 구성된, CPU가 직접 해독하는 최소 실행 단위이며 ISA를 통해 소프트웨어와 하드웨어를 연결한다. CPU는 Fetch-Decode-Execute-Writeback 사이클을 반복하며 명령어를 처리하고, RISC와 CISC라는 서로 다른 설계 철학이 명령어 형식과 실행 방식에 영향을 준다. 실무에서는 컴파일러 최적화, SIMD 활용, JIT 컴파일, 프로파일링을 통해 명령어 수준의 성능을 개선하지만, 분기 예측 실패나 메모리 접근 패턴 같은 하드웨어 특성을 이해하지 못하면 오히려 성능 저하를 초래할 수 있다.

© 2026 Tyler Song