[컴파일러] 중간 언어 (IR)

👑 중간 언어 (Intermediate Representation, IR)란

중간 언어란 컴파일러의 전단부와 후단부를 연결해 주는 중간 형태의 코드를 의미한다.

초기의 컴파일러는 단일 패스 컴파일러로서 소스 프로그램을 중간 코드를 거치지 않고,

직접 목적 코드로 번역하였다. 이후 컴파일러에 대한 이론이 발전함에 따라서 점차 번역

단계가 세분화되었고, 컴파일러를 여러 모듈로 나누어 설계하게 되었다. 따라서 각 모듈들을

연결해 주는 중간 코드들이 필요하게 되었다.

컴파일러에서 중간 언어를 사용함으로써 얻을 수 있는 장점들은 다음과 같다.

  • 컴파일러를 여러 독립적 모듈로 구성 가능

  • 컴파일러 자체의 이식성 증가

  • 중간 코드를 통한 최적화를 수행하여 효율적인 최적화 수행 가능

이외에도 여러 가지 장점들을 가지고 있지만, 목적 코드로 직접 번역하는 것 보다 컴파일 시간이

더 많이 소요되고, 비효율적인 코드를 생산할 수 있다는 단점 또한 존재한다. 하지만, 중간 언어

단계에서의 최적화를 통해 비효율적인 코드 생산 문제를 극복할 수 있다.



💡 High-Level IR & Low-Level IR

중간 언어는 High-Level IRLow-Level IR로 나뉘며, 각자 다른 목적과 표현력을 가진다.

  • High-Level IR

    • 프로그래밍 언어의 구조와 더 유사하게 설계되어 있다.

    • 함수, 조건문, 루프 등 고수준 언어의 표현력이 유지되어 고수준의 최적화를 수행하기 좋다.


  • Low-Level IR

    • 기계 코드에 더 근접한 형태로, 어셈블리로의 번역이 용이하다.

    • N-tuple Representation, Stack Machine Code, Tree Representation 등이 있다.


N-tuple Representation

IR을 튜플 형식으로 표현하는 방식, 일반적으로 한 줄에 하나의 명령을 나타낸다.

N개의 요소로 이루어진 튜플 구조를 사용하며, 명령의 연산자와 피연산자를 명시적으로 기술한다.

형태: (Operation, Operand1, Operand2, Result)
예시: ADD R1, R2, R3 → (ADD, R1, R2, R3)



💡 Three-address code

Three Address Code (TAC)는 컴파일러에서 코드 생성 과정을 간소화하기 위해 사용되는

IR 중 하나이다. 복잡한 표현식을 최대 세 개의 주소(두 개의 피연산자와 하나의 결과)로 이루어진

간단한 단계들로 분해하여 표현한다.

TAC에서의 결과는 항상 컴파일러가 생성한 임시 변수에 저장된다. 이러한 설계는 실행 중의 연산 순서를

명확히 하며, 코드 최적화와 기계어로의 번역을 용이하게 한다.

컴파일러는 TAC를 다음과 같이 활용한다.

  • 최적화

    • TAC는 컴파일 과정 중 최적화 단계에서 중간 표현으로 자주 사용된다.

    • TAC를 통해 컴파일러는 코드를 분석하고, 최적화를 수행할 수 있다.

  • 코드 생성

    • TAC는 코드 생성 단계에서도 중간 표현으로 사용된다.

    • 컴파일러가 목표 플랫폼에 특화된 코드를 생성하도록 돕는 동시에, 생성된 코드가 올바르고
      효율적임을 보장한다.

  • 디버깅

    • TAC는 저수준 언어이기 때문에, 최종 생성된 기계어보다 읽고 이해하기 쉬운 경우가 많다.

    • 이를 통해 개발자가 프로그램의 실행 과정을 추적하고, 발생할 수 있는 오류나 문제를 식별할 수 있다.

  • 언어 변환

    • TAC는 한 프로그래밍 언어에서 다른 언어로 코드를 변환하는 데 사용될 수도 있다.

    • 코드를 공통된 중간 표현으로 변환하면, 여러 목표 언어로 코드를 변환하는 작업이 더 쉬워진다.



예시

x = (y + z) × (-e)를 TAC로 표현하면 다음과 같다.

t1 = y + z
t2 = -e
x = t1 × t2



💡 LLVM IR

LLVM(Low Level Virtual Machine) IR은 현대 컴파일러에서 가장 널리 사용되는 중간 표현

중 하나로 LLVM 프로젝트의 핵심 요소이다. LLVM은 모듈식 컴파일러 인프라로, 여러 언어와 플랫폼을

지원하며, LLVM IR은 이 인프라에서 코드 분석과 최적화를 수행하는 기본 단위이다.


예시 코드

다음은 a = b + c를 LLVM IR로 표현한 예시이다.

define i32 @add_example(i32 %b, i32 %c) {
entry:
  %add_result = add i32 %b, %c // 로컬 변수는 `%`를, 전역변수는 `@`를 사용
  ret i32 %add_result
}



💡 Java Bytecode

Java Bytecode는 Java 프로그램이 실행되기 위해 Java Virtual Machine (JVM)에서 사용하는

중간 표현 언어이다. Java 컴파일러는 Java 소스 코드를 컴파일하여 바이트코드로 변환하며, 이

바이트코드는 플랫폼 독립적으로 설계되어 다양한 환경에서 실행 가능하다.


  • 플랫폼 독립성

    • Java Bytecode는 JVM이 설치된 모든 환경에서 동일하게 동작한다.

    • 이는 “Write Once, Run Anywhere”라는 Java의 철학을 실현한다.

  • 스택 기반 명령어

    • Java Bytecode는 스택 기반으로 동작하며, 명령어들이 JVM의 스택에서 데이터를 push/pop
      하며 연산을 수행한다.


예시 코드

다음은 Java 코드와 해당 Java Bytecode를 비교한 예시이다.

public int add(int a, int b) {
    return a + b;
}


Java Bytecode

public int add(int, int);
  Code:
     0: iload_1       // 로컬 변수 a를 스택에 로드
     1: iload_2       // 로컬 변수 b를 스택에 로드
     2: iadd          // 두 값을 더함
     3: ireturn       // 결과를 반환



👑 정리

중간 언어 (IR, Intermediate Representation)는 컴파일러 설계에서 필수적인 역할을 담당하며,

고수준 언어와 저수준 기계어 사이를 연결하는 다리같은 역할을 수행한다. IR은 컴파일러의 전단부

(Front-End)와 후단부(Back-End) 사이에서 정보의 전달 및 변환을 효과적으로 수행하며, 다음과 같은

핵심 역할을 통해 컴파일러의 성능과 확장성을 극대화한다.

  • 컴파일러의 모듈화

    • IR을 도입함으로써 컴파일러를 여러 독립적 모듈로 나눌 수 있어, 유지보수와 확장이 용이해진다.
  • 최적화

    • IR은 코드 최적화의 핵심 단계에서 활용되며, 효율적인 실행 코드를 생성하기 위해 사용된다.
  • 플랫폼 독립성

    • IR은 소스 코드와 기계 코드의 차이를 추상화하여, 다양한 플랫폼에서 동일한 코드의 실행을
      가능하게 한다.


중간 언어는 High-Level IR에서 Low-Level IR까지 그 표현력과 목적에 따라 다양한 형태를 가지며,

컴파일러뿐만 아니라 언어 번역, 코드 분석, 디버깅 등 여러 분야에서도 활용된다. 효율적이고, 확장 가능한

컴파일러의 설계에 있어 IR은 중요한 역할을 담당하고 있다.

Categories:

Updated:

Leave a comment