Compiler - Code Optimization
이번 포스트는 컴파일러의 5번째 단계인 Code Optimization에 대해 다룬다.
Code Optimization
Code Optimizer란?
- 컴파일러의 중요한 목표 중 하나 = 효율적인 코드 생성
- Source -> Target code로 번역할 때 효율을 개선할 수 있다.
- 보통 IR 단계에서 많이 수행된다.
- Source level에서 하기에는 너무 고수준이다.
- Target code에서 하기에는 너무 저수준이다.
- 목표
- 실행 시간을 감소, 메모리 사용량 감소, 코드 크기 감소, 전력 소비 감소
- 다만 프로그램의 semantic은 보존한다.
Baisc Blocks (기본 블록)
- 연속적인 명령어 집합으로 Jump target이나 Jump source가 없는 구간이다.
- Basic Block 내부에서는 항상 순차적으로 진행한다.
- 어떻게 찾을 수 있는가?
- Entry Point : 항상 처음 명령어부터 실행된다.
- Exit Point : Basic Block 중간에서는 절대 Jump가 발생하지 않는다.
- Jump가 발생했다면 그 부분이 Basic Block의 끝 부분
Control Flow Graps (CFG)
- Basic Block 간의 흐름을 Graph로 나타낸 것
- 대부분의 Optimization은 Basic Block 단위 또는 CFG 단위로 수행된다.
- Global 단위도 CFG를 기반으로 전역적인 분석을 수행해야 한다.
Local Optimizations
Common Subexpression Elimination
-
프로그램 내에서 같은 계산을 여러 번 하는 경우에 사용된다.
-
이미 계산이 되어있으니 한번 만 계산하고 이것을 재사용한다.
Q. Subexpression이란?
- 어떤 표현식 E가 코드 내 여러 위치에서 항상 동일한 값을 산출한다.
- 유효한 조건
- 해당 Subexpression을 구성하는 변수들의 값이 중간에 변경되지 않아야 한다.
- 변수의 정의가 도중에 변경되지 않아야 CSE가 가능하다.
-
구현하는 방법 ? : DAG를 사용한다.
- Available Expression Anlaysis를 활용한 방법으로도 대체할 수 있다.
Directed Acyclic Graphs (DAG)
- 방향성이 있는 비순환 그래프
- 표현식을 DAG로 표현하면 중복된 계산을 자연스럽게 공유할 수 있다.
- CSE를 위한 중복 검사 + 공유 저장이 가능한 자료구조
- 해쉬 테이블을 사용하여 구성한다.
Global과 Local 에서 다 사용되는 분석들
Available Expression Analysis
- 프로그램의 각 지점에서 어떤 Expression이 이미 계산되어 있고 그 결과가 유효한지를 분석하는 기법
- CSE를 구현할 때 DAG를 명시적으로 만들지 않고 사용이 가능하다.
- Available한다는 뜻
- 그 지점까지 오는 모든 경로에서 E가 이미 계산되어있다.
- 그 이후로 E에 사용된 변수들의 값이 변경되지 않다.
- Block간 (Global)에서 더 유용하게 쓰인다.
- 표현 방법
{a * a, b * t0}
- 기본적인 아이디어
- Generate : 블락 내부에서 계산되어 Available하게 되는 표현식들의 집합으로 조건이 만족되면 그 이후로 그 값을 재사용할 수 있다.
- Kill : 블락 내부에서 변수 값을 변경함으로써 특정 표현식을 무효화시키는 경우로 하나라도 변경하게 되면 더이상 유효하지 않다.(재사용할 수가 없다.)
- “Kill을 먼저 적용한 다음에 Gen을 적용해야 한다.”
X = X + 1과 같은 예시에서 활용
- 한계
- 현재 지점에서 어떤 표현식이 Available한지 분석은 가능하지만, 조금만 경로가 달라도 Available 판단이 불가능하다.
- 또한 변 assigned value가 redefined될 때 어떤 것을 넣어야할지도 판단이 불가능하다.
- => 새로운 접근 방법 : Reaching Definitions
Reaching Definitions
- Reaching : 현재 지점에 도달하는 모든 변수의 정의를 분석하는 것
- 현재 프로그램 포인트에서 변수 X의 값은 어디서 정의된 것인지를 추적한다.
- Copy Propagation, Dead Code Elimination에 필요하다.
- 표현 방법
{to = b + c, t1 = a * a }
Common Subexpression Elimination을 만족하기 위한 조건
- Expression E가 현재 시점에서 Available해야 한다.
- 이미 계산된 적이 있고, 그 이후로 사용된 변수들의 값이 변경되지 않았어야 한다.
- E의 결과를 들고 있는 유효한 변수가 존재해야 한다.
- 어떤 변수에 저장해놨다가 재사용한다.
- 즉, 그 변수가 현재 시점에서도 유효해야 한다. 그 변수에 대한 정의가 현재까지 Reach하고 있어야 한다.
Copy Propagation
- 복사된 변수 사용을 원래의 변수로 대체하는 최적화 기법이다.
x = y와 같은 코드가 있을 때, 이는 불필요한 변수 사용이다.- 레지스터 할당 시에도 도움이 안됨.
- 변수의 값이 중간에 변경하면 안되기 때문에 Reaching Definition Analysis를 기반으로 적용이 가능하다.
Dead Code Elimination
- Dead Code란? :
- 계산되기는 하지만 절대로 사용되지 않는 값을 말한다.
- 즉 제거가 가능하다.
- Dead Code인지 판단하기 위해서는 해당 변수가 향후 사용될 가능성이 있는지 없는지에 대한 확인이 필요하다.
- 이를 찾기 위한 방법이 Available Expression, Reaching Definition 으로는 할 수가 없다.
- 새로운 방법이 필요 => Live Variable Analysis
Live-Variable Analysis
- Live Variable이란?
- 현재 프로그램 지점에서 앞으로 실행 중에 그 변수 값이 참조(사용)될 가능성이 있는 변수
- 즉 변수 값이 현재 쓸모 있는 상태인지를 판단하는 것이다.
-
프로그램의 종료에서는 Live Variable이 없다.
- 계산 시 주의할 점
- Live Variable은 Backward로 계산해야 한다. (뒤에서 앞으로)
- 정의가 발생되면 해당 변수는 Kill 된다.
- 이미 있어도 새로 정의되면 이전 값은 사용될 수가 없다.
- RHS가 Constant이면 변수는 없다.
- 변수 값이 더 이상 사용되지 않을거라면 그 변수에 값을 넣는 것도 의미가 없다. -> 즉, 제거가 가능하다.
Arithmetic Simplification
- 산술 연산 중에서 결과가 뻔한 연산을 미리 단순하게 바꾸는 과정
- 수학적 성질을 이용해 연산 자체를 줄여서 최적화를 한다.
- 적용 예시
- x * 1 => x
- x + 0 => x
- 특징
- 안전한 최적화 (항상 적용이 가능하다.)
- 의미도 변하지 않는다.
- 부동소수점과 같은 경우 오차 문제를 조심해야 한다.
Constant Folding
- 컴파일 타임에 이미 값을 알 수 있는 상수 연산을 미리 계산해서 결과를 바꿔버리는 것을 의미한다.
- 실행 시가 아니라 컴파일 시점에 결과를 계산해서 실행시간을 절약할 수 있다.
-
예시)
x = 2 + 3=>x = 5 - RHS가 전부 상수 일 때만 가능하다.
Global Optimizations
- Local Optimizations 은 Basic Block 내에서만 중복된 계산을 제거하였다.
- Global Optimization은 Basic Block을 넘어 전체 Control Flow Graph(CFG)에서도 중복된 계산을 제거한다.
- 왜 필요한가?
- 많은 경우 중복 계산은 Block 사이에서도 발생한다.
In & OUT set
- IN[B] : Block B 시작 지점에서의 Data-flow 정보
- OUT[B] : Block B 끝난 후의 Data-flow 정보
- 여기서 Data- flow 정보란? : set of variables
Dead-Code Elimination (in Global Optimization) 과정 설명
- 전체 CFG에 관하여 Live Variable Analysis 수행
- IN / OUT set을 계산한다.
- 각 Blockd에서 Local analysis 와 Optimize를 수행한다.
1번과 2번step을 변화가 없을 때까지 수행한다.
Global Data-Flow Analysis
| Available Expressions | Reaching Definitions | Live Variables | |
|---|---|---|---|
| Domain | Sets of expressions | Sets of definitions | Sets of variables |
| Application | Common subexpression elimination | Common subexpression elimination Copy Propagation |
Dead code elimination |
| Direction | Forwards | Forwards | Backwards |
| Boundary | OUT[ENTRY] = ∅ | OUT[ENTRY] = ∅ | IN[EXIT] = ∅ |
Availalbe Expressions
- 현재 시점에서 어떤 expression이 Availble 한지?
-
Direction : Forwards (앞으로 진행)
OUT[ENTRY] = ∅(프로그램 시작 전에는 아무 expression도 available 하지 않다.)
Reaching Definitions
- 어떤 변수 정의가 현재 시점에 Reach 했는지?
- Direction : Forwards (앞으로 진행)
OUT[ENTRY] = ∅(프로그램 시작 전에는 아무 expression도 available 하지 않다.)
Live Variables
- 현재 시점에서 앞으로 참조될 가능성이 있는 변수 집합
- Direction : Backwards (뒤에서 앞으로 진행)
IN[EXIT] = ∅(프로그램의 끝에는 Live한 변수가 없기 때문에)
Global Live-Variable Analysis
\[IN[B] = use_B ∪ (OUT[B] - def_B)\]- Use_B : Block B 내부에서 그 변수 값이 사용된 변수들의 집합
- 해당 Block 내에서 사용한 적이 있는 변수
- RHS에 사용된 적이 있는 변수
- Def_B : Block B 내부에서 정의된 변수들의 집합
- 해당 Block 내에서 새로 값이 할당된 변수들
해석하면 다음과 같다.
IN[B] 수식은 “Block 내부에서 사용하는 변수들” + “Block 이후에서 여전히 Live한 변수 중에서, B에서 다시 정의되지 않은 변수들”
=> 이를 통해 Dead Code Elimination, Register Allocation 등에서 변수의 Live 여부를 정확하게 판단이 가능하다.
- Global Live-Variable Pesudo Code
IN[EXIT] = ∅
for (each basic block B other then EXIT) IN[B] = ∅;
while (changes to any IN occur)
for (each basic block B other then EXIT) {
OUT[B] = U IN[S]
IN[B] = use_B U (OUT[B] - def_B)
}
Global Reaching Definition Analysis
\[OUT[B] = gen_B ∪ (IN[B] - kill_B)\]- Gen_B : Block B 내부에서 정의된 정의들의 집합
- 즉 B가 끝날 때 이 정의는 무조건 Reaching 하고 있다.
- Block B 내부에서 정의된 변수들의 정의
- Kill_B : Block B 내부에서 정의가 kill 되는 정의들의 집합
- 어떤 변수 x를 정의하고, x에 대한 다른곳에서 온 모든 정의는 kill 된다.
해석하면 다음과 같다.
OUT[B] 수식은 “B 내부에서 새로 정의한 것” 과 “B 이전에서 온 정의들 중에서 B에서 다시 정의되지 않은 것”을 합친 것을 말한다.
- 현재 프로그램의 어떤 지점에서 어떤 변수의 정의가 도달(Reaching)하고 있는가를 분석하는 것
- Copy Propagation, Dead Code Elimination 에 사용된다.
- Global Reaching Definition Pesudo Code
OUT[ENTRY] = ∅
for (each basic block B other then ENTRY) OUT[B] = ∅
while (changes to any OUT occur)
for (each basic block B other than ENTRY) {
IN[B] = U OUT[P]
OUT[B] = gen_B U (IN[B] - kill_B)
}
Global Available Expressions Analysis
\[OUT[B] = e_gen[B] ∪ (IN[B] - e_kill[B])\]- e_gen[B] : Block B 내부에서 생성된 표현들의 집합
- Block B를 통과하고 나면 이 표현식은 Available 하다.
- 즉, B 내부에서 계산이 완료된 표현식들
- e_kill[B] : Block B 내부에서 Kill된 표현식들의 집합
- RHS에 등장한 변수들 중, LHS에 정의된 변수와 관련된 표현식을 kill 한다.
해석하면 다음과 같다.
OUT[B] 수식은 “Block B 내부에서 계산 완료된 표현식들” 과 “Block 이전에서 Available했던 표현식들 중에서 B 내부에 kIll되지 않은 표현식들”의 합집합이다.
주의할 점
i = i + 1 처럼 단순한 계산은 맞지만, 표현식의 결과가 변수에 저장되지 않는 표현식들은 e_gen에 포함되지 않는다.
- Global Available Expressions Pesudo Code
OUT[ENTRY] = ∅
for (each basic block B other than ENTRY) OUT[B] = U;
while (changes to any OUT occur)
for (each basic block B otehr then EMPTY) {
IN[B] = Intersection OUT[P]
OUT[B] = e_gen_B U (IN[B] - e_kill_B)
}
다음 포스트로는 컴파일러의 마지막 단계인 Code Generator에 대해 다룰 예정이다.