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 바이너리)이 중간에서 해석하며 실행한다.