문서의 각 단락이 어느 리비전에서 마지막으로 수정되었는지 확인할 수 있습니다. 왼쪽의 정보 칩을 통해 작성자와 수정 시점을 파악하세요.

중간 표현 계층 | |
이름 | 중간 표현 계층 |
영문명 | Intermediate Representation Layer |
분류 | |
목적 | 원본 데이터와 최종 표현 사이의 추상화된 데이터 형태를 제공 |
주요 특징 | 플랫폼 독립성, 변환 효율성, 분석 용이성 |
사용 분야 | |
상세 정보 | |
다른 명칭 | IR 계층, 중간 언어(Intermediate Language), 중간 코드 |
주요 역할 | 소스 코드 또는 원시 데이터를 다양한 대상(하드웨어, 런타임 환경)에 맞게 최적화하고 변환하기 위한 표준화된 중간 형태 생성 |
대표적 예시 | LLVM IR, Java 바이트코드, Apache Spark의 RDD, TensorFlow의 그래프 |
장점 | 하드웨어/플랫폼 종속성 제거, 코드 최적화 및 분석 단순화, 재사용성 향상 |
단점 | 추가적인 변환 단계로 인한 오버헤드 발생 가능 |
관련 개념 | |
작동 방식 | 소스/원본 → (파싱/변환) → 중간 표현 계층 → (최적화/변환) → 타겟 표현/실행 코드 |
구현 수준 | 고수준(구문적), 중수준(기계 독립적), 저수준(기계 의존적) IR로 구분 가능 |

중간 표현 계층은 컴파일러나 인터프리터 같은 번역기가 소스 코드를 목적 코드나 바이트코드로 변환하는 과정에서 사용하는 내부적인 데이터 구조이다. 이 계층은 소스 언어의 구체적인 문법적 세부 사항에서 벗어나, 프로그램의 핵심적인 논리와 구조를 보다 단순하고 표준화된 형태로 표현하는 역할을 한다.
주요 목적은 컴파일러 설계의 복잡성을 관리 가능한 모듈로 분리하는 것이다. 프론트엔드는 소스 코드를 중간 표현으로 변환하고, 백엔드는 이 중간 표현을 특정 하드웨어 아키텍처에 맞는 기계어로 변환한다. 이렇게 중간 계층을 둠으로써, 하나의 프론트엔드(예: C++ 컴파일러)가 생성한 중간 표현을 여러 다른 백엔드(예: x86, ARM 백엔드)에서 재사용할 수 있어 컴파일러 개발 효율성이 크게 향상된다.
또한 중간 표현은 다양한 코드 최적화 기법을 적용하기 위한 이상적인 대상이 된다. 소스 코드 수준에서는 문법이 복잡하고, 기계어 수준에서는 하드웨어 의존성이 강해 최적화가 어렵다. 반면, 중간 표현은 프로그램의 의미를 유지하면서도 충분히 낮은 수준의 연산으로 표현되어, 데드 코드 제거나 상수 전파 같은 분석과 변환을 체계적으로 수행할 수 있는 기반을 제공한다.
따라서 중간 표현 계층은 현대적인 컴파일러 및 정적 분석 도구의 핵심 구성 요소로, 언어 독립성과 플랫폼 독립성을 가능하게 하며, 코드의 효율성과 안정성을 높이는 다양한 변환 작업의 중심에 위치한다.

중간 표현 계층은 컴파일러나 인터프리터 같은 언어 처리 시스템에서, 소스 코드와 목적 코드 사이에 존재하는 추상적인 표현 형태이다. 이 계층의 주요 목적은 소스 언어의 구체적인 문법적 세부 사항으로부터 벗어나, 프로그램의 핵심적인 의미와 구조를 보다 단순하고 표준화된 형태로 나타내는 것이다. 이를 통해 컴파일 과정의 프론트엔드(파싱)와 백엔드(코드 생성)를 효과적으로 분리하여 시스템의 모듈화와 재사용성을 높인다.
중간 표현 계층은 추상 구문 트리(AST)와 밀접한 관련이 있지만 명확한 차이점을 가진다. AST는 소스 코드의 구문 구조를 트리 형태로 그대로 반영하며, 언어의 특정 문법 요소(예: 세미콜론, 괄호)를 포함하는 경우가 많다. 반면, 중간 표현은 보다 낮은 수준의 추상화를 지향하며, 다양한 고급 프로그래밍 언어로부터 생성될 수 있는 공통된 연산 집합으로 구성된다. 예를 들어, 복잡한 제어 흐름은 기본 블록과 제어 흐름 그래프(CFG)로 단순화되고, 다양한 종류의 루프 문은 일관된 점프 명령어로 변환된다.
이러한 설계는 두 가지 핵심 이점을 제공한다. 첫째, 새로운 소스 언어를 지원하려면 해당 언어의 프론트엔드(파서)만 새로 작성하여 중간 표현을 생성하면 되며, 기존의 최적화 및 코드 생성 백엔드를 그대로 재사용할 수 있다. 둘째, 중간 표현은 기계어에 비해 훨씬 분석과 변환에 용이한 형태이기 때문에, 다양한 코드 최적화 기법을 적용하기 위한 이상적인 작업대 역할을 한다. 따라서 중간 표현 계층은 컴파일러 설계에서 핵심적인 추상화 도구로 자리 잡았다.
중간 표현 계층은 컴파일러나 인터프리터가 소스 코드를 목표 기계어나 다른 고수준 언어로 변환하는 과정에서 사용하는 내부적인 데이터 구조이자 표현 형식이다. 이 계층은 소스 언어의 구체적인 문법적 세부 사항에서 벗어나, 프로그램의 핵심적인 논리와 구조를 보다 추상적이고 표준화된 형태로 포착하는 것을 목적으로 한다.
주요 목적은 크게 두 가지로 나뉜다. 첫째, 언어 독립성을 제공하여 하나의 프론트엔드(파서)가 여러 소스 언어를 동일한 중간 표현으로 변환하고, 하나의 백엔드(코드 생성기)가 그 중간 표현을 여러 목표 플랫폼의 코드로 변환할 수 있게 한다. 이는 컴파일러 설계의 모듈화와 재사용성을 극대화한다. 둘째, 변환 과정에서 다양한 코드 최적화를 적용하기 위한 이상적인 단계를 마련한다. 소스 코드 단계에서는 언어별 복잡성이, 목표 기계어 단계에서는 하드웨어별 제약이 강해 최적화가 어려운 반면, 중간 표현은 두 세계 사이의 균형점을 제공한다.
이러한 중간 표현은 추상 구문 트리(AST)와 구별된다. AST는 소스 코드의 구문 구조를 그대로 반영하는 트리 구조인 반면, 중간 표현은 실행 흐름과 연산의 의미를 명시적으로 드러내도록 설계된다. 예를 들어, 제어 흐름 그래프나 3-주소 코드 같은 선형 형식은 AST보다 더 낮은 수준의 표현으로, 실제 실행 모델에 가깝다. 따라서 중간 표현은 분석과 변환에 더욱 적합한 형태를 지닌다.
추상 구문 트리(AST)는 소스 코드의 구문 구조를 트리 형태로 표현한 것으로, 주로 구문 분석 단계에서 생성됩니다. 이는 원본 프로그래밍 언어의 문법과 밀접하게 연결되어 있으며, 괄호나 세미콜론 같은 구문적 세부 사항을 포함하는 경우가 많습니다. AST의 주요 목적은 소스 코드의 구조를 정확하게 포착하여 의미 분석에 활용하는 것입니다.
반면, 중간 표현 계층(IR)은 컴파일 과정의 중간 단계에서 사용되는 더 낮은 수준의 표현입니다. IR은 구문적 세부 사항을 제거하고, 기계나 목표 언어에 더 가까운 명령어 형태로 변환됩니다. IR은 종종 3-주소 코드나 제어 흐름 그래프(CFG)와 같은 선형 또는 그래프 기반 형태를 취하여 최적화와 코드 생성에 적합하도록 설계됩니다.
두 표현의 주요 차이점은 다음과 같이 요약할 수 있습니다.
특성 | 추상 구문 트리 (AST) | 중간 표현 계층 (IR) |
|---|---|---|
생성 시점 | 구문 분석 직후, 의미 분석 전 | 의미 분석 이후, 코드 생성 전 |
표현 수준 | 고수준, 구문 구조 보존 | 저수준, 구문 요소 생략 |
주요 목적 | 소스 코드 구조 분석 및 검증 | 코드 최적화 및 기계 독립적 변환 |
의존성 | 특정 소스 언어 문법에 의존 | 언어 독립적 또는 가상 기계에 의존 |
표현 형태 | 계층적 트리 구조 | 선형 명령어 시퀀스 또는 그래프 |
요컨대, AST는 소스 코드의 '원본 구조'를 나타내는 반면, IR은 컴파일러가 다양한 최적화를 수행하고 최종 기계 코드로 변환하기 위한 '작업용 표현'입니다. 많은 현대 컴파일러는 AST를 먼저 생성한 후, 이를 더 단순하고 표준화된 IR로 변환하여 후속 처리 단계를 단순화합니다.

