프로그램 언어를 해석하고 실행시키는 대표적인 방법으로 Compile 과 Interpret 방식이 있다.

Compile 작업은 Compiler 에 의해 실행되고, Interpret 작업은 Interpreter 에 의해 실행되는데, 두 컨셉이 명확하게 다르기 때문에 

많은 프로그래밍 언어들은 둘 중 한가지 방식을 통해 언어를 실행하도록 설계된다. (Java 와 같이 두가지를 모두 채용하는 경우도 있다!)

그렇기 때문에 Compiler 와 Interpreter 를 이해하는 것은 어떤 언어를 배우던지간에 해당 언어의 구동원리를 배울 수 있는 중요한 선행학습이라할 수 있겠다.


컴파일 (Compile)

프로그래밍 언어를 Runtime 이전에 기계어로 해석하는 작업 방식이다.
이때 원래의 소스를 원시 코드, 바뀐 코드를 목적 코드(Object Code) 라 한다.

런타임 이전에 Assembly 언어로 변환하기 때문에 구동 시간이 오래걸리지만, 구동된 이후는 하나의 패키지로 매우 빠르게 작동하게 된다.
구동시에 코드와 함께 시스템으로부터 메모리를 할당받으며 할당받은 메모리를 사용하게 된다.

런타임 이전에 이미 해석을 마치고 대게 컴파일 결과물이 바로 기계어로 전환되기 때문에 OS 및 빌드 환경에 종속적이다.
그러므로 OS 환경에 맞게 호환되는 라이브러리와 빌드환경을 구분해서 구축해줘야 한다.

Compile 언어의 대표격으로 C / C++ 와 같은 언어들을 들 수 있으며, Java 역시 Byte Code 로 바꾸기 위한 과정에서 컴파일을 수행한다.


인터프릿 (Interpret)

런타임 이전에 기계어로 프로그래밍 언어를 변환하는 컴파일 방식과 다르게, 런타임 이후에 Row 단위로 해석(Interpret) 하며 프로그램을 구동시키는 방식이다.

프로그래밍 언어를 기계어로 바로 바꾸지않고 중간 단계를 거친 뒤, 런타임에 즉시 해석하기 때문에 바로 컴팩트한 패키지 형태로 Binary 파일을 뽑아낼 수 있는 Compile 방식에 비해 낮은 퍼포먼스를 보이게 된다.

런타임에 직접 코드를 구동시키는 특징이 있기 때문에 실제 실행시간은 느리며, 대신 런타임에 실시간 Debugging 및 코드 수정이 가능하다.

또한 메모리를 별도로 할당받아 수행되지 않으며, 필요할 때 할당하여 사용한다. 이와 관련되어 코드의 흐름 자체도 실제 필요할 때, 실제 수행되어야하는 시점에 수행되기 때문에 덕타이핑(Duck Typing) 이 가능한 측면이 있으나, 반대로 정적 분석이 되지않는 Trade off 를 갖고 있다.

 

대표적인 Interpreter 언어로는 Javascript 와 같은 스크립팅 언어들이 있다. 하지만, 스크립트 언어 뿐 아니라 컴파일 이후의 동작에서 Interpret 을 수행하는 언어들도 많이 존재한다.


많은 프로그래밍 언어들의 인터프리터는 해석을 위한 Virtual Machine 을 두고, Machine 위에서 Interpret 을 수행하게 되는데, 이 때 해석의 기반이 되는 머신들이 OS 환경들을 지원해줌으로써, 해당 방식으로 인터프리터는 OS 및 플랫폼 에 종속되지않는 프로그램 구동이 가능하게 된다.
(이런 특징을 지닌 Interpreter 는 Java 의 JVM 과 Python 의 Analyzer 가 있겠다.)


컴파일러와 인터프리터의 차이는 잘 이해하고 언어와 환경을 파악하는데 활용하는 것이 중요하다.


컴파일 과정은 사람이 이해할 수 있는 High Level Programming Language 로 구성된 소스코드를 기계가 이해할 수 있는 Lower Level Language 로 바꾸는 과정이다.


