Python Architecture & GIL
Python 아키텍처 및 GIL의 정의, 범위, 선행 지식, 학습 주제, 참고 근거를 정리한 CS&E 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
programming-languages-compilersprogramming-languagescompilersruntime-systemsmemory-managementpython-architecturegillanguages-compilers9 min read
1. Overview
Python 아키텍처와 GIL(Python Architecture & GIL)은 세계에서 가장 인기 있는 스크립팅 언어의 사실상 표준 구현체인 CPython의 내부 실행 구조를 다루며, Python이 왜 배우기 쉽지만 실행은 느린지, 그리고 GIL이 멀티코어 시대를 어떻게 제한해 왔는지를 정리합니다.
학습자는 소스 코드가 AST를 거쳐 바이트코드로 컴파일되고( .pyc ), C로 구현된 거대한 eval_frame_default 인터프리터 루프에서 실행되는 과정을 살펴봅니다. 이어서 Python 객체 모델의 핵심인 PyObject와 딕셔너리(dict) 기반 네임스페이스, 그리고 Python 동시성의 오래된 과제인 **GIL(Global Interpreter Lock)**의 탄생 배경과 멀티프로세싱/asyncio를 통한 우회 전략, 최근 Python 3.13의 Free-threading (No-GIL) 선언까지 Python 런타임의 변화 흐름을 정리합니다.
2. Scope & Boundaries
In-Scope
- CPython 아키텍처 (CPython Architecture): CPython 인터프리터 루프, 바이트코드 컴파일 과정,
.pyc캐싱. - 객체 모델 (Object Model):
PyObject구조(참조 계수, 타입 포인터), 모든 것이 객체(Everything is an Object),__dict__이름 공간. - 메모리 관리 (Memory Management): 참조 계수(Reference Counting) 기반 기본 GC, 순환 참조 탐지(Cyclic GC).
- GIL과 동시성 (GIL & Concurrency): GIL(Global Interpreter Lock)의 원리, CPU-bound vs I/O-bound 스레딩 효과,
multiprocessing모듈, PEP 703 (No-GIL).
Out-of-Scope
- 타 런타임 (PyPy, Jython, MicroPython): 대체 구현체 아키텍처 상세 → 비교 언급 정도로 제한.
- Python 문법/표준 라이브러리 활용: 언어 자체의 기능 사용법 → 언어 기본 영역.
Boundaries
- 스레드 vs 프로세스 (GIL 제약): Python
threading은 I/O 바운드 작업(네트워크 요청, 파일 읽기)에서는 GIL이 해제되어 동시성 이득을 얻지만, CPU 바운드 작업(행렬 연산, 암호화)에서는 GIL로 인해 싱글 코어 성능에 머뭅니다. CPU 바운드 작업을 병렬화하려면 반드시multiprocessing을 사용하여 프로세스 간 통신(IPC) 비용을 지불하거나 C 확장을 사용해야 합니다.
3. Counterexample
- 멀티스레딩으로 CPU 계산 가속화 시도 (CPU-bound Multithreading with GIL): 피보나치 수열 계산 같은 CPU 바운드 작업을 빠르게 하려고 Python
threading.Thread를 4개 생성하여 병렬 실행. 결과적으로 단일 스레드 실행보다 더 느려집니다(컨텍스트 스위칭 오버헤드 + GIL 경쟁). GIL로 인해 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있기 때문입니다. CPU 집약적 연산은 멀티스레딩이 아니라 멀티프로세싱을 써야 합니다. - Mutable Default Argument 버그 (Mutable Default Argument):
def append_to(element, target=[]): target.append(element); return target. 함수 선언 시점에target리스트 객체가 생성되고(네임스페이스에 바인딩), 이후 모든 함수 호출이 동일한 리스트 객체를 공유합니다.append_to(1)→[1],append_to(2)→[1, 2]. 모든 것이 객체이고 함수 정의 자체도 실행 시점의 객체 생성임을 이해하지 못해 발생하는 CPython 객체 모델의 고전적 함정입니다.
4. Prerequisites
- 참조 계수 GC (Basic): CPython의 메모리 관리가 참조 계수를 기반으로 함. (05-02-04 Garbage Collection & Memory Management)
- 가상 머신 기초 (Basic): 바이트코드와 인터프리터 루프 개념. (05-02-03 Virtual Machines & JIT Compilation)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 스택 기반의 거대한 C 스위치 문, CPython VM과 바이트코드 (CPython VM & Bytecode)
- Why to Learn: "Python 코드는 어떻게 실행되는가?"에 대한 답은 CPython 런타임의
ceval.c에 있는 거대한 루프에 있습니다. 컴파일 속도가 빠른 바이트코드 체계와, 그것을 인터프리팅하는 스택 머신을 이해하기 위함입니다. - What to Learn:
- Concepts: 소스 코드 → 파서(AST) → 컴파일러 → 바이트코드(
.pyc), 스택 기반 가상 머신(Stack Machine),dis모듈, 프레임 객체(Frame Object). - Skills:
dis모듈로 바이트코드 분석, 함수 호출 시 프레임 스택 동작 이해.
- Concepts: 소스 코드 → 파서(AST) → 컴파일러 → 바이트코드(
- How to Learn:
- 1단계: 바이트코드 컴파일:
__pycache__에 저장되는.pyc파일은 파싱 시간을 절약할 뿐 런타임 속도를 빠르게 하진 않음을 확인합니다. - 2단계: 스택 머신:
a = 1 + 2.LOAD_CONST 1,LOAD_CONST 2,BINARY_ADD,STORE_FAST a. 피연산자를 스택에 쌓고, 연산자가 스택에서 꺼내 계산 후 다시 쌓는 구조를 살펴봅니다.
- 1단계: 바이트코드 컴파일:
- Implement: 파이썬
import dis; dis.dis("a, b = b, a"). 튜플 언패킹(ROT_TWO바이트코드)이 임시 변수 할당보다 왜 효율적인지(가상 머신 스택 조작) 바이트코드 수와 동작 비교 출력. CPython의 거대 C 스위치문 모사 미니 파이썬 바이트코드 인터프리터 구현.
Recommended
Core Topic 02: 모든 것은 객체이고 모든 이름은 딕셔너리다, PyObject와 메모리 (PyObject & Memory)
- Why to Learn: "파이썬에는 변수가 없고 이름표(Tag)만 있다"는 말의 실체인 C 구조체
PyObject를 이해해야 동적 타이핑의 비용, 참조 횟수 메모리 관리, 얕은 복사/깊은 복사 버그를 명확히 진단할 수 있습니다. - What to Learn:
- Concepts:
PyObject(C 구조체:ob_refcnt,ob_type), 모든 것은 객체(Everything is an Object), 네임스페이스(globals(),locals()),id()(메모리 주소), 참조 계수(Reference Counting). - Skills:
id()비교와is연산자,sys.getrefcount(), 작은 정수 캐싱(Small Integer Caching).
- Concepts:
- How to Learn:
- 1단계: PyObject 구조: 모든 파이썬 값은 힙에 할당된 C 구조체입니다.
x = 42는42를 담은PyLongObject를 만들고 이름x를 그 주소에 바인딩합니다.y = x는 객체 복사가 아니라 포인터 복사(참조 계수 +1)임을 확인합니다. - 2단계: 네임스페이스: 클래스, 인스턴스, 모듈 등 모든 스코프는 내부적으로 C
dict(__dict__)입니다. 객체 속성 조회obj.attr은 해시 테이블 룩업과 동일하여 동적이지만 C/C++ 구조체 접근보다 느린 원인을 살펴봅니다.
- 1단계: PyObject 구조: 모든 파이썬 값은 힙에 할당된 C 구조체입니다.
- Implement: 파이썬
id()및is시뮬레이션.a = 256; b = 256; a is b (True)(작은 정수 캐싱 -5~256).a = 257; b = 257; a is b (False)콘솔 출력으로 싱글턴 캐싱 정책 증명. 인스턴스__dict__조작을 통해 동적으로 속성이 추가/제거되는Everything is a dict데모.
Practical
Core Topic 03: 코어 하나만 씁니다, GIL의 탄생과 한계 (Global Interpreter Lock)
- Why to Learn: 멀티코어 서버에서 CPU 사용률이 100%(코어 1개)를 넘지 못하는 파이썬의 병목, GIL(Global Interpreter Lock)이 왜 만들어졌고 정확히 무엇을 막는지를 이해하기 위함입니다.
- What to Learn:
- Concepts: GIL(Global Interpreter Lock), CPython 스레드 안전성(Thread Safety for C API), 참조 계수 경쟁 조건 방지, 컨텍스트 스위칭 비용.
- Skills: CPU-bound 작업과 I/O-bound 작업 식별, GIL 해제 조건.
- How to Learn:
- 1단계: GIL의 존재 이유: CPython 메모리 관리는 참조 계수(
ob_refcnt)를 씁니다. 여러 스레드가 동시에x를 참조하면 참조 계수 업데이트 시 Race Condition이 발생하여 메모리가 누수되거나 크래시됩니다. 모든 객체에 락을 거는 대신 인터프리터 전체에 단 1개의 큰 락(GIL)을 거는 해결 방식을 살펴봅니다. - 2단계: I/O와 GIL 해제: 네트워크 대기,
time.sleep(), 파일 읽기 등 I/O 대기 중에는 CPython이 자발적으로 GIL을 해제(Release)하여 다른 스레드가 파이썬 코드를 실행할 수 있게 합니다. 즉, I/O 바운드 작업에서는 파이썬 멀티스레딩이 유효함을 살펴봅니다.
- 1단계: GIL의 존재 이유: CPython 메모리 관리는 참조 계수(
- Implement: 파이썬 스레딩 벤치마크.
count_to_100_million()(CPU-bound) 단일 스레드 실행 시간 vs 4개 스레드 분할 실행 시간 비교 (멀티스레드가 더 느림 증명).fetch_url_sleep()(I/O-bound) 단일 스레드 순차 실행 시간 vs 4개 스레드 병렬 실행 시간 비교 (멀티스레드가 약 4배 빠름 증명).
Advanced
Core Topic 04: GIL을 우회하고 마침내 제거하다, 병렬성과 No-GIL (Concurrency Workarounds & PEP 703)
- Why to Learn: GIL의 제약 아래서 실제 데이터 사이언스 워크로드를 병렬화하는 C 확장/멀티프로세싱 우회 기법을 익히고, Python 3.13부터 도입되는 "GIL 없는 파이썬(Free-threading)" 변화가 시스템에 미칠 영향을 이해하기 위해서입니다.
- What to Learn:
- Concepts:
multiprocessing(IPC 비용과 프로세스 분리), C 확장/Cython/Numba의 GIL 해제(with nogil), PEP 703(Making the Global Interpreter Lock Optional in CPython), 바이어스 참조 계수(Biased Reference Counting), 밈 멀티스레딩(Mimalloc). - Skills:
ProcessPoolExecutorvsThreadPoolExecutor선택 기준.
- Concepts:
- How to Learn:
- 1단계: GIL 우회: CPU 바운드 작업을 병렬화하려면 1)
multiprocessing으로 여러 CPython 프로세스를 띄우거나 (객체 직렬화/역직렬화 통신 오버헤드 존재), 2) Numpy 내부 C 코드처럼 연산 중 명시적으로 GIL을 해제하고 C 레벨에서 병렬 처리하는 전략을 살펴봅니다. - 2단계: PEP 703 (No-GIL): GIL을 제거하면 참조 계수를 원자적(Atomic) 연산으로 바꿔야 하므로 싱글 스레드 성능이 10~15% 하락합니다. 이를 극복하기 위해 객체 접근 스레드에 락 소유권을 편향(Biased)시키는 최신 런타임 락-프리 최적화 기법을 살펴봅니다.
- 1단계: GIL 우회: CPU 바운드 작업을 병렬화하려면 1)
- Implement: CPU 병렬화 벤치마크. 행렬 곱셈 시뮬레이션을
concurrent.futures.ThreadPoolExecutor(GIL 블로킹 확인) vsProcessPoolExecutor(멀티코어 활용, IPC 오버헤드 측정) 시간 비교. NumPy 벡터화 연산 실행 시 내부 C 확장 코드에서 GIL이 자동 해제되어 다중 코어를 활발히 사용하는 Htop 캡처 데모.
7. Terminology
8. References
Primary
- [P1] CS2023 - Programming Languages (PL) - Runtime Systems
- [P5] SFIA - Software Development (PROG) - Performance Tuning
Secondary
- [Fluent Python] Luciano Ramalho - Pythonic Data Model and Metaprogramming
- [Effective Python] Brett Slatkin - Concurrency and Parallelism (GIL)
Industry
- [Python Developer's Guide] - CPython Internals & Global Interpreter Lock
- [PyPy Documentation] - Just-in-Time Compilation in PyPy
9. Final Checklist
Primary
- CPython 아키텍처에서 소스 코드(
.py)가 바이트코드(.pyc)로 컴파일된 후 가상머신(PVM) 위에서 한 줄씩 인터프리팅되는 물리적 과정을 설명할 수 있는가? - 파이썬의 동적 타입 검사(Dynamic Typing)와 덕 타이핑(Duck Typing) 철학이 컴파일 언어의 정적 타입 시스템(Static Typing) 대비 갖는 개발 속도 상의 이점을 논증할 수 있는가?
Secondary
- 파이썬 프로그램에서 CPU 바운드 작업을 할 때 스레드(Thread)를 늘려도 속도가 전혀 빨라지지 않는 이유를 GIL(Global Interpreter Lock)의 CPython 구조적 한계로 설명할 수 있는가?
- GIL의 한계를 극복하기 위해
multiprocessing모듈을 사용하여 프로세스를 나누는 우회 기법의 메모리 오버헤드(IPC 비용)를 분석할 수 있는가?
Industry
- I/O 바운드(네트워크 통신 등) 작업에서는 GIL이 대기 상태에서 풀리므로, 스레딩이나
asyncio코루틴이 여전히 강력한 성능을 내는 원리를 설계할 수 있는가? - 순수 수학 연산이 많은 파이썬 코드를 실행할 때, CPython 대신 JIT 기반의 PyPy를 도입하거나 C 모듈(NumPy, Cython)로 병목을 오프로딩하는 아키텍처 결정을 내릴 수 있는가?