중간 표현 계층은 컴파일러나 인터프리터 설계에서 소스 코드와 목적 코드 사이에 위치하는 추상적인 표현 형태이다. 이 계층은 여러 중요한 특징을 가지며, 이 특징들은 소프트웨어 개발 도구의 효율성과 유연성을 크게 향상시킨다.
첫 번째 주요 특징은 언어 독립성이다. 중간 표현은 특정 프로그래밍 언어의 구문적 세부 사항으로부터 벗어난 형태로 설계된다. 이는 하나의 프론트엔드가 여러 다른 소스 언어를 동일한 중간 표현으로 변환할 수 있게 하며, 하나의 백엔드가 이 중간 표현을 다양한 목표 플랫폼의 기계어로 변환할 수 있게 한다. 결과적으로, N개의 소스 언어와 M개의 목표 아키텍처를 지원하기 위해 필요한 컴파일러 구성 요소의 수를 N+M으로 줄일 수 있다[1]. 이는 개발 및 유지보수 비용을 절감하는 데 기여한다.
두 번째 특징은 최적화 가능성이다. 중간 표현은 소스 코드보다는 낮은 수준이지만 기계어보다는 높은 수준의 추상화를 유지하여, 플랫폼에 종속되지 않은 다양한 최적화를 적용하기에 이상적인 형태를 제공한다. 예를 들어, 사용하지 않는 변수 제거, 상수 폴딩, 루프 언롤링과 같은 변환을 중간 표현 단계에서 수행할 수 있다. 이렇게 중간 단계에서 한 번 최적화를 수행하면, 그 결과를 여러 다른 플랫폼에 재사용할 수 있어 효율적이다.
세 번째 특징은 분석 용이성이다. 중간 표현은 일반적으로 소스 코드의 추상 구문 트리(AST)보다 단순화되고 정규화된 구조를 가진다. 제어 흐름과 데이터 흐름을 명확하게 표현하도록 설계되어, 정적 분석 도구가 프로그램의 동작을 이해하고 버그나 보안 취약점을 발견하는 데 유리하다. 또한, 중간 표현은 종종 인간이 읽을 수 있는 텍스트 형태로 덤프(dump)될 수 있어, 컴파일러 개발자나 도구 사용자가 변환 및 최적화 과정을 디버깅하고 검증하는 데 도움을 준다.
중간 표현 계층의 언어 독립성은 소스 코드의 원래 프로그래밍 언어에 구애받지 않고 다양한 언어로부터 생성될 수 있으며, 다양한 목표 플랫폼으로 변환될 수 있는 특성을 의미한다. 이는 중간 표현이 고수준 언어의 구체적인 문법적 특성이나 저수준 기계어의 하드웨어 의존적 세부 사항으로부터 추상화된 형태를 취하기 때문에 가능하다. 예를 들어, C++, Python, Rust 등 서로 다른 문법과 패러다임을 가진 언어들도 동일한 중간 표현(예: LLVM IR)으로 컴파일될 수 있다. 이는 컴파일러 설계에서 프론트엔드와 백엔드를 명확히 분리하는 데 핵심적인 역할을 한다.
언어 독립성의 주요 이점은 재사용성과 도구 생태계의 통합에 있다. 한 번 구현된 중간 표현용 최적화 도구나 정적 분석기는 해당 중간 표현을 생성할 수 있는 모든 프로그래밍 언어에 대해 동일하게 적용될 수 있다. 이는 새로운 프로그래밍 언어를 개발할 때, 언어 설계자들이 고수준 언어의 프론트엔드만 구현하면 기존에 검증된 강력한 최적화 및 코드 생성 백엔드 인프라를 즉시 활용할 수 있게 함으로써 개발 비용과 시간을 크게 절감한다. 또한, 서로 다른 언어로 작성된 모듈 간의 상호 운용성을 위한 공통 기반으로도 작동할 수 있다.
장점 | 설명 |
|---|---|
도구 재사용 | 최적화, 분석, 디버깅 도구를 여러 언어에 걸쳐 공통으로 사용 가능 |
상호 운용성 | 다른 언어로 작성된 코드가 동일한 중간 표현을 통해 통합 및 링크 가능 |
플랫폼 확장성 |
이러한 독립성은 완전한 중립성을 의미하지는 않는다. 중간 표현의 설계는 여전히 특정 프로그래밍 패러다임(예: 객체 지향, 함수형)이나 메모리 모델에 영향을 받을 수 있다. 그러나 그 영향은 원본 소스 언어의 구체적인 문법보다는 보다 일반화된 계산 모델의 수준에 머무른다. 결과적으로, 언어 독립성은 컴파일러 기술의 표준화와 모듈화를 촉진하며, 소프트웨어 개발 도구 체인의 효율성과 유연성을 높이는 근간이 된다.
중간 표현 계층은 컴파일러나 인터프리터가 소스 코드를 목적 코드로 변환하는 과정에서 다양한 최적화를 적용하기 위한 이상적인 단계를 제공합니다. 소스 코드는 고수준의 추상적 구조를 가지고 있어 직접적인 최적화가 어렵고, 목적 코드는 하드웨어에 종속된 저수준 명령어로 구성되어 최적화의 유연성이 제한됩니다. 중간 표현은 이 두 단계 사이에 위치하여, 기계 독립적이면서도 충분히 저수준인 형태로 코드를 표현함으로써 광범위한 최적화 기법을 적용할 수 있는 토대를 마련합니다.
최적화는 일반적으로 중간 표현이 그래프나 선형 코드 형태로 변환된 후에 수행됩니다. 대표적인 최적화 기법으로는 불필요한 코드 제거(데드 코드 제거), 상수 표현식 계산(상수 접기), 반복문 내 불변 코드의 외부 이동(루프 불변 코드 이동), 공통 부분 표현식 제거 등이 있습니다. 이러한 변환들은 중간 표현의 명확하고 표준화된 구조 덕분에 체계적으로 분석하고 적용할 수 있습니다.
최적화 유형 | 설명 | 예시 |
|---|---|---|
지역 최적화 | 기본 블록 내에서 수행되는 최적화. 문맥 분석 범위가 좁아 빠르게 적용 가능합니다. | 상수 접기, 공통 부분 표현식 제거 |
전역 최적화 | 함수나 프로그램 전체를 분석하여 수행되는 최적화. 더 복잡한 분석이 필요하지만 효과는 큽니다. | 데드 코드 제거, 루프 최적화, 전역 레지스터 할당 |
프로시저 간 최적화 | 여러 함수 또는 모듈을 통합하여 분석하는 최적화. 링크 타임 최적화(LTO)에 활용됩니다. | 인라인 확장, 프로시저 간 상수 전파 |
이러한 최적화 과정은 중간 표현의 다단계 특성을 활용하기도 합니다. 예를 들어, 고수준에 가까운 중간 표현에서는 루프 변환이나 고수준 데이터 구조 분석에 유리한 최적화를 수행하고, 저수준에 가까운 중간 표현으로 내려갈수록 레지스터 할당이나 명령어 스케줄링과 같은 기계 의존적 최적화를 적용합니다. 결과적으로 중간 표현 계층은 컴파일러가 다양한 목표 플랫폼에 대해 높은 성능의 코드를 생성할 수 있도록 하는 핵심 메커니즘으로 작동합니다.
중간 표현 계층은 소스 코드의 복잡한 구문 구조를 단순화하여 프로그램의 동작을 더 쉽게 분석할 수 있는 형태로 제공한다. 소스 코드는 가독성을 위해 설계된 반면, 중간 표현은 기계 처리를 위해 설계되기 때문에, 제어 흐름과 데이터 흐름을 명확하게 드러낸다. 이는 정적 분석 도구가 프로그램의 잠재적 오류, 보안 취약점, 성능 병목 현상을 식별하는 데 필수적이다.
중간 표현은 분석을 위한 표준화된 인터페이스 역할을 한다. 서로 다른 프로그래밍 언어로 작성된 소스 코드를 동일한 중간 표현으로 변환하면, 분석 도구는 언어별 특수성을 고려하지 않고도 일관된 방식으로 코드를 검사할 수 있다. 예를 들어, 데이터 흐름 분석이나 정적 단일 대입(SSA) 형태와 같은 분석 기법은 중간 표현 위에서 효율적으로 수행될 수 있다.
분석의 용이성은 중간 표현의 구조적 특성에서 비롯된다. 추상 구문 트리(AST)가 문법적 구조를 보존하는 데 중점을 둔다면, 대부분의 중간 표현은 실행 의미론에 더 집중하여 연산과 피연산자를 명시적으로 나열한다. 이는 다음과 같은 분석 작업을 단순화한다.
제어 흐름 분석: 기본 블록과 점프 명령어를 통해 프로그램의 실행 경로를 명확히 추적할 수 있다.
의존성 분석: 명령어 간의 데이터 의존성을 쉽게 파악하여 재정의나 병렬 실행 가능성을 판단한다.
자원 사용 분석: 레지스터나 메모리 접근 패턴을 분석하여 최적화 기회를 발견한다.
이러한 구조적 장점 덕분에, 중간 표현은 컴파일러의 최적화 단계뿐만 아니라, 린터, 정적 분석기, 형식 검증 도구 등 다양한 소프트웨어 공학 도구의 핵심 구성 요소로 널리 사용된다.

