HW부터 Python 스크립트까지 — CPython 실행 구조
한 문장 요약
Python 코드 → CPython이 바이트코드로 컴파일 → CPython VM(C로 된 인터프리터)이 루프 실행 → OS → CPU → 트랜지스터
전체 실행 스택
┌─────────────────────────────────────────────────┐
│ Python 소스 코드 │
│ # hello.py │
│ def add(a, b): │
│ return a + b │
│ print(add(1, 2)) │
└──────────────────────┬──────────────────────────┘
│ python hello.py 실행
▼
┌─────────────────────────────────────────────────┐
│ CPython 인터프리터 시작 │
│ (CPython 자체는 C로 컴파일된 네이티브 바이너리) │
│ /usr/bin/python3 → ELF 바이너리 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 1단계: 렉싱 + 파싱 (Parsing) │
│ │
│ 소스 코드 → 토큰 스트림 → AST │
│ │
│ AST 예시 (add 함수): │
│ FunctionDef(name='add') │
│ └─ arguments: [a, b] │
│ └─ body: Return │
│ └─ BinOp(Add, Name(a), Name(b)) │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2단계: 바이트코드 컴파일 │
│ │
│ AST → 바이트코드 (.pyc 캐시 파일) │
│ __pycache__/hello.cpython-311.pyc │
│ │
│ add 함수 바이트코드 (dis.dis() 출력): │
│ ┌────────────────────────────────────────┐ │
│ │ LOAD_FAST 'a' │ │
│ │ LOAD_FAST 'b' │ │
│ │ BINARY_OP + │ │
│ │ RETURN_VALUE │ │
│ └────────────────────────────────────────┘ │
│ │
│ 바이트코드 = 1바이트 opcode + 피연산자 │
│ CPython VM이 이해하는 가상 명령어 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3단계: CPython VM 실행 루프 │
│ (Python/ceval.c 의 for 루프) │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ while (1) { │ │
│ │ opcode = *pc++; // 다음 바이트코드 │ │
│ │ switch (opcode) { │ │
│ │ case LOAD_FAST: ... │ │
│ │ case BINARY_OP: ... │ │
│ │ case RETURN_VALUE: ... │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────┘ │
│ │
│ CPython VM은 스택 기반 머신: │
│ ┌───────────────────────┐ │
│ │ Evaluation Stack │ │
│ │ ┌─────┐ │ │
│ │ │ 3 │ ← BINARY_OP │ a+b 결과 │
│ │ │ 2 │ ← LOAD_FAST │ b값 │
│ │ │ 1 │ ← LOAD_FAST │ a값 │
│ │ └─────┘ │ │
│ └───────────────────────┘ │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ GIL (Global Interpreter Lock) │
│ │
│ CPython의 핵심 제약: │
│ - 한 번에 하나의 스레드만 바이트코드 실행 │
│ - 멀티코어 CPU를 Python 스레드가 활용 못함 │
│ - I/O 대기 중에는 GIL 해제 (다른 스레드 실행) │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ CPU Core 0 CPU Core 1 │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │Thread 1 │ │Thread 2 │ │ │
│ │ │ 실행 중 │ │GIL 대기 │ ← 동시 X │ │
│ │ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 해결책: multiprocessing, asyncio, C 확장(GIL X)│
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ PyObject: 모든 것이 객체 │
│ │
│ Python의 정수 1 → C 구조체: │
│ ┌──────────────────────────┐ │
│ │ PyObject { │ │
│ │ ob_refcnt: 1 │ 참조 카운트 │
│ │ ob_type: &PyLong_Type │ 타입 포인터 │
│ │ ob_digit: [1] │ 실제 값 │
│ │ } │ │
│ └──────────────────────────┘ │
│ → 정수 하나에도 수십 바이트 메모리 사용 │
│ → C의 int(4바이트)보다 훨씬 큰 오버헤드 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ OS 시스템 콜 (필요 시) │
│ │
│ print() → sys.stdout.write() │
│ → 파일 디스크립터 1번에 write() │
│ → write(1, buf, n) 시스템 콜 │
│ → 커널이 터미널 드라이버 호출 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ CPU Fetch-Decode-Execute │
│ (CPython 자체의 C 코드가 실행됨) │
│ │
│ CPython VM의 switch문 → 네이티브 기계어로 실행 │
│ Python 바이트코드 1개 = C 코드 수십 줄 │
│ = 기계어 수백 개 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 트랜지스터 / 전기 신호 │
│ 0V / 3.3V → 논리 게이트 → ALU → 연산 결과 │
└─────────────────────────────────────────────────┘
.pyc 캐시 동작 원리
첫 실행:
hello.py → [컴파일] → __pycache__/hello.cpython-311.pyc
↓
매직 넘버 + 타임스탬프 + 바이트코드
두 번째 실행:
hello.py 변경 없음 → .pyc 타임스탬프 일치 → 파싱/컴파일 건너뜀
hello.py 변경됨 → .pyc 재생성
.pyc 구조:
┌──────────────────────┐
│ magic number (4B) │ CPython 버전 확인
│ flags (4B) │
│ timestamp (4B) │ 소스 수정 시간
│ source size (4B) │
│ marshalled bytecode │ 바이트코드 직렬화
└──────────────────────┘
성능 비교: Python vs 네이티브 코드
Python 정수 덧셈 a + b:
1. 바이트코드 opcode 해석 (VM 루프)
2. PyObject 타입 확인 (ob_type)
3. PyLong_Type.tp_as_number.nb_add() 호출
4. 새 PyObject 생성 + 참조 카운트 갱신
5. 스택에 결과 push
→ 약 수십 ~ 수백 나노초
C 정수 덧셈 a + b:
1. ADD 명령어 1개 실행
→ 약 1 나노초 이하
핵심 요약
hello.py
↓ CPython 렉싱/파싱
AST
↓ 바이트코드 컴파일
.pyc (바이트코드)
↓ CPython VM (ceval.c 루프)
C 함수 호출 (CPython 내부)
↓ OS 시스템 콜 (I/O 등)
커널
↓ CPU Fetch-Decode-Execute (CPython의 C 코드)
트랜지스터 ON/OFF (전기 신호)
핵심: Python 코드는 직접 CPU에서 실행되지 않는다.
CPython(C 바이너리)이 중간에서 해석하며 실행한다.