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

ALU: 산술논리연산의 하드웨어 실행 단위

ALU(Arithmetic Logic Unit)는 CPU 내부에서 덧셈, 뺄셈 같은 산술 연산과 AND, OR 같은 논리 연산을 실제로 수행하는 조합 논리 회로(combinational logic circuit)다. 제어 유닛(Control Unit)이 보내는 opcode에 따라 여러 연산 중 하나를 선택해 실행하고, 결과와 함께 Zero, Carry, O

송민성7분 읽기

1. 개념

ALU(Arithmetic Logic Unit, 산술논리연산장치)는 CPU 내부에서 정수 산술 연산과 비트 단위 논리 연산을 수행하는 하드웨어 회로다. 입력으로 두 개의 피연산자(operand)와 연산 종류를 지정하는 제어 신호(opcode)를 받고, 출력으로 연산 결과와 상태 플래그(status flag)를 내보낸다.

레지스터나 캐시가 값을 저장만 하는 것과 달리, ALU는 값을 실제로 계산하는 유일한 실행 단위다. CPU의 명령어 실행 사이클(fetch-decode-execute) 중 execute 단계에서 산술/논리 연산 명령어를 만나면 항상 ALU가 동작한다.

text
┌────────────┐ A ─────▶│ │ │ ALU │────▶ Result B ─────▶│ │────▶ Flags (Zero, Carry, Overflow, Negative) └────────────┘ Opcode (연산 선택)

2. 왜 사용하는가

CPU가 실행하는 모든 산술/논리 연산(정수 덧셈, 비교, 비트마스킹, 시프트 등)은 결국 ALU 하나의 회로로 통합되어 처리된다. 연산마다 별도의 회로를 두지 않고 opcode로 선택하는 하나의 범용 회로를 두는 이유는 다음과 같다.

  • 회로 재사용: 가산기, 게이트 등의 하드웨어 자원을 여러 연산이 공유해 다이 면적(die area)을 절약한다.
  • 명령어 집합 확장 용이성: 새 연산을 추가할 때 ALU 내부에 연산 유닛과 opcode 매핑만 추가하면 된다.
  • 플래그 기반 제어흐름: 비교 연산(CMP), 조건 분기(JE, JG 등)가 ALU가 생성한 플래그를 그대로 활용한다.

3. 동작 원리

ALU는 여러 개의 하위 회로(가산기, 논리 게이트, 시프터)를 두고 멀티플렉서(MUX)로 opcode에 맞는 결과를 선택하는 구조다.