중간 표현 계층의 구조는 크게 그래프 기반 표현과 선형 표현으로 나뉜다. 그래프 기반 표현은 제어 흐름 그래프나 데이터 흐름 그래프와 같이 프로그램의 실행 경로와 데이터 의존 관계를 노드와 간선으로 시각적으로 모델링한다. 이 방식은 프로그램의 구조를 직관적으로 파악하고 복잡한 최적화를 수행하는 데 유리하다. 반면, 선형 표현은 명령어를 순차적으로 나열하는 방식으로, 대표적인 예로 3-주소 코드가 있다.
3-주소 코드는 대부분의 연산이 최대 두 개의 피연산자(source)와 하나의 결과(target)를 가지는 형태로 구성된다. 이는 복잡한 표현식을 단순한 명령어 시퀀스로 분해하여, 이후 기계어 코드 생성이나 최적화를 보다 체계적으로 수행할 수 있게 한다. 선형 표현은 구현이 비교적 간단하고 직렬화가 용이하다는 장점을 가진다.
두 표현 방식은 상호 변환이 가능하며, 도구의 목적에 따라 선택적으로 사용된다. 예를 들어, 정적 분석에는 그래프 기반 표현이, 실제 코드 생성 단계에는 선형 표현이 더 자주 활용된다. 어떤 방식을 취하든, 중간 표현의 핵심 구조는 원본 소스 코드의 의미를 보존하면서도 구체적인 하드웨어나 고수준 언어의 문법적 세부사항으로부터 자유로워야 한다.
표현 방식 | 주요 특징 | 대표 예시 | 주 활용 분야 |
|---|---|---|---|
그래프 기반 | 제어/데이터 흐름을 노드와 간선으로 표현, 분석에 유리 | 제어 흐름 그래프, 정적 단일 대입(SSA) 형태 | 정적 분석, 고급 최적화 |
선형 표현 | 명령어를 순차적으로 나열, 구현과 변환이 단순 | 3-주소 코드, 스택 기반 코드 | 코드 생성, 간단한 최적화 |
중간 표현 계층에서 그래프 기반 표현은 프로그램의 제어 흐름과 데이터 의존성을 시각적으로 모델링하는 일반적인 방식이다. 이 표현은 기본적으로 노드와 간선으로 구성된 그래프 구조를 사용하여 연산과 그 사이의 관계를 나타낸다. 가장 널리 사용되는 그래프 기반 표현으로는 제어 흐름 그래프와 데이터 흐름 그래프가 있다. 제어 흐름 그래프는 프로그램의 실행 가능한 기본 블록을 노드로, 블록 간의 점프나 분기를 간선으로 표현하여 프로그램이 실행될 수 있는 경로를 보여준다. 데이터 흐름 그래프는 값의 정의와 사용에 초점을 맞춰, 연산 간의 데이터 의존 관계를 명시적으로 드러낸다.
이러한 그래프 표현은 프로그램의 구조를 분석하고 최적화하는 데 매우 효과적이다. 예를 들어, 순환 의존성이 없는 방향성 비순환 그래프 형태는 중복된 하위 표현식을 쉽게 식별하고 제거하는 공통 부분식 제거 같은 최적화를 가능하게 한다. 또한, 그래프의 순회를 통해 도달 가능성 분석이나 라이브 변수 분석 같은 정적 분석을 수행할 수 있다. 그래프의 명시적인 구조 덕분에 컴파일러나 분석 도구가 프로그램의 동작을 더 깊이 이해하고 변환할 수 있다.
그래프 유형 | 주요 구성 요소 | 표현 목적 | 활용 예시 |
|---|---|---|---|
기본 블록, 조건/무조건 분기 간선 | 프로그램 실행 경로 시각화 | ||
연산 노드, 데이터 의존 간선 | 값의 생성과 소비 관계 모델링 | ||
작업/명령어 노드, 의존 간선 | 작업 간 선후행 관계 명시 |
선형 표현인 3-주소 코드와 비교할 때, 그래프 기반 표현은 연산들 사이의 관계를 더 풍부하고 직관적으로 보여준다는 장점이 있다. 그러나 이 표현은 일반적으로 더 많은 메모리를 소비하며, 생성과 조작 과정이 복잡할 수 있다. 따라서 많은 컴파일러 프레임워크는 내부적으로 그래프 기반 표현을 사용하되, 최종적으로 기계 코드를 생성하기 전에는 선형적인 형태로 변환하는 방식을 취한다.
중간 표현 계층의 선형 표현은 명령어나 연산을 순차적으로 나열하는 형태를 가진다. 이는 추상 구문 트리와 같은 트리 구조와는 대조적으로, 실행 흐름을 직선적인 코드 열로 나타낸다. 가장 대표적인 예로 3-주소 코드가 있으며, 각 명령어가 일반적으로 두 개의 피연산자를 사용하여 연산을 수행하고, 그 결과를 하나의 대상에 저장하는 형식을 따른다.
3-주소 코드의 기본 형식은 대상 = 피연산자1 연산자 피연산자2이다. 이 구조는 복잡한 표현식을 여러 개의 단순한 명령어로 분해하여 중간 표현을 단순화하고, 이후의 코드 최적화 및 기계어 생성 단계를 용이하게 한다. 예를 들어, a = b + c * d라는 표현식은 3-주소 코드로 t1 = c * d, a = b + t1과 같이 두 개의 명령어로 변환된다[2].
선형 표현의 다른 형태로는 포스트픽스 표기법이나 바이트코드가 있다. 이러한 표현들은 명령어의 선형 시퀀스로 구성되어, 가상 머신에 의한 해석이나 실행이 효율적으로 이루어질 수 있도록 설계되었다. 선형 표현은 일반적으로 메모리 효율성이 높고 순차 처리에 적합한 특징을 가진다.
표현 방식 | 주요 특징 | 사용 예 |
|---|---|---|
3-주소 코드 | 각 명령어가 최대 세 개의 주소(피연산자)를 사용 | 전통적인 컴파일러 최적화 단계 |
바이트코드 | 바이트 단위의 연산 코드로 구성, 컴팩트함 | |
포스트픽스 표현 | 연산자가 피연산자 뒤에 위치(역폴란드 표기법) | 일부 스택 기반 가상 머신 |
선형 표현은 특히 컴파일러 백엔드에서 레지스터 할당 및 명령어 스케줄링과 같은 저수준 최적화를 수행하기에 적합한 형태를 제공한다. 이는 프로그램의 제어 흐름 그래프 내에서 기본 블록 단위로 쉽게 처리될 수 있기 때문이다.

