GDGoC 세션 발표: 왜 컴파일 언어는 빠르고 인터프리터 언어는 느릴까
GDGoC Konkuk 2025년 하반기 세션 발표자료입니다.
레포지터리 최상단에 슬라이드 파일이 업로드 되어있습니다.
- 주제: 왜 컴파일 언어는 빠르고 인터프리터 언어는 느릴까?
- 부제: Python의 내부 구현 분석을 통한 인터프리터 언어의 비용 이해와 C언어와의 구조적 차이
- 일시: 2025년 12월 2일
- 장소: 건국대학교 공학관 D동 X-Space
당시 시험기간으로 업로드하는 걸 잊어서 뒤늦게 업로드합니다...
많은 사람들 앞에서 발표하는게 처음이라 준비하느라 고생 깨나 했는데, 긴장을 엄청한 것 빼곤 별다른 문제 없이 발표를 끝마칠 수 있어 다행이었습니다.
다음엔 더 많은 사람들 앞에서 떨지 않고 발표할 수 있길...
발표 자료 PPT
발표 대본
1. 개요
1-1. 압도적으로 편리한 파이썬
여러분들은 어떤 프로그래밍 언어로 프로그래밍을 처음 시작하셨나요?
저는 C언어로 시작했습니다.
C언어는 고급 프로그래밍 언어들 중에서도 컴퓨터구조와 가장 밀접하게 연관된 언어예요,
그래서 그 내부 동작을 굉장히 직관적으로 알 수 있죠.
예를 들어,
변수를 선언할 때는 타입명을 명시해서 몇 바이트의 할당이 필요한지 알린다,
타입은 할당된 메모리를 해석하는 방식이기도 하다,
...
그래서 처음으로 파이썬을 접했을 때, 개인적으로는 많은 충격을 받았습니다.
슬라이드에 소개된 짧은 파이썬 코드는, 알고보면 매우 놀라운 코드입니다.
타입명을 적지 않아도 동작하니깐요.
이 코드는 42라는 숫자와 Hello GDGoC!라는 문자열을 출력해낼겁니다.
숫자와 문자열 타입을 알아서 식별하고, 연산까지 수행해내는 거예요.
(슬라이드 넘김)
파이썬은 굉장히 편리합니다.
이것들은 잘 알려진 파이썬의 장점들 중 일부예요.
근데 대체 어떻게 이게 가능한걸까요?
타입을 명시하지 않았는데
메모리 공간 할당을 얼마나 할지
메모리를 어떻게 해석할지 컴퓨터는 어떻게 알죠?
1-2. 이 편리함 뒤에 어떤 비용이 숨어져있을까?
옛날 개발자들이 바보가 아니란 말이죠.
'프로그래밍 언어'는 자연어와는 달리 인간이 인공적으로 설계한 언어입니다.
C언어가 매번 타입 선언을 하도록 설계가 된 건 분명 이유가 있습니다, 그것도 컴퓨터공학적으로 굉장히 타당한 이유죠.
근데 파이썬은,
그 '필수적인 이유'를 생략하고 있지 않나요?
어떻게 그것이 가능할까요?
(슬라이드 넘김)
하드웨어 아키텍처 자체에는 변함이 없기에 마법을 부리지 않는 이상 '그냥' 생략해버리는 건 불가합니다.
그런데 우리 개발자들은 요술봉은 없지만,
코드 더미로 비슷한 걸 할 수는 있거든요,
사진은 파이썬의 공식 구현체인 CPython의 레포지터리입니다.
이것이 바로 파이썬이 부리는 놀라운 마법의 실체입니다.
2. 언어 실행 방식 분류(Interpreted vs Compiled)
2-1. 컴파일 언어 vs 인터프리터 언어
컴파일 언어가 됐든, 인터프리터 언어가 됐든 결국 공통의 목적은
"주어진 소스 코드를 CPU가 실행할 수 있는 형태로 처리하는 것"
입니다.
그런데 그 해석 시점이 언제냐가 관건이에요.
(슬라이드 넘김)
(슬라이드 넘김)
사전적으로,
컴파일 언어란 소스 코드를 기계어로 미리 변환해 놓고, 그 결과물을 실행하는 방식의 언어입니다.
그리고
인터프리터(Interpreted) 언어란 “실행 과정에서 소스 코드를 해석하여 즉시 실행하는 방식의 언어”입니다.
둘의 절충안으로 VM 기반 컴파일 언어라는 것도 있는데, 오늘 발표에서는 VM 기반 컴파일 언어는 다루지 않습니다.
2-2. "컴파일 = 빠르다 / 인터프리터 = 느리다" ???
일반적으로 컴파일 언어는 빠르고, 인터프리터 언어는 느리다고 알려져있습니다.
(슬라이드 넘김)
그러나 컴파일이냐 인터프리터냐가 속도를 결정하는 직접적인 요인은 아니에요.
(슬라이드 넘김)
그보다는 해당 실행 방식을 지원하기 위해 언어가 어떻게 설계되고 구현되었느냐가 성능 상의 차이를 만들게 됩니다.
2-3. 오늘 비교할 대상: "C vs Python" 구조적 차이
오늘의 발표에서는 컴파일 언어에선 대표적으로 C를,
인터프리터 언어에서는 대표적으로 Python을 꼽아
둘을 비교해보는 시간을 가질 것입니다.
단순히 “Python은 인터프리터라서 느리고, C는 컴파일 언어라서 빠르다”가 아니라,
근본적으로 어떤 구조적 차이가 있길래 실행 속도에 차이가 발생하는 것인지 분석합니다.
(슬라이드 넘김)
유념하셔야 할 부분은,
컴파일과 인터프리터라는 구분으로 언어를 칼로 자르듯이 나눌 수는 없다는 겁니다.
어디까지나 일반적인 경향이라는 점을 유의해주세요.
3. 실행 과정 비교(Flow Diagram)
3-1. C와 Python 코드의 실행 과정 조감도
컴파일 언어에서는
소스 코드를 기계어 코드로 변환하고, 최종적으로 실행 파일을 만들어 해당 실행 파일을 CPU가 명령어 단위로 실행하는 것으로 실행이 이루어집니다.
인터프리터 언어에서는
소스 코드를 바이트 코드로 변환하고, VM 즉 버추얼 머신이 해당 바이트 코드를 받아 바이트 코드 명령 단위로 실행합니다.
둘의 가장 큰 차이점은 '기계어 코드의 생성 유무'입니다.
사실 인터프리터 언어더라도 결국 기계어로 처리가 될거예요,
프로세스를 돌리는 것은 CPU고,
CPU는 기계어만을 알기 때문입니다.
그렇지만 기계어 코드가 '생성'되지는 않아요.
그 비밀은 바로 VM에 있는데요, 먼저 C 코드의 실행 과정을 살펴본 후 이어 설명하겠습니다.
3-2. (1) C 코드의 실행 과정
C 코드를 실행하기 위해
소스 코드를 먼저 전처리해 텍스트 레벨에서의 작업을 마무리하고,
컴파일을 수행합니다.
파싱을 통해 컴파일러 내부 표현인 IR (Intermediate Representation)로 변환하고, 타입 체킹을 통한 최적화를 수행합니다.
최적화 버전을 바탕으로 기계어 코드가 생성이 되며,
빌드에 필요한 모든 파일들 그리고 외부 라이브러리와의 링킹을 통해 최종적으로 실행 파일이 만들어집니다.
이 실행 파일에는 실행에 필요한 명령어 리스트가 기계어로 적혀있어요.
CPU는 이 실행 파일에 적힌 기계어 코드를 그대로 실행합니다.
(동적 라이브러리 생략)
(어셈블리 코드 생성은 옵션이라는 내용 생략)
Python Virtual Machine (Python VM)
파이썬의 실행 과정을 보기에 앞서, Python 인터프리터에 대한 이해가 필요합니다.
파이썬 인터프리터는요, 여러분이 잘 알고계신 파이썬 실행파일 그 자체예요.
저희가 터미널에 python이라고 커맨드를 입력하면 환경 변수에 등록된 경로로부터 실행파일을 찾아 실행하는거잖아요? 바로 그 실행파일입니다.
파이썬 인터프리터는 컴파일러, VM, 런타임 모듈을 하나로 모아둔 실행파일입니다.
그리고 이 실행파일은, C 소스코드로 작성하여 C 컴파일러로 빌드한 결과물이에요!
여기 사진은
터미널에 python이라고 입력했을 때의 스크린샷인데요,
여기 보시면 GCC 15.2.0 64bit라고 적혀있죠?
이게 바로 이 python 실행 파일을 어떤 C컴파일러로 빌드해서 생성했는지
알려주는 겁니다.
(슬라이드 넘김)
cpython 레포지터리를 가보면 이 실행파일을 생성하는데 사용된 소스코드들을 볼 수 있는데
compile.c 얘가 컴파일러 소스 코드고
(슬라이드 넘김)
ceval.c
얘가 VM의 소스 코드입니다
3-3. (2) Python 코드의 실행 과정
Python 코드의 실행 과정은 크게 컴파일 단계와 실행 단계로 나뉘는데요,
컴파일 단계에서는 소스 코드를 토큰화하고, AST 즉 추상 구문 트리를 생성한 후 바이트코드를 생성합니다.
실행 단계에서는 VM이 바이트코드 해석 실행 루프를 돌면서 바이트코드를 하나씩 실행해나갑니다.
여기서 포인트는 한 줄의 Python 코드가 내부적으로는 여러 바이트코드로 변환된다는 것입니다.
마치 C 소스코드가 어셈블리 코드로 변환되는 것과 유사해요.
실행 단계에서 수행되는 CEval loop란,
C언어로 작성된 평가 루프입니다.
사실, 내부는 그냥 거대한 swich-case문이나 다름없어요.
바이트코드를 하나씩 분기 처리하고 실행하는 과정을 반복합니다.
이때 아주 많은 작업이 수반되는데, 뒷 내용에서 좀더 자세하게 보시게 됩니다.
파이썬 공식 라이브러리 중 'dis'라는 것이 있어요.
바이트코드 disassembler인데요,
바이트코드를 사람이 읽기 쉽게 출력해주는 라이브러리입니다.
어셈블리 코드랑 유사하죠?
한번 해보시면 꽤 재밌을 겁니다.
결국에는요,
전체 소스 파일에 대해 딱 한번 컴파일해서
바이트코드 더미를 만든 후
VM이 loop를 돌며 그것을 해석하고 처리하는 구조인 것입니다.
근데 VM이 이미 빌드가 완료된 실행 파일이니까 기계어는 생성이 되지 않는거죠.
이것이 인터프리터 언어가 기계어를 생성하지 않으면서도 CPU가 처리할 수 있게끔 하는 원리입니다.
3-4. C와 Python 코드의 실행 과정 특성
C와 파이썬의 실행 과정의 특성은 다음과 같은데요,
C언어는
'컴파일 타임에 최대한 많은 것을 결정하고 최적화해둔다,
따라서 런타임 연산이 상당히 줄어든다.'
그에 비해 Python은
'VM 프로세스가 루프를 돌면서 변환된 바이트코드를 하나씩 해석 및 실행하는 구조'
이기에
런타임 연산이 상당하다.
컴파일 타임은 반대로 C언어가 훨씬 오래 걸리겠죠.
최적화라든지, 할게 많으니까요.
(슬라이드 넘김)
즉 두 방식의 핵심 차이 중 하나는
컴파일에 걸리는 시간과 실행 시간의 trade-off이기도 합니다.
이 차이를 꼭 알아주세요.
4. 타입 시스템 & 객체 메모리 구조
4-1. 정적 타입 vs 동적 타입
정적 타입은 변수의 타입이 컴파일 시점에 확정되는 방식입니다.
즉 컴파일러가 타입 정보를 알고 있습니다.
그렇기 때문에, 순수한 plain 데이터만 존재하면 충분합니다.
그에 비해 동적 타입은 런타임에서 타입 결정이 이루어지는 방식입니다.
타입을 결정하려면 정보가 있어야겠죠?
그래서 plain 데이터만 있는게 아니라 메타 데이터가 추가로 필요합니다.
4-2. C의 데이터 구조: POD (Plain Old Data)
순수한 데이터만을 나열한 데이터 구조를 POD라고 불러요.
데이터가 메모리에 순서대로 나열되어있으니까,
offset 기반으로 접근하면 되겠죠?
C언어에서 좌측의 구조체를 토대로 객체를 생성하면
메모리 구조는 우측과 같을 겁니다.
만약 여기서 초록색에 해당하는 salary 멤버의 값을 읽고 싶다, 라고 하면 int 타입인 id의 4바이트와, char 배열인 Name의 10바이트를 건너뛰어 14바이트부터 읽기 시작하면 되는거죠.
(슬라이드 넘기기)
offset 기반으로 접근이 가능하다는 것은 꽤 중요합니다.
이렇게 메모리의 해석 방식을 컴파일 타임에 확정적으로 알 수 있다는 건,
기계어에 반영 가능하다는 얘기기도 하거든요.
이것이 바로 컴파일 언어가 효율을 향상하는 방법 중 하나입니다.
4-3. Python의 철학: “Everything is an object”
파이썬의 아주 중요한 철학이 있는데, 바로 '모든 것은 객체'라는 것입니다.
그래서 정수나 문자열 같은 '값'은 물론이고, '타입' 그 자체도 객체로 표현돼요.
내부 구조의 변경을 염두에 두기 때문에 모든 객체들은 힙에 동적으로 저장이 되고요,
모든 변수들은 힙에 저장된 객체의 주소를 참조합니다.
또한 모든 연산은 타입 객체에 등록된 메서드의 호출로 이루어집니다.
4-4. Python Object Model (Cpython)
파이썬의 객체 모델을 이해하기 위해 꼭 알아야하는 것은
PyObject 구조체입니다.
여기서 C언어의 포인터가 나올텐데요, 포인터는 다른 객체를 '참조'하기 위해 사용한다고 생각하시면 됩니다. 복잡하게 사용되지 않아요.
PyObject 구조체는 객체의 헤더 역할을 맡고 있습니다.
그래서 C언어 레벨에서 모든 파이썬 객체들은 PyObject를 내부적으로 갖고 있습니다.
여기 보시면
ob_type과 ob_refcnt라는 두 개의 멤버를 갖는데,
주요하게 봐야할 것은 ob_type입니다.
여기 보시면 ob_type의 자료형이 PyTypeObject 포인터인데요,
PyTypeObject가 뭐길래 참조하고 있을까요?
(슬라이드 넘김)
PyTypeObject란 타입 그 자체에 대한 정보를 저장하는 구조체입니다.
파이썬에서는 모든게 객체라서 값 뿐만 아니라 타입조차도 객체라고 했잖아요,
그겁니다.
예를 들어 파이썬의 리스트 타입도 PyTypeObject가 존재하겠죠? tp_name 필드에는 'list'라는 문자열이 들어갈테고요.
(슬라이드 넘김)
여기서 주목하셔야할 건 tp_dict입니다.
이건 딕셔너리 객체예요. 여러분들이 잘 알고 계신 그 파이썬의 딕셔너리 맞습니다.
tp_dict는 타입의 속성과 메서드를 저장합니다.
어? 근데 객체가 내부적으로 객체를 또 참조하고 있는 형태네요?
그럼 tp_dict을 이용해 검색을 한다고 하면,
(슬라이드 넘김)
우선 딕셔너리에 접근하기 위해 포인터를 따라가 임의 주소에 접근해야겠죠? 딕셔너리가 해시 테이블로 구현되어있으니까 해시 테이블 조회도 해야하고요.
게다가 상속 받은 클래스가 있다면 부모 클래스의 딕셔너리도 탐색해야합니다.
그래서...
(슬라이드 넘김)
파이썬에서 속성, 메서드를 접근하는 비용은 상당히 비쌉니다.
파이썬 코드 상으로는 그저 한 줄로 끝나는 코드가 내부의 C언어 구현상으로는 아주 많은 작업을 하고 있는거예요.
4-5. 정적 타입 vs 동적 타입 비용 비교
결론적으로 동적 타입의 구조적 비용을
세 가지 관점에서 정리할 수 있습니다.
첫번째 메모리.
파이썬은 모든 것을 객체로 취급하고
모든 객체는 PyObject 기반 모델을 채택하고 있기 때문에
메모리를 상대적으로 많이 씁니다.
두 번째 시간.
정적 타입은 offset으로 바로 접근 가능하지만,
동적 타입은 속성 조회를 하기 위해
런타임에 많은 작업을 처리해야해서
상대적으로 느립니다.
세 번째 최적화.
타입이 런타임에 결정되고, 속성 접근도 동적 연산이기 때문에
정적 타입 언어라면 가능한 여러 종류의 컴파일러 최적화가
동적 타입에선 불가능합니다.
따라서 최적화 측면에서도 불리합니다.
5. 디스패치 메커니즘
5-1. Dispatch(디스패치)란
디스패치란 실행 대상을 판정하고, 대상의 위치를 탐색하고, 실행하는 전체 과정을 통칭합니다.
실행 대상은 함수가 될 수도 있고, 속성도 될 수 있는데 여기서는 함수에 한정해서 설명하겠습니다.
판정 즉 resolution이 컴파일 타임에 이루어지냐 혹은 런타임에 이루어지냐에 따라서 정적 디스패치와 동적 디스패치로 구분합니다.
(슬라이드 넘김)
C언어와 파이썬
시간 비용 차이의 주된 원인이 바로 이 디스패치입니다.
5-2. 정적 디스패치 (Static Dispatch)
정적 디스패치는 간단히 말해
함수 주소가 이미 기계어에 박혀 있어서 CPU가 바로 점프를 할 수 있는 방식입니다.
컴파일 시점에 호출 대상이 확정되기 때문에
컴파일러가 기계어 명령어에 호출 대상의 주소를 삽입할 수 있는거죠.
그러니 CPU는 별도의 탐색 작업 없이 즉시 함수를 호출할 수 있습니다.
(슬라이드 넘김)
따라서 런타임 오버헤드가 사실상 없습니다.
5-3. 동적 디스패치 (Dynamic Dispatch)
동적 디스패치란 '런타임에 호출할 함수의 주소를 찾아 점프'하는 방식입니다.
호출 대상을 판정하고, 탐색하는 작업이 런타임에 이루어지게 되죠.
왜냐면 실행하기 전에는 호출 대상을 알 수가 없기 때문입니다!
(슬라이드 넘김)
동적 디스패치는 그 특성상 실행 중 추가 연산이 다량 발생하고, 이는 런타임 오버헤드로 이어집니다.
비용 관점에서 가장 주목해야할 것은 탐색, lookup 단계입니다.
lookup에는 다양한 방식이 있는데 좌측은 vtable 방식, 우측은 Name Lookup 방식입니다.
사실 동적 디스패치 자체는 파이썬과 같은 동적 타입 언어뿐만 아니라 C++과 같은 정적 타입 언어에서도 사용합니다.
그러나 탐색 비용에서 큰 차이가 있습니다.
함수 테이블 자체도 하나의 타입인데,
정적 타입이라면 이를 offset을 사용해 바로 접근할 수 있을 것이고
동적 타입이라면 동적으로 탐색을 수행해 위치를 찾아야하기 때문입니다.
앞서 보았지만, 파이썬의 딕셔너리 탐색은 시간 비용이 상당히 큰 작업입니다.
(슬라이드 넘김)
따라서, 동적 언어와 동적 디스패치의 조합에서 시간 비용은 폭발적으로 증가하게 됩니다. 파이썬은 바로 이 경우에 해당하는 것이죠.
5-4. 정적 디스패치의 최적화(Optimization)
시간 비용의 차이를 만드는 또 다른 요인은 최적화입니다.
정적 디스패치는 동적 디스패치에서는 할 수 없는 여러 최적화들이 있어요.
가장 효과가 큰 것이 인라이닝입니다.
인 라이닝은 말 그대로 라인 안에 함수 코드를 그대로 삽입하는 최적화입니다.
그렇게 된다면 런타임에서 함수를 호출할 필요가 없습니다. 스택 프레임이라든지, 함수 호출에 있어서 드는 비용 자체가 사라지게 되는거죠.
그리고 실행할 코드를 알고 있으니 해당 코드를 최적화하는 것도 가능합니다.
부가적으로는 분기 예측이 향상되도록 최적화를 하는 것도 있는데 인라이닝에 비해서 효과가 크지는 않습니다.
6. 결론
6-1. C vs Python 설계상의 차이
지금까지 저희는 C언어와 파이썬의 구조적 차이와 그 비용에 대해 알아보았습니다.
정리해보면 다음과 같습니다.
(슬라이드 넘김)
크게
타입 시스템의 차이,
오브젝트 모델의 차이,
디스패치 메커니즘 상에서의 차이가 존재합니다.
최적화를 할 수 있는 범위에서도 큰 차이가 있다는 것을 살펴봤고요.
본래 컴파일과 인터프리터 언어는 '실행 방식'에 의해 분류하는 것입니다.
그렇지만 그 실행 방식을 지원하기 위해 일반적으로 이러한 설계 상의 차이가 존재하고,
결국 런타임 속도를 포함해 많은 차이가 발생하게 되는 것입니다.
6-2. 컴파일 vs 인터프리터
컴파일 언어와 인터프리터 언어에는 각자의 장단점이 존재합니다.
흔히 컴파일 언어가 빠르다고 하지만,
그건 실행 준비 시간을 희생해서 최적화를 극도로 수행한 것이죠.
인터프리터 언어가 느리다고 하지만 실행 준비 시간은 아주 빠르고, 런타임에 코드 변경이 가능할 정도로 유연하고요.
격언 (?)
C와 파이썬에 대해 이런 유명한 구절이 있습니다.
C와 유닉스의 개발자인 데니스 리치는
“C는 괴팍하고, 결점도 있지만, 거대한 성공이다”
라고 했고,
그리고 여러 컴퓨터 언어 서적의 저자인 브루스 에켈은
“인생은 짧으니, 당신은 파이썬이 필요하다.”
라고 했어요.
이제 여러분은 왜 컴파일 언어는 빠르고 인터프리터 언어는 느린지, 어떤 설계 상의 차이가 존재하고 어떤 비용이 숨어져있는지 알고 있어요.
여러분의 선택은 무엇인가요?
이상으로 발표 마치겠습니다. 감사합니다.
PS. 20분 내외 분량으로 압축하느라 정말 힘들었습니다