인터프리터
인터프리터는 프로그램에 적힌 명령을 하나씩 읽어 그 뜻대로 실행해 주는 프로그램입니다. 미리 다른 언어로 옮겨 두는 단계를 두지 않습니다. 읽는 즉시 수행합니다.
쉽고 빠른 이해
프로그램에 적힌 명령을 하나씩 읽어 그 뜻대로 움직여 주는 프로그램입니다. 셸에 명령을 한 줄 치면 셸이 그 줄을 읽어 실행하는데, 그 셸이 인터프리터입니다.
미리 기계가 아는 형태로 옮겨 두면 옮긴 결과가 그 기계에 묶입니다. 기계가 바뀌면 다시 옮겨야 합니다. 인터프리터를 쓰면 기계마다 달라지는 것은 인터프리터 쪽이고 프로그램은 바꾸지 않아도 됩니다.
어떻게 도는가:
- 다음 명령을 하나 집어 옵니다
- 무슨 명령인지 알아냅니다
- 시키는 동작을 수행하고 1번으로 돌아갑니다
대가는 속도입니다. 같은 명령을 몇 번을 돌든 무슨 명령인지 알아내는 일을 그때마다 새로 합니다. 속도가 급한 곳에는 미리 옮겨 두는 쪽이 맞습니다.
상세
요리사가 조리법 카드를 한 줄씩 읽어 가며 그때그때 손을 움직입니다. 카드를 미리 자기 메모지에 옮겨 적어 둔 요리사도 냄비 앞에서는 그 메모를 한 줄씩 짚어 가며 손을 움직입니다. 같은 요리를 몇 번을 만들든 그때마다 처음부터 한 줄씩 다시 읽습니다.
인터프리터는 프로그램에 적힌 명령을 하나씩 읽어, 그 명령이 무엇을 시키는지 알아내고, 시키는 동작을 자기가 수행하는 프로그램입니다. 두 값을 더하라는 명령을 만나면 더해 놓는 것도, 화면에 찍으라는 명령을 만나면 찍는 것도 인터프리터가 합니다.
프로그램을 다루는 길은 크게 둘입니다. 하나는 한 언어를 다른 언어로 옮겨 놓는 것입니다. C 를 어셈블리어로 옮기는 것이 그렇습니다. 옮긴 결과가 나중에 실행하기 더 알맞은 꼴이 되리라는 뜻이 깔려 있고, 옮기는 단계가 뒤에 더 붙기도 합니다. 옮긴 끝이 바탕 하드웨어가 바로 실행할 수 있는 꼴, 곧 기계어면 그 프로그램은 CPU(Central Processing Unit, 중앙 처리 장치)가 곧바로 실행합니다. 인터프리터는 그 중간 단계들을 없앱니다. 옮기는 쪽이 하는 분석은 똑같이 하되 실행을 곧바로 합니다.
flowchart TD
subgraph K["옮겨 두는 길"]
C1["소스 프로그램"] --> C2["다른 언어로 옮긴다"]
C2 --> C3["옮기는 단계가 더 붙기도 한다"]
C3 --> C4["바탕 하드웨어가 바로 실행하는 꼴 · 기계어"]
C4 --> C5["CPU 가 곧바로 실행한다"]
end
subgraph P["곧바로 수행하는 길"]
I1["소스 프로그램"] --> I2["옮기는 쪽과 똑같은 분석"]
I2 --> I3["인터프리터가 곧바로 수행한다"]
end
읽고 수행하는 되풀이 고리
인터프리터 가운데는 소스를 먼저 다른 표현으로 옮겨 두고 그것을 읽는 것들이 있습니다. 그렇게 옮겨 둔 표현을 중간 표현이라고 합니다. 명령을 납작하게 한 줄로 늘어놓아 기계어와 비슷해진 중간 표현은 가상 머신 코드라고 따로 부릅니다.
가상 머신 코드를 도는 인터프리터의 본체는 되풀이되는 고리 하나입니다. 명령을 하나 집어 오고, 딸린 값이 있으면 그것도 읽고, 그 명령이 정한 동작을 수행합니다. 할 일이 남아 있는 한 이것을 되풀이합니다.
명령 하나를 처리하는 일은 셋으로 나뉩니다. 명령에 딸린 값을 읽는 것, 그 명령이 하는 일을 수행하는 것, 그리고 다음 명령을 집어 와 해독하고 실행을 시작하는 것입니다. 마지막 셋째를 분기(dispatch)라고 부릅니다.
flowchart TD
A["다음 명령을 집어 와 해독한다 · 분기"] --> B["딸린 값이 있으면 읽는다"]
B --> C["그 명령이 정한 동작을 수행한다"]
C --> D{"할 일이 남았나"}
D -->|남았다| A
D -->|없다| E["끝낸다"]
분기가 무는 비용
분기는 가상 머신 코드를 도는 모든 인터프리터에 공통이고, 인터프리터 실행 시간의 대부분을 차지할 수 있습니다. 인터프리터의 구조와 성능을 다룬 에르틀과 그레그의 연구가 이 비용을 쟀습니다. 잰 대상은 두루 쓸 성능을 겨냥해 만든 인터프리터이고, 잰 것은 그런 인터프리터가 수행하는 간접 분기의 수와 그것이 먹는 실행 시간입니다.
간접 분기는 뛸 곳이 코드에 박혀 있지 않고 실행 중에 값으로 정해지는 분기입니다. 이 연구가 쓴 벤치마크에서 간접 분기는 실행한 명령의 3.2~13% 였습니다. 시뮬레이션해 본 여러 구성에서 이 분기들이 실행 시간의 절반 넘게 썼습니다.
미리 옮겨 둔 프로그램은 더 빨리 돕니다. 기계어로 옮겨 두었다면 특히 그렇습니다. 인터프리터는 명령마다 분기 비용을 다시 치릅니다.
배경
프로그램을 돌려 보려면 먼저 기계가 아는 형태로 옮겨 두어야 했습니다. 옮긴 결과는 그 기계에 묶입니다. 기계 종류가 바뀌면 같은 프로그램을 다시 옮겨야 합니다. 프로그램을 한 줄 고칠 때마다 옮기는 단계를 한 번 더 지나야 하는 것도 부담이었습니다.
필요한 것은 프로그램 자체를 자료로 받아 그 뜻대로 실행해 주는 프로그램 하나였습니다. 그런 프로그램이 있으면 기계마다 달라지는 쪽은 그 프로그램이고, 옮겨 갈 프로그램은 손대지 않아도 됩니다. 기계에 매이지 않는 표현을 쓴다는 전제 아래, 다른 기계에서 돌리려면 인터프리터만 갖춰 주면 됩니다.
1960년 존 매카시는 기호식으로 함수를 정의하는 형식을 세우면서, 함수를 식으로 옮겨 적는
규칙을 먼저 만들었습니다. 함수 자체를 자료로 다룰 수 있게 하려는 것이었습니다. 그 위에
보편 함수 apply 를 정의하고, 이 함수가 보편 튜링 기계의 이론적 역할과 인터프리터의
실천적 역할을 한다고 적었습니다.
apply 는 함수와 인자를 받아 그 값을 나타내는 식을 만들고, 그 식을 계산하는 일은 eval 에
넘깁니다. eval 은 계산할 식 하나와 짝들의 목록 하나를 받습니다. 이 프로그래밍 시스템은
Advice Taker 라는 구상을 실험하려고 설계된 것이었습니다 — 기계가 명령문뿐 아니라 선언문도
다루도록 지시할 수 있게 하려는 구상이었습니다.
갈래
무엇을 「명령 하나」로 집어 오느냐가 갈래를 가르는 축입니다. 소스 프로그램을 되풀이해 파싱하는 비용을 피하려고, 효율을 겨냥한 인터프리터는 프로그램을 중간 표현으로 먼저 컴파일해 두고 그것을 해석합니다. 중간 표현을 해석하는 비용을 줄이려고는 명령을 납작하게 한 줄로 늘어놓은 가상 머신 코드를 씁니다. 그 가상 머신 코드에서 명령 하나가 1바이트 연산 부호로 시작하는 꼴을 바이트코드라고 부릅니다.
이 축의 한쪽 끝에는 만들기 쉬움이, 다른 쪽 끝에는 속도가 있습니다. OCaml 네이티브 코드 컴파일러가 OCaml 인터프리터보다 2~15배, 평균 6배 빠르다고 OCaml 쪽 자주 묻는 질문 모음이 전합니다. 그 수치를 인용한 연구는 같은 대목에서 반대쪽도 적습니다 — 그 바이트코드 인터프리터 본체는 1036줄이고, 대상 기계에 맞춘 코드를 하나도 두지 않습니다. 그러면서 네이티브 코드 컴파일러가 지원하는 대상 전부에서 돌고, 그 컴파일러가 지원하지 않는 몇 가지 대상에서도 돕니다.
소스 글자를 그대로 읽는 방식
소스에 적힌 글자를 그때그때 뜯어보며 실행합니다. 셸이 이 방식입니다. 셸은 입력을 읽어 낱말과 연산자로 토큰을 쪼갭니다. 그것을 단순 명령과 복합 명령으로 파싱하고, 명령의 부분마다 확장을 수행해 경로 이름과 필드의 목록을 만듭니다. 리다이렉션을 처리해 그 연산자와 피연산자를 인자 목록에서 걷어냅니다. 그러고 나서 함수나 내장 유틸리티나 실행 파일이나 스크립트를 실행하고, 선택에 따라 명령이 끝나기를 기다려 종료 상태를 거둡니다.
flowchart TD
A["입력을 읽는다"] --> B["낱말과 연산자로 토큰을 쪼갠다"]
B --> C["단순 명령과 복합 명령으로 파싱한다"]
C --> D["명령의 부분마다 확장을 수행한다"]
D --> E["리다이렉션을 처리한다"]
E --> F{"무엇을 실행하나"}
F --> G["함수"]
F --> H["내장 유틸리티"]
F --> I["실행 파일"]
F --> J["스크립트"]
G --> Z["선택에 따라 끝나기를 기다려 종료 상태를 거둔다"]
H --> Z
I --> Z
J --> Z
만들기 쉽다는 것이 이 방식이 주는 것입니다. 대신 같은 소스를 여러 번 도는 동안 뜯어보는 일을 매번 새로 합니다.
나무 꼴 중간 표현을 타고 도는 방식
중간 표현을 납작하게 늘어놓지 않고 나무 꼴로 두는 길도 있습니다. 소스를 나무 꼴 구조로 바꿔 둔 것, 곧 추상 구문 트리가 그런 중간 표현입니다. 효율을 겨냥한 인터프리터는 이 꼴 대신 납작하게 늘어놓은 가상 머신 코드를 고릅니다. 해석 비용이 더 적은 쪽이 납작한 배치이기 때문입니다.
바이트코드를 하나씩 도는 방식
소스를 바이트코드로 한 번 컴파일해 두고 그 명령을 하나씩 실행합니다. 명령 하나는 수행할 연산을 정하는 1바이트 연산 부호와, 그 연산이 쓸 인자나 자료를 대는 피연산자 0개 이상으로 이루어집니다. 피연산자가 없어 연산 부호 하나로만 된 명령도 많습니다.
명령이 번호로 정해져 있으니 무슨 명령인지 알아내는 일이 짧아집니다. 대신 소스를 옮기는 단계가 한 번 더 들어갑니다. 파이썬과 자바의 널리 쓰이는 구현이 이 방식입니다.
flowchart TD
S["소스 프로그램"] -->|"소스 글자를 그대로 읽는 방식"| L["명령을 하나씩 실행한다"]
S --> T["추상 구문 트리"]
T -->|"나무 꼴 중간 표현을 타고 도는 방식"| L
S --> B["바이트코드 · 가상 머신 코드"]
B -->|"바이트코드를 하나씩 도는 방식"| L
예시
CPython
파이썬 소스 코드는 바이트코드로 컴파일됩니다. 바이트코드는 CPython 인터프리터 안에서
파이썬 프로그램을 나타내는 내부 표현입니다. 이 바이트코드는 .pyc 파일에 캐시되어, 같은
파일을 두 번째 실행할 때는 소스에서 바이트코드로 다시 컴파일하는 일을 건너뜁니다.
바이트코드는 CPython 구현의 세부입니다. 판이 바뀌면 명령이 더해지거나 빠지거나 바뀌지 않는다는 보장이 없고, 다른 파이썬 가상 머신 사이에서 통하리라 기대해서도 안 됩니다. CPython 인터프리터는 요즘 실행 조건에 맞춰 바이트코드를 특화하도록 적응시키기까지 합니다.
POSIX 셸
POSIX(Portable Operating System Interface, 이식 가능 운영체제 인터페이스)는 sh 를 표준
명령 언어 인터프리터로 규정합니다. sh 는 명령줄 문자열이나 표준 입력이나 지정된 파일에서
읽은 명령을 실행해야 합니다.
자바 가상 머신
자바 가상 머신이 실행할 컴파일된 코드는 클래스 파일 형식으로 나타냅니다. 하드웨어와 운영체제에 매이지 않는 이진 형식이고, 대개 파일에 저장되지만 반드시 그런 것은 아닙니다. 자바 가상 머신은 자바 언어를 모르고 이 이진 형식만 압니다.
경계
실행 도중에 자주 도는 구간을 기계어로 옮겨 두고 다음부터는 그것을 쓰는 구현도 인터프리터인가. 맞습니다. 그런 구현이 인터프리트를 그만둔 것이 아닙니다.
근거는 둘입니다. 첫째, JIT(Just-In-Time, 적시) 컴파일은 프로그램이 실행을 시작한 뒤에 동적으로 이뤄지는 모든 번역을 넓게 가리키는 말입니다. 두 표현을 다 실행할 수 있는 체계(polyexecutable)는 어느 쪽 표현으로도 프로그램을 돌릴 수 있어서, 컴파일러를 부를 값어치가 있는 때를 고를 여유를 갖습니다. 인터프리트는 그 체계 안에 남아 있습니다.
stateDiagram-v2
state "인터프리트로 돈다" as A
state "그 구간을 기계어로 옮긴다" as B
state "그 구간은 기계어로 실행한다" as C
[*] --> A
A --> B: 컴파일러를 부를 값어치가 있다고 본 구간
B --> C
C --> A: 나머지 구간
둘째, 같은 명령 집합을 어떤 기술로 구현할지는 구현자에게 열려 있습니다. 자바 가상 머신은 본래부터 인터프리트되는 것이 아니고, 그 명령 집합을 실리콘 CPU 의 명령 집합으로 컴파일해 구현해도 똑같이 성립합니다. 마이크로코드로 구현하거나 실리콘에 직접 구현할 수도 있습니다. 명령을 기계어로 옮기는 내부 최적화는 구현자의 재량으로 남겨 두는 것이라고 못 박혀 있습니다.
그래서 판정의 대상은 언어가 아니라 구현입니다. 「파이썬은 인터프리터 언어다」는 그래서 엄밀하지 않습니다. 언어 레퍼런스는 언어를 기술하는 문서이고, 구현 세부를 너무 많이 넣는 것은 위험하다고 스스로 적습니다 — 구현은 바뀔 수 있고 같은 언어의 다른 구현은 다르게 동작할 수 있기 때문입니다. 파이썬에는 C 로 쓴 CPython 말고도 자바로 구현한 Jython, 파이썬 코드를 닷넷 어셈블리로 곧바로 컴파일하는 IronPython, 파이썬으로 쓴 PyPy 가 있습니다.
관련 항목
인터프리터가 읽어 들이는 입력 형태
소스 코드 · 바이트코드 · 추상 구문 트리 · 중간 표현 · 기계어 · 클래스 파일
인터프리터가 소스를 받아 거치는 처리 단계
어휘 분석 · 구문 분석 · 파싱 · 의미 분석 · 심볼 테이블 · 분기
같은 프로그램을 대신 실행에 올리는 다른 수단
컴파일러 · javac · JIT 컴파일 · AOT 컴파일 · 트랜스파일러 · 어셈블러 · 링커
인터프리터를 안에 품고 도는 실행 환경
가상 머신 · JVM · 런타임 · 운영체제 · 프로세스 · 셸
인터프리터로 주로 굴리는 프로그래밍 언어
Python · JavaScript · Ruby · Lua · PHP · Perl
인터프리터 노릇을 하는 실제 구현체
CPython · PyPy · V8 · Node.js · BEAM · Bash
인터프리터가 실행 중에 맡아 주는 관리 작업
가비지 컬렉션 · 동적 타이핑 · 예외 처리 · 스택 프레임 · 메모리 할당
인터프리터의 성능을 재는 지표
실행 속도 · 시작 시간 · 메모리 사용량 · 처리량 · 지연
인터프리터를 직접 붙잡고 쓰는 도구
REPL · 디버거 · 프로파일러 · 스크립트 · 패키지 관리자 · javap
인터프리터를 떠받치는 이론 바탕
형식 문법 · 프로그래밍 언어 · 튜링 완전성 · 메타순환 인터프리터 · 부트스트래핑
인터프리터가 다루는 하드웨어 자원
다른 이름: interpreter · 해석기