중간 표현 계층은 컴파일러 설계에서 핵심적인 역할을 수행한다. 소스 코드를 직접 기계어로 변환하는 대신, 중간 표현을 거치는 방식은 컴파일러를 프론트엔드(언어별 분석)와 백엔드(목표 플랫폼별 코드 생성)로 명확히 분리한다. 이를 통해 새로운 프로그래밍 언어를 지원하려면 해당 언어의 프론트엔드만 개발하면 되고, 새로운 프로세서 아키텍처를 지원하려면 해당 아키텍처의 백엔드만 개발하면 된다. 이는 개발 비용을 크게 절감하고 재사용성을 높인다.
정적 분석 도구는 중간 표현을 기반으로 프로그램의 동작을 실행 없이 검증한다. 중간 표현은 소스 코드보다 단순하고 구조화되어 있어, 데이터 흐름 분석, 제어 흐름 분석, 정적 단일 대입(SSA) 형태 변환 등의 분석을 수행하기에 더 적합하다. 이를 통해 메모리 누수, 널 포인터 역참조, 배열 범위 초과 같은 잠재적 오류나 보안 취약점을 찾아낼 수 있다.
코드 변환 및 최적화는 중간 표현 계층의 가장 중요한 활용 분야 중 하나이다. 중간 표현은 다양한 최적화 기법을 적용하기 위한 표준화된 작업대 역할을 한다. 최적화는 중간 표현 단계에서 수행되며, 그 예는 다음과 같다.
최적화 유형 | 설명 | 예시 |
|---|---|---|
불필요 코드 제거 | 실행 결과에 영향을 미치지 않는 코드를 삭제한다. | |
루프 최적화 | 반복문의 실행 효율을 높인다. | |
인라인 확장 | 함수 호출 오버헤드를 줄인다. | 함수 본문을 호출 위치에 직접 삽입한다. |
이러한 변환과 최적화는 최종 생성되는 기계어 코드의 성능과 크기를 결정적으로 향상시킨다.
중간 표현 계층은 컴파일러 설계의 핵심 구성 요소로, 소스 코드와 목적 코드 사이의 다리 역할을 한다. 컴파일러는 일반적으로 프론트엔드와 백엔드로 나뉘는데, 중간 표현은 이 두 부분을 분리하여 설계를 모듈화하고 재사용성을 높이는 데 기여한다. 프론트엔드는 특정 프로그래밍 언어의 구문을 분석하여 중간 표현을 생성하고, 백엔드는 이 중간 표현을 특정 하드웨어 아키텍처에 맞는 기계어로 변환한다. 이 구조 덕분에 새로운 언어를 지원하려면 프론트엔드만 새로 작성하면 되고, 새로운 프로세서를 지원하려면 백엔드만 개발하면 된다.
중간 표현을 사용함으로써 컴파일러는 복잡한 최적화 작업을 보다 체계적으로 수행할 수 있다. 최적화는 주로 중간 표현 단계에서 이루어지며, 소스 코드 수준이나 기계어 수준에서 수행하는 것보다 더 효율적이고 일반적이다. 예를 들어, 불필요한 계산 제거, 루프 최적화, 상수 전파 등의 기법은 중간 표현의 구조를 분석하고 변환하여 적용된다. 이는 최적화 로직이 특정 언어나 하드웨어에 종속되지 않도록 보장한다.
또한, 중간 표현은 컴파일러의 디버깅과 테스트를 용이하게 한다. 컴파일 과정의 중간 단계를 명확한 형태로 확인할 수 있어, 오류가 발생한 위치를 정확히 진단하는 데 도움이 된다. 여러 언어를 동일한 중간 표현으로 컴파일하는 멀티 언어 컴파일러나, 하나의 중간 표현을 여러 플랫폼의 코드로 변환하는 크로스 컴파일러 설계에도 필수적이다. 대표적인 예로 LLVM 컴파일러 인프라스트럭처는 LLVM IR이라는 강력한 중간 표현을 사용하여 다양한 언어와 아키텍처를 지원한다.
정적 분석 도구는 소스 코드나 중간 표현을 실제 실행 없이 분석하여 프로그램의 속성, 오류, 취약점 등을 검출하는 소프트웨어입니다. 중간 표현 계층은 이러한 도구의 핵심 분석 대상으로 작용합니다. 소스 코드를 직접 분석하는 것보다 표준화되고 단순화된 중간 표현을 사용하면, 분석 도구의 구현 복잡도를 낮추고 다양한 프로그래밍 언어에 대한 지원을 용이하게 합니다. 또한, 제어 흐름 그래프나 데이터 흐름 분석과 같은 정형화된 분석 기법을 적용하기에 적합한 형태를 제공합니다.
중간 표현을 기반으로 한 정적 분석은 주로 메모리 안전성 위반, 널 포인터 역참조, 자원 누수, 보안 취약점 등을 찾아내는 데 활용됩니다. 예를 들어, LLVM IR을 분석하여 정의되지 않은 변수 사용이나 배열 범위 초과 접근을 검사할 수 있습니다. 이러한 분석은 컴파일 시간에 수행되어 잠재적인 런타임 오류를 사전에 방지하는 데 기여합니다.
다양한 정적 분석 도구의 역할은 다음과 같이 구분할 수 있습니다.
도구 유형 | 주요 분석 대상 (중간 표현) | 주요 목적 |
|---|---|---|
린터(Linter) | 주로 추상 구문 트리(AST) 또는 간단한 IR | 코딩 스타일, 간단한 오류 패턴 검출 |
정적 분석기(Static Analyzer) | 복잡한 데이터 흐름 및 제어 흐름 기반 오류 탐지 | |
형식 검증기(Formal Verifier) | 수학적 모델로 변환된 중간 표현 | 프로그램의 정확성에 대한 수학적 증명 |
이러한 도구들은 중간 표현의 단계적 변환 과정 각 단계에서 분석을 수행할 수 있습니다. 예를 들어, 고수준 중간 표현에서는 타입 안전성을, 저수준 중간 표현에서는 레지스터 할당이나 메모리 접근 패턴을 분석할 수 있습니다. 결과적으로, 중간 표현 계층은 소프트웨어의 신뢰성과 보안성을 향상시키는 정적 분석 기술의 토대를 마련합니다.
중간 표현 계층은 소스 코드를 다양한 목적에 맞게 변환하거나 성능을 개선하는 최적화 작업의 핵심 매개체 역할을 한다. 컴파일러나 변환 도구는 소스 코드를 먼저 중간 표현으로 변환한 후, 이 표현을 대상 플랫폼의 기계어나 다른 고수준 언어로 변환한다. 이 과정에서 중간 표현은 구체적인 소스 언어 문법이나 하드웨어 아키텍처의 세부 사항으로부터 자유로워, 변환과 최적화를 보다 일반화된 방식으로 수행할 수 있게 한다.
코드 변환 측면에서 중간 표현은 크로스 컴파일이나 소스-투-소스 컴파일러의 기반이 된다. 예를 들어, C++ 코드를 WebAssembly로 변환하거나, 한 프로그래밍 언어의 코드를 다른 언어의 코드로 번역할 때, 중간 표현을 공통의 중간 단계로 사용하면 변환 로직을 단순화할 수 있다. 또한, 리팩토링 도구나 코드 마이그레이션 도구는 중간 표현을 분석하여 코드 구조를 안전하게 변경한다.
최적화에 있어서 중간 표현은 다양한 수준에서 적용된다. 지역 최적화는 기본 블록 내에서 상수 폴딩이나 불필요한 코드 제거 같은 작업을 수행한다. 전역 최적화는 제어 흐름 그래프를 분석하여 죽은 코드 제거, 루프 불변 코드 이동, 강도 감소 같은 더 복잡한 변환을 가능하게 한다. 중간 표현의 형태(예: SSA 폼)는 이러한 분석과 변환을 특히 효율적으로 만든다.
최적화 유형 | 설명 | 중간 표현에서의 적용 예 |
|---|---|---|
지역 최적화 | 기본 블록 내에서 수행되는 최적화 | 상수 표현식 계산, 중복된 부하 연산 제거 |
전역 최적화 | 함수 전체의 제어 흐름을 분석하여 수행하는 최적화 | 전역 공통 부분식 제거, 루프 최적화 |
기계 독립적 최적화 | 대상 하드웨어와 관계없이 중간 표현 수준에서 수행 가능한 최적화 | 죽은 코드 제거, 인라인 확장 |
기계 종속적 최적화 | 특정 하드웨어(예: 레지스터 세트, 파이프라인)를 고려한 최적화 | 레지스터 할당, 명령어 스케줄링 |
이러한 변환과 최적화는 최종 생성되는 코드의 크기를 줄이고 실행 속도를 높이는 데 기여한다. LLVM 컴파일러 인프라의 LLVM IR이 대표적인 예로, 다양한 프론트엔드 언어로부터 생성된 IR에 대해 광범위한 최적화 패스[3]를 적용한 후, 여러 백엔드(예: x86, ARM)를 위한 코드로 변환한다.