가산기 (Adder) 가장 기본이 되는 회로는 전가산기(Full Adder)를 비트 수만큼 직렬로 연결한 리플 캐리 가산기(Ripple Carry Adder)다. 각 비트의 캐리(carry)가 다음 비트로 전파되며, 뺄셈은 2의 보수(two's complement)를 이용해 A - B = A + (~B + 1)로 덧셈 회로를 재사용해 처리한다.

논리 연산 AND, OR, XOR, NOT은 각 비트 위치에 동일한 게이트를 병렬로 배치해 한 클럭 내에 전체 비트폭 결과를 만든다. 가산기와 달리 캐리 전파가 없어 지연시간이 짧다.

연산 선택 opcode 값에 따라 MUX가 가산기 결과, 논리 게이트 결과, 시프트 결과 중 하나를 최종 출력으로 내보낸다.

플래그 생성

  • Zero(Z): 결과가 0이면 1
  • Carry(C): 최상위 비트에서 캐리가 발생하면 1 (부호 없는 오버플로우 판단)
  • Overflow(V): 부호 있는 연산에서 부호 비트가 예상과 다르게 바뀌면 1
  • Negative(N): 결과의 최상위 비트(부호 비트)가 1이면 1

4. 코드 예제

하드웨어 회로 자체를 Python으로 만들 수는 없지만, ALU의 opcode 선택과 플래그 계산 로직은 그대로 시뮬레이션할 수 있다. 4비트 ALU를 예로 든다.

python
from enum import IntEnum BIT_WIDTH = 4 MASK = (1 << BIT_WIDTH) - 1 # 4비트 마스크: 0b1111 class OpCode(IntEnum): ADD = 0 SUB = 1 AND = 2 OR = 3 XOR = 4 NOT = 5 def to_signed(value: int, bits: int = BIT_WIDTH) -> int: """부호 없는 값을 2의 보수 기준 부호 있는 값으로 변환""" if value & (1 << (bits - 1)): return value - (1 << bits) return value def alu(a: int, b: int, opcode: OpCode) -> tuple[int, dict]: a &= MASK b &= MASK carry = 0 if opcode == OpCode.ADD: raw = a + b result = raw & MASK carry = 1 if raw > MASK else 0 elif opcode == OpCode.SUB: # 2의 보수를 이용한 뺄셈: A + (~B + 1) b_complement = (~b + 1) & MASK raw = a + b_complement result = raw & MASK carry = 1 if raw > MASK else 0 elif opcode == OpCode.AND: result = a & b elif opcode == OpCode.OR: result = a | b elif opcode == OpCode.XOR: result = a ^ b elif opcode == OpCode.NOT: result = (~a) & MASK else: raise ValueError("지원하지 않는 opcode") # 오버플로우 플래그: 부호 있는 연산에서 부호 비트가 예상과 다르게 바뀐 경우 signed_a, signed_b = to_signed(a), to_signed(b) signed_result = to_signed(result) if opcode == OpCode.ADD: overflow = 1 if (signed_a >= 0 and signed_b >= 0 and signed_result < 0) or \ (signed_a < 0 and signed_b < 0 and signed_result >= 0) else 0 elif opcode == OpCode.SUB: overflow = 1 if (signed_a >= 0 and signed_b < 0 and signed_result < 0) or \ (signed_a < 0 and signed_b >= 0 and signed_result >= 0) else 0 else: overflow = 0 flags = { "Zero": 1 if result == 0 else 0, "Carry": carry, "Overflow": overflow, "Negative": 1 if result & (1 << (BIT_WIDTH - 1)) else 0, } return result, flags # 실행 예시 result, flags = alu(0b0111, 0b0001, OpCode.ADD) # 7 + 1 = 8 (4비트 부호 있는 범위 초과) print(f"result = {result:04b}, flags = {flags}") # result = 1000, flags = {'Zero': 0, 'Carry': 0, 'Overflow': 1, 'Negative': 1}

0111(+7)0001(+1)을 더하면 4비트 부호 있는 정수의 표현 범위(-8~+7)를 벗어나므로 Overflow 플래그가 1로 세워진다. 실제 CPU에서 정수 오버플로우가 발생하는 원리와 동일하다.

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

ALU 자체는 소프트웨어 알고리즘이 아니라 회로이므로 빅오 표기법 대신 전파 지연(propagation delay) 관점으로 성능을 본다.

  • 리플 캐리 가산기(Ripple Carry Adder): n비트 가산기에서 캐리가 최하위 비트부터 최상위 비트까지 순차적으로 전파되므로, 전체 지연시간이 비트 수 n에 비례해 증가한다. 구조가 단순하지만 비트 폭이 커질수록(32비트, 64비트) 느려진다.
  • 캐리 선견 가산기(Carry Lookahead Adder): 각 비트의 캐리를 미리 병렬로 계산하는 추가 회로를 둬서 지연시간을 크게 줄인다. 게이트 수는 늘지만 지연시간이 비트 수에 거의 비례하지 않아 현대 CPU의 정수 가산기 대부분이 이 방식이나 변형을 사용한다.
  • 논리 연산(AND/OR/XOR): 비트별로 독립적인 게이트 연산이라 캐리 전파가 없고, 가산/감산보다 지연시간이 짧다.

CPU 클럭 주파수는 이 ALU를 포함한 실행 경로의 전파 지연시간에 의해 상한이 결정된다. 가산기 지연시간이 길면 클럭 사이클 하나를 더 길게 잡아야 하므로 클럭 주파수를 높이기 어렵다.

6. 실무 사용 사례

  • 정수 오버플로우 버그: 32비트 부호 있는 정수 연산에서 INT_MAX + 1이 음수가 되는 현상은 ALU의 Overflow 플래그가 세워지는 상황을 언어가 무시하고 결과값만 반환하기 때문에 생긴다. JavaScript는 정수를 배정도 부동소수점으로 다뤄 이 문제를 우회하지만 대신 Number.MAX_SAFE_INTEGER(2^53 - 1) 이후로는 정밀도 손실이 발생한다.
  • 비트 연산 최적화: 권한 플래그를 비트마스크로 관리하거나(flags |= PERMISSION_READ), 2의 거듭제곱 배수 연산을 시프트 연산(x << 1 대신 x * 2)으로 대체하는 최적화는 모두 ALU의 논리/시프트 유닛이 덧셈·곱셈보다 빠르다는 전제에서 나온다.
  • SIMD (Single Instruction Multiple Data): 현대 CPU는 하나의 ALU 대신 여러 개의 산술 유닛을 병렬 배치한 벡터 ALU를 둬서 여러 데이터를 한 클럭에 동시에 처리한다. 이미지 처리, 암호화 연산에서 성능 차이가 크게 난다.
  • 부동소수점은 별도 유닛: 3.14 + 2.71 같은 실수 연산은 ALU가 아니라 FPU(Floating Point Unit)가 담당한다. ALU는 정수와 비트 패턴 연산만 처리한다.

7. 주의할 점

  • ALU는 정수와 비트 연산만 처리하며, 부동소수점 연산은 FPU라는 별도 하드웨어가 담당한다. "ALU가 모든 계산을 한다"고 오해하면 안 된다.
  • Carry 플래그와 Overflow 플래그는 서로 다른 것을 의미한다. Carry는 부호 없는 정수 연산의 오버플로우를, Overflow는 부호 있는 정수 연산의 오버플로우를 나타낸다. 같은 덧셈 결과라도 두 플래그가 다르게 세워질 수 있다.
  • 리플 캐리 가산기의 지연시간이 비트 수에 비례한다는 사실은 하드웨어 설계 관점의 특성이며, 애플리케이션 코드 성능과는 직접 연결되지 않는다. 이를 근거로 "정수 크기를 줄이면 애플리케이션이 빨라진다"고 일반화하면 안 된다. 컴파일러와 CPU 파이프라인 최적화가 이미 이 문제를 흡수한다.

8. 핵심 정리

ALU는 CPU 안에서 산술 연산과 논리 연산을 실제로 수행하는 조합 논리 회로이며, opcode로 연산을 선택하고 결과와 함께 Zero, Carry, Overflow, Negative 플래그를 생성한다. 뺄셈은 2의 보수를 이용해 덧셈 회로로 재사용하고, 리플 캐리 가산기 대신 캐리 선견 가산기를 써서 비트 폭이 커져도 지연시간을 최소화하는 것이 현대 CPU 설계의 핵심이다. 애플리케이션 개발자 입장에서는 정수 오버플로우, 비트 연산 최적화, 정수와 부동소수점 연산이 서로 다른 하드웨어 유닛에서 처리된다는 점을 이해하는 것이 실무적으로 유용하다.

© 2026 Tyler Song