컴파일러는 다음과 같은 과정을 통해 컴파일을 수행한다.


[출처 : https://www.programcreek.com/2011/02/how-compiler-works/]


위의 그림은 컴파일의 단계를 간략하게 설명한다. 다음은 그림에 대한 설명이다.


(1) Lexical Analysis 

소스코드를 Token 으로 분할한다. 모든 키워드와 Parenthesis, 변수들 및 괄호들을 분리해낸다.


(2) Syntax Analysis

앞선 단계에서의 스캔으로 만들어진 토큰들(Token Stream)의 문법을 분석하기 위한 자료구조료 변형한다.

이렇게 만들어지는 자료구조를 Parse Tree 라고 한다.

이 단계에서는 Token 이 Valid 한지 검출하지 못하며, Token 이 사용되기 이전에 정의 또는 초기화되어있는 지 등 정적분석은 불가능하다.


또한 이단계에서 파싱이 일어난다. Parsing 작업은 Top-Down, Bottom-Up 두가지 방식으로 나뉜다.

Top-Down Parsing 은 Parse Tree 의 윗쪽부터 파싱을 수행하며, Bottom-Up Parsing 은 트리의 아래쪽부터 파싱이 수행된다.


유명한 파서의 종류로 Top-Down 방식의 Non-Backtracking Predictive Parser 인 LL Parser와 Bottom-Up 방식의 LR Parser 가 존재한다.


(3) Semantic Analysis

각 기호들 및 구문들을 의미있는 값들로 변경한다. 가령 기호 < 는 bool 을 Return 하는 Operand 함수로,

While 과 같은 키워드는 반복 구조를 이루는 void 함수로, 각 변수는 메모리로 치환한다.

이 단계에서 Type 의 Mismatch 나, 변수의 미정의, 파리미터 미정의 등 문법적 요소들이 검증된다.


(4) IR Generation

구문 분석으로 이루어진 자료를 중간 언어로 변경하는 작업을 수행한다.

IR 은 Intermediate Representation 의 약어로 소스코드에 근접한 기계어인 High Level IR, 타겟 머신에 종속적으로 디자인된 Low Level IR 이 존재한다.

컴파일러는 소스코드를 High Level IR -> Low Level IR 로 변경한 뒤 Target Machine Code 로 해석한다.


(5) IR Optimization

중간 언어를 최적화한다. 불필요한 루프를 없애거나, 사용하지 않는 변수의 정리 등이 수행된다.


(6) Code Generation

Syntax Analyzer 및 Semantic Analyzing 된 출력값을 Low level Code 로 해석한다.

이 단계를 거쳐서 Assembly Code 등의 Object Code 로 번역된다.


이 단계에서 수행되는 작업들은 다음과 같다.

 - Instruction Selection : 어떤 명령어를 사용할 것인지

 - Instruction Scheduling : 어떤 명령어를 먼저 실행할지 (최적화)

 - Register Allocation : 변수들을 프로세서 레지스터에 할당

 - Debug data generation : 디버그 모드인 경우 디버그를 위한 코드를 생성


(7) Optimization

위의 과정에서 생성된 코드를 한단계 더 최적화한다.

이 단계의 최적화는 두단계로, Machine 에 종속되지 않은 일반적인 형태의 최적화와 Machine 에 종속된 최적화가 이루어진다.

이 과정을 거치면서 중복 제거, 메모리 확보, 코드 정리, 루프 최적화, Control Flow 개선이 일어난다.


위의 단계들을 거치면 High Level Source Code 는 Machine Code 로 변환된다.

Generating 되는 코드는 대게 머신별로 다르게 되며, 세부로직 및 최적화의 과정 역시 컴파일러에 따라 차이가 존재하게 된다.




본 포스팅은 컴파일러에 대한 간략한 소개를 다루고 있으며 더 자세한 내용은 다음 링크들을 참조한다.

(이해해야할 분량이 많다.)


참조

https://www.tutorialspoint.com/compiler_design/index.htm

https://www.programcreek.com/2011/02/how-compiler-works/


+ Recent posts