중간 표현은 다양한 형태로 구현되며, 각각의 설계 목적과 특징을 가진다. 대표적인 중간 표현으로는 LLVM IR, Java 바이트코드, Microsoft CIL 등이 있다.
LLVM IR은 LLVM 컴파일러 인프라스트럭처의 핵심 구성 요소이다. 이는 정적 단일 할당(SSA) 형식을 기반으로 한 저수준의 언어 독립적 표현으로, 강력한 타입 시스템과 명시적인 제어 흐름 그래프를 특징으로 한다. LLVM IR은 컴파일 시간에 다양한 최적화를 수행하기에 적합한 형태로 설계되었으며, C, C++, Rust, Swift 등 다양한 프로그래밍 언어의 백엔드로 활용된다. 메모리에서의 표현과 텍스트/바이너리 파일 형식을 모두 지원한다.
Java 바이트코드는 Java 가상 머신이 실행하는 중간 표현이다. 스택 기반의 명령어 집합 구조를 가지며, .class 파일에 저장되어 플랫폼 독립성을 실현하는 데 기여한다. 이 바이트코드는 컴파일 시간이 아닌 런타임에 JIT 컴파일러에 의해 기계어로 변환되거나 인터프리터에 의해 실행된다. Microsoft CIL(Common Intermediate Language)은 .NET 프레임워크의 중간 언어로, Java 바이트코드와 유사한 역할을 수행한다. CIL은 스택 기반이지만, 로컬 변수에 대한 직접 접근 등 보다 풍부한 명령어 세트를 포함한다. C#이나 VB.NET 등의 고급 언어로 작성된 코드는 CIL로 컴파일된 후, CLR(Common Language Runtime) 환경에서 실행된다.
이 외에도 GCC 컴파일러 제네레이터에서 사용하는 GIMPLE과 RTL, 함수형 언어 구현에 사용되는 람다 계산 기반 표현, 그리고 특정 도메인(예: 그래픽스 셰이더)을 위한 중간 표현들도 존재한다. 각 중간 표현은 특정한 컴파일러 아키텍처나 런타임 환경의 요구사항에 맞춰 설계되었다.
LLVM IR은 LLVM 컴파일러 인프라스트럭처의 핵심 구성 요소인 중간 표현이다. 이는 고수준 프로그래밍 언어와 다양한 목표 머신 아키텍처 사이의 중간 단계로 작동하도록 설계되었다. LLVM IR은 정적 단일 할당(SSA) 형식을 기반으로 하며, 세 가지 동등한 형태(텍스트 표현, 메모리 내 객체, 비트코드 직렬화)로 존재한다. 이는 강력한 타입 시스템을 가지며, 언어 독립적인 저수준 연산을 제공하면서도 고수준의 타입과 제어 흐름 정보를 유지한다는 특징이 있다.
LLVM IR의 주요 구조는 모듈, 함수, 기본 블록, 명령어로 구성된다. 각 모듈은 전역 변수와 함수 선언을 포함하며, 함수는 일련의 기본 블록으로 이루어지고, 각 기본 블록은 SSA 형태의 명령어 시퀀스로 구성된다. 제어 흐름은 명시적인 분기 명령어를 통해 기본 블록 간에 연결된다. 이는 기계어와 유사한 저수준 연산(예: 덧셈, 비교, 메모리 로드/저장)을 제공하지만, 임의의 가상 레지스터를 사용하고 복잡한 제어 흐름 구조를 직접 지원한다.
LLVM IR은 컴파일 파이프라인에서 광범위한 최적화를 수행할 수 있는 플랫폼 역할을 한다. LLVM은 메모리에서 IR을 조작하는 풍부한 최적화 패스 라이브러리를 제공하며, 이를 통해 불필요한 계산 제거, 루프 최적화, 인라인 확장, 데드 코드 제거 등 다양한 최적화가 가능하다. 이러한 최적화는 IR이 특정 하드웨어 아키텍처에 의존하지 않기 때문에, 한 번 작성된 최적화 패스가 여러 다른 대상 아키텍처에 재사용될 수 있다.
최종적으로, LLVM IR은 LLVM 백엔드에 의해 특정 대상 아키텍처(예: x86, ARM, RISC-V)의 기계어로 변환된다. 이 변환 과정에는 레지스터 할당, 명령어 선택, 명령어 스케줄링 등의 단계가 포함된다. LLVM IR의 성공은 모듈화된 설계와 강력한 중간 표현 덕분에, Clang(C/C++), Swift, Rust 등 다양한 프로그래밍 언어의 컴파일러 프론트엔드가 이를 공통 백엔드로 사용할 수 있게 했다는 점에 있다.
Java 바이트코드는 자바 가상 머신이 실행할 수 있는 명령어 집합으로, 자바 프로그래밍 언어의 중간 표현 계층을 구성합니다. 자바 컴파일러(javac)는 자바 소스 코드(.java 파일)를 플랫폼 독립적인 바이트코드(.class 파일)로 변환합니다. 이 바이트코드는 특정 하드웨어나 운영체제에 종속되지 않는 이진 형식으로, JVM이 설치된 모든 환경에서 실행될 수 있습니다.
바이트코드는 스택 기반의 가상 머신을 위해 설계되었으며, 대부분의 명령어가 오퍼랜드 스택을 조작하는 방식으로 동작합니다. 예를 들어, 두 값을 더하는 연산은 먼저 두 값을 스택에 푸시(push)한 후, 덧셈 명령어가 스택 상단의 두 값을 꺼내어(pop) 계산하고 결과를 다시 스택에 푸시하는 방식으로 이루어집니다. 주요 명령어 유형은 다음과 같습니다.
명령어 유형 | 설명 | 예시 |
|---|---|---|
스택 조작 | 오퍼랜드 스택에 값을 푸시하거나 팝함 |
|
산술 연산 | 정수 또는 부동소수점 산술 연산 수행 |
|
제어 흐름 | 조건 분기 및 무조건 점프 |
|
메소드 호출 | 메소드를 호출하고 결과를 받음 |
|
필드 접근 | 객체의 필드를 읽거나 씀 |
|
바이트코드는 JIT 컴파일러에 의해 실행 시점에 해당 플랫폼의 네이티브 코드로 동적으로 컴파일되거나, 인터프리터에 의해 직접 해석되어 실행됩니다. 이 설계는 "한 번 작성하고, 어디서나 실행된다"는 자바의 핵심 철학을 실현하는 기반이 됩니다. 또한, 바이트코드의 상대적으로 높은 수준의 정보는 리플렉션과 같은 고급 기능이나 정적 분석 도구의 동작을 가능하게 합니다.
Microsoft CIL(Common Intermediate Language)은 .NET 프레임워크와 .NET Core/.NET 5 이상의 플랫폼에서 사용되는 중간 표현 계층 언어이다. 원래 이름은 MSIL(Microsoft Intermediate Language)이었으나, 표준화 과정을 거쳐 CIL로 불린다. 이 언어는 공통 언어 기반(CLI) 사양의 핵심 부분을 이루며, C#, VB.NET, F# 등 다양한 .NET 언어로 작성된 소스 코드가 컴파일되는 대상 형식이다.
CIL은 스택 기반의 바이트코드 형식을 취한다. 명령어 집합은 대부분의 연산이 명시적인 오퍼랜드 대신 평가 스택(evaluation stack)의 값을 사용하도록 설계되었다. 예를 들어, 두 숫자를 더하는 ADD 명령은 스택에서 두 값을 꺼내 더한 후, 그 결과를 다시 스택에 넣는다. 이는 레지스터 기반의 중간 표현과 구별되는 특징이다. CIL 코드는 JIT 컴파일러에 의해 실행 시점에 특정 하드웨어의 네이티브 코드로 변환되어 실행되거나, 경우에 따라 AOT 컴파일을 통해 미리 네이티브 코드로 변환되기도 한다.
CIL의 구조와 명령어 집합은 높은 수준의 언어 정보를 보존하도록 설계되어, 강력한 리플렉션과 메타데이터 접근을 가능하게 한다. 다음은 CIL의 주요 구성 요소를 나타낸 표이다.
구성 요소 | 설명 |
|---|---|
명령어(Instructions) | 스택 조작, 산술 연산, 제어 흐름, 메서드 호출 등을 위한 바이트코드 명령어 집합 |
메타데이터(Metadata) | 형식 정의, 메서드 시그니처, 어셈블리 정보 등에 대한 상세한 설명 정보 |
매니페스트(Manifest) | 어셈블리 자체의 정보와 의존하는 다른 어셈블리에 대한 정보 |
리소스(Resources) | 어셈블리에 포함된 문자열, 이미지 등 임베디드 리소스 |
CIL은 언어 독립성과 플랫폼 독립성(.NET이 지원하는 범위 내에서)을 실현하는 데 기여한다. 하나의 .NET 언어로 작성된 라이브러리는 CIL 수준에서 다른 .NET 언어에서 완전히 호환되어 사용될 수 있다. 또한, CIL 코드는 관리 코드(managed code)로서 가비지 컬렉션, 형식 안전성, 예외 처리 등의 서비스를 런타임으로부터 제공받는다.

중간 표현의 생성은 일반적으로 컴파일러의 프론트엔드 단계에서 이루어진다. 소스 코드는 먼저 어휘 분석과 구문 분석을 거쳐 추상 구문 트리로 변환된다. 이후 AST는 의미 분석을 통해 타입 검사와 같은 과정을 거치고, 이를 기반으로 중간 표현으로 변환된다. 이 변환 과정은 소스 언어의 고수준 구문 구조를 보다 단순하고 표준화된 저수준 연산 집합으로 매핑하는 작업이다. 예를 들어, 복잡한 제어 흐름 문(if, while 등)은 조건부 점프와 레이블을 사용한 기본 블록의 조합으로 표현된다.
생성된 중간 표현은 이후 컴파일러의 백엔드로 전달되어 목표 플랫폼에 맞는 기계어 코드로 변환된다. 이 변환 과정은 주로 코드 생성과 레지스터 할당 단계를 포함한다. 코드 생성기는 중간 표현의 각 연산을 해당 명령어 집합 구조의 명령어 시퀀스로 변환한다. 이때 중간 표현이 언어와 하드웨어로부터 독립적이기 때문에, 동일한 중간 표현을 서로 다른 여러 백엔드(예: x86, ARM, RISC-V)에 입력하여 다양한 목표 코드를 생성할 수 있다.
변환 과정에서 중간 표현은 종종 여러 단계를 거치며 점진적으로 하드웨어에 가까운 형태로 변형된다. 예를 들어, 초기에는 고수준의 함수 호출과 복잡한 데이터 구조 접근을 그대로 표현하다가, 이후 단계에서 함수 인자 전달 규약을 명시하거나 메모리 접근을 명시적인 주소 계산과 로드/스토어 명령으로 분해한다. 이는 최종 기계어 코드의 효율성을 높이는 데 기여한다.
생성/변환 단계 | 주요 입력 | 주요 출력 | 수행 작업 |
|---|---|---|---|
프론트엔드 | 소스 코드 | 중간 표현(IR) | 구문 분석, 의미 분석, IR 생성 |
중간 단계 최적화 | 중간 표현(IR) | 최적화된 IR | 불필요 코드 제거, 루프 최적화, 상수 전파 등 |
백엔드 (코드 생성) | 최적화된 IR | 어셈블리 코드/기계어 | 명령어 선택, 레지스터 할당, 명령어 스케줄링 |
최종 출력 | 어셈블리 코드 | 목적 파일/실행 파일 | 어셈블리, 링킹 |
이 표는 중간 표현이 생성되고 최종 코드로 변환되는 일반적인 흐름을 보여준다. 각 단계는 이전 단계의 출력을 입력으로 받아 처리하며, 중간 표현은 프론트엔드와 백엔드를 연결하는 핵심 매개체 역할을 한다.
프론트엔드에서의 생성은 컴파일러나 인터프리터의 작업 흐름에서 소스 코드를 중간 표현 계층으로 변환하는 첫 번째 주요 단계이다. 이 과정은 일반적으로 어휘 분석, 구문 분석, 의미 분석 단계를 거쳐 완성된다. 구문 분석 단계에서 생성된 추상 구문 트리는 중간 표현의 초기 형태로 간주될 수 있으며, 이후 의미 분석을 통해 타입 정보와 심볼 테이블이 부착된다. 최종적으로 이 추상 구문 트리는 보다 표준화되고 저수준의 중간 표현으로 변환된다.
생성된 중간 표현의 형태는 도구의 설계 목표에 따라 달라진다. 고수준 중간 표현은 소스 코드의 구조와 의도를 많이 유지하여 분석과 리팩토링에 유리한 반면, 저수준 중간 표현은 기계어에 가까운 형태로, 레지스터 할당이나 명령어 선택과 같은 후속 최적화 및 코드 생성 작업에 적합하다. 프론트엔드는 특정 프로그래밍 언어의 고유한 문법과 의미 규칙을 처리하는 책임을 지니므로, 생성되는 중간 표현은 해당 언어의 모든 구문적 요소를 명확하게 표현할 수 있어야 한다.
프론트엔드 생성 과정에서의 주요 작업은 다음과 같다.
단계 | 입력 | 출력 | 주요 작업 |
|---|---|---|---|
어휘 분석 | 소스 코드 문자열 | 토큰(Token) 시퀀스 | 주석/공백 제거, 키워드/식별자/리터럴 식별 |
구문 분석 | 토큰 시퀀스 | 추상 구문 트리 | 문법 규칙 검증, 트리 구조 생성 |
의미 분석 | 추상 구문 트리 | 의미 정보가 부착된 AST | 타입 검사, 변수/함수 선언 확인, 유효 범위 분석 |
IR 생성 | 의미 정보가 부착된 AST | 중간 표현(IR) |
이 과정에서 오류가 발견되면, 프론트엔드는 적절한 진단 메시지를 생성하고 컴파일 과정을 중단할 수 있다. 성공적으로 생성된 중간 표현은 이후 미들엔드를 거쳐 다양한 최적화가 적용되거나, 백엔드에서 특정 하드웨어 플랫폼의 기계어나 다른 목표 언어로 변환된다.
중간 표현은 프론트엔드 단계에서 생성된 후, 백엔드 단계에서 목표 기계어나 바이트코드로 변환된다. 이 변환 과정은 일반적으로 명령어 선택, 레지스터 할당, 명령어 스케줄링 등의 하위 단계를 포함한다.
먼저 명령어 선택 단계에서는 중간 표현의 연산들을 목표 아키텍처가 실제로 지원하는 명령어 집합으로 매핑한다. 예를 들어, 중간 표현의 '곱하기' 연산이 특정 프로세서에서는 '시프트'와 '더하기' 명령어의 조합으로 구현될 수 있다. 이 단계는 주로 트리 패턴 매칭이나 DAG 기반 알고리즘을 사용하여 효율적인 명령어 시퀀스를 선택한다.
다음으로 레지스터 할당 단계가 수행된다. 중간 표현은 무한한 수의 가상 레지스터를 사용하지만, 실제 하드웨어는 제한된 수의 물리적 레지스터만을 제공한다. 이 단계에서는 그래프 채색 알고리즘을 응용한 방법 등으로 가상 레지스터들을 물리적 레지스터에 할당하고, 레지스터가 부족할 경우 스택 메모리로의 스필 작업을 삽입한다. 효율적인 레지스터 할당은 메모리 접근을 최소화하여 성능을 크게 향상시킨다.
마지막으로 명령어 스케줄링 단계에서는 생성된 명령어들의 실행 순서를 재배치하여 파이프라인 하드웨어의 효율성을 높인다. 이 과정은 의존성 그래프를 분석하여 데이터 의존성을 해치지 않으면서 명령어 지연 시간이나 프로세서의 기능 유닛 사용률을 최적화한다. 이 세 단계를 거쳐 중간 표현은 최종적으로 실행 가능한 기계어 코드로 변환된다.

중간 표현 계층에서 수행되는 최적화는 크게 지역 최적화와 전역 최적화로 구분된다. 지역 최적화는 기본 블록과 같이 제어 흐름이 분기 없이 순차적으로 실행되는 코드 영역 내에서 수행된다. 대표적인 기법으로는 상수 전파, 공통 부분식 제거, 불필요한 코드 제거 등이 있다. 이들은 주로 연산 결과를 미리 계산하거나 중복 계산을 제거하여 실행 시간을 단축하는 데 목적을 둔다.
전역 최적화는 함수나 프로그램 전체의 제어 흐름 그래프를 분석하여 수행된다. 데이터 흐름 분석을 기반으로 하는 루프 최적화, 인라인 확장, 전역 레지스터 할당 등이 이에 해당한다. 예를 들어, 루프 최적화에서는 루프 불변 코드 이동이나 강도 감소를 통해 반복적으로 실행되는 코드의 효율성을 높인다. 이러한 최적화는 지역 최적화보다 더 넓은 범위의 정보를 필요로 하며, 그만큼 더 복잡한 분석을 수반한다.
최적화 기법은 중간 표현의 형태에 따라 적용 방식이 달라진다. 그래프 기반 표현(예: SSA 폼)은 정의와 사용 관계를 명확히 추적하기 쉬워 데이터 흐름 분석에 유리하다. 반면, 선형 표현(예: 3-주소 코드)은 명령어 스케줄링이나 피어홀 최적화와 같은 저수준 최적화에 더 적합한 경우가 많다. 최적화의 순서도 중요하며, 일반적으로 비용이 적게 드는 지역 최적화를 먼저 수행한 후, 그 결과를 바탕으로 전역 최적화를 적용하는 계층적 접근이 흔히 사용된다.
최적화 유형 | 주요 기법 예시 | 분석 범위 | 목적 |
|---|---|---|---|
지역 최적화 | 상수 접기, 사용되지 않는 변수 제거 | 기본 블록 내부 | 불필요한 연산 제거, 코드 간소화 |
전역 최적화 | 함수 또는 프로그램 전체 | 실행 경로 개선, 메모리 접근 최소화 | |
루프 최적화 | 루프 구조 내부 | 반복문 실행 효율 극대화 |
지역 최적화는 중간 표현 계층 내에서, 주로 하나의 기본 블록 내부에 국한된 코드를 분석하고 개선하는 기법을 의미한다. 기본 블록은 분기 명령어 없이 순차적으로 실행되는 명령어들의 연속된 집합이다. 이 범위 내에서 컴파일러는 제한된 컨텍스트만을 고려하여 빠르고 효율적으로 변환을 수행할 수 있다.
주요 지역 최적화 기법으로는 상수 전파, 공통 부분 표현식 제거, 불필요한 코드 제거 등이 있다. 상수 전파는 변수가 알려진 상수 값을 가질 때, 그 변수의 사용을 상수로 직접 대체하는 과정이다. 공통 부분 표현식 제거는 동일한 계산이 블록 내에서 반복될 경우, 그 결과를 임시 변수에 저장하고 재사용함으로써 중복 연산을 제거한다. 불필요한 코드 제거는 실행 결과에 영향을 미치지 않는 할당문이나 죽은 코드를 제거한다.
이러한 최적화는 제어 흐름 그래프의 각 기본 블록을 독립적으로 처리하기 때문에 분석 비용이 상대적으로 낮다. 또한, 지역 최적화는 이후에 수행될 전역 최적화의 기초 단계 역할을 하기도 한다. 지역 최적화만으로도 상당한 성능 향상을 기대할 수 있으며, 특히 루프 내부의 기본 블록에 적용될 때 효과가 두드러진다.
최적화 기법 | 설명 | 예시 (최적화 전 → 후) |
|---|---|---|
상수 전파 | 변수를 알려진 상수 값으로 대체 |
|
공통 부분 표현식 제거 | 동일한 계산을 한 번만 수행하고 재사용 |
|
불필요한 코드 제거 | 결과에 영향을 주지 않는 문장 제거 |
|
전역 최적화는 중간 표현 계층 전체를 대상으로 수행되어 프로그램의 실행 효율을 종합적으로 향상시키는 기법이다. 지역 최적화가 기본 블록이나 프로시저 내부와 같은 제한된 범위에서 작동하는 반면, 전역 최적화는 함수나 모듈 전체, 때로는 프로그램 전체의 제어 흐름과 데이터 흐름을 분석하여 적용된다[4]. 이는 더 넓은 문맥 정보를 활용함으로써 지역 최적화만으로는 제거할 수 없는 중복 계산이나 데드 코드를 발견하고, 레지스터 할당 및 명령어 스케줄링을 더 효과적으로 수행할 수 있게 한다.
주요 전역 최적화 기법으로는 상수 전파, 공통 부분식 제거, 루프 최적화, 데드 코드 제거 등이 있다. 예를 들어, 상수 전파는 함수 전체를 분석하여 변수가 특정 상수값을 항상 가짐을 확인하고, 해당 변수를 사용하는 모든 곳을 그 상수로 대체한다. 공통 부분식 제거는 프로그램의 서로 다른 지점에서 반복적으로 계산되는 동일한 표현식을 찾아내어 한 번만 계산하고 그 결과를 재사용하도록 한다. 루프 최적화에는 루프 불변 코드 이동, 루프 융합, 루프 언롤링 등이 포함되어 반복문의 실행 오버헤드를 줄인다.
이러한 최적화를 수행하기 위해서는 제어 흐름 그래프(CFG)와 데이터 흐름 분석(DFA)이 필수적이다. 컴파일러는 CFG를 통해 프로그램의 실행 경로를 분석하고, DFA를 통해 변수 정의와 사용이 프로그램의 각 지점에서 어떻게 전파되는지 추적한다. 이 정보를 바탕으로 최적화가 프로그램의 의미를 변경하지 않으면서도 안전하게 적용될 수 있는 지점을 판단한다. 전역 최적화는 일반적으로 중간 표현 계층의 생성 후, 그리고 기계어 코드 생성 직전에 수행되는 중간 단계에서 이루어진다.
최적화 기법 | 주요 목적 | 분석 범위 |
|---|---|---|
상수 전파 | 변수를 알려진 상수값으로 대체하여 런타임 계산 제거 | 함수 또는 모듈 전체 |
공통 부분식 제거 | 동일한 계산의 중복 수행 방지 | 기본 블록을 넘어선 전체 경로 |
루프 불변 코드 이동 | 루프 내부에서 변하지 않는 계산을 루프 밖으로 이동 | 루프 및 그 주변 문맥 |
전역 레지스터 할당 | 자주 사용되는 변수를 레지스터에 장시간 유지 | 함수 전체의 라이브 범위 분석 |
전역 최적화는 계산 비용이 높은 과정이지만, 생성되는 코드의 품질과 성능에 지대한 영향을 미친다. 현대 컴파일러는 이러한 분석과 변환을 LLVM IR이나 유사한 중간 표현을 통해 효율적으로 구현한다.
