mojo's Blog
Optimization 본문
Directed Acyclic Graph (DAG)
자료 흐름 분석이 완료된 후 기본 블록에 대한 코드를 생성할 때, 각 블록에 대해 DAG(directed acyclic graph)
생성하여 활용한다.
● DAG는 값이 어떻게 계산되는지를 그림으로 나타내준다.
● DAG는 주로 블록 내부에서 사용되지만, 블록 외부에서 어떤 문장이 그 문장에서 계산한 값을 사용하는지
알아볼 수 있으므로 매우 중요하다.
DAG는 모든 산술식에 대한 노드를 가진다.
● 내부 노드(interior node)는 연산자를 나타내고 그 자식 노드는 피연산자를 나타낸다.
● 터미널 노드는 id나 constant이고 중간 노드는 연산자이다.
● 노드 옆에 여러 개의 id를 놓을 수 있으며, 이는 노드의 계산 결과를 이 id들이 공유함을 나타낸다.
[알고리즘] DAG
● [입력] 기본 블록
● [출력] 터미널 노드 레이블은 id 혹은 constant, 내부 노드는 op인 기본 블록에 대한 DAG
● [방법]
*생성되는 노드는 1개나 2개의 자식 노드를 가지게 된다.
1개의 자식 노드를 가질 경우 ‘왼쪽’과 ‘오른쪽’ 자식 노드로 구분된다.
* x = y op z, x = op y, x = y 세 가지 중 하나의 3-주소 코드가 주어졌다고 가정한다.
* if i <= 20 goto _ 와 같은 경우에 는 x = y op z에서 x가 정의되지 않은 것으로 간주한다.
DAG 구성은 다음 1-3단계를 블록의 각 문장에 대해 차례로 수행하면 된다.
1. x = y op z의 경우에는 만약 node(y)와 node(z)가 정의되어 있지 않다면 터미널 노드 y와 z를 생성한다.
2.
● x = y op z의 경우에는 왼쪽 자식이 node(y)이고 오른쪽 자식이 node(z)이면서 똑같은 노드 op가 존재하는
지 알아보고, 만약 존 재하지 않으면 새 노드 node(op)를 생성한다. 이때 부모 노드는 이미 만들어진 노드를
찾거나 아니면 새롭게 생성되는 노드가 된 다.
● x = op y의 경우에는 node(y) 하나만을 자식으로 가지고 있는 노드 op가 존재하는지 알아보고, 만약 존재하지
않으면 새 노드를 생성한다. 이때 부모 노드는 이미 만들어진 노드를 찾거나 아니면 새롭게 생성되는 노드가 된다.
● x = y의 경우에는 새로운 노드를 생성하지 않는다.
3. 2단계에서 찾은 부모 노드에 있는 id 리스트에 x를 첨가한다.
Example
치환문 x = (x + 1) * (x + 1)에 대해 3-주소 코드로 변환하고 [알고리즘]를 적용 하여 DAG를 그려보자.
3-address code
t1 = x + 1
t2 = x + 1
t3 = t1 * t2
x = t3
Flow Analysis (간략하게만 보고 넘어가기)
Code optimizer:
● 프로그램의 전체적인 정보를 모으고, 이 정보를 흐름 그래프의 각 블록에 제공
● 최적화에 이용하기 위해 제어 흐름에 관한 정보를 기본 블록의 집합에 정리
자료 흐름 분석: 프로그램이 실행되었을 때 어떤 일이 발생할지 추론하여 발견하는 것
● 즉 프로그램 안에서 사용되는 변수의 통로에 관한 정보를 모으는 처리 과정
● 유효 변수(live variables), 공통 부분식 등에 대한 정보를 모으는 일을 한다.
정적 분석법(static analysis):
● 최적화의 적용되는 위치를 파악하고,
● 어떤 지점에서 코드를 변경했을 때 안전한지 여부를 증명하는 것
변수 값이 다음에 언제 사용될지를 아는 것은 좋은 코드를 생성하는 데 필수적
3- 주소 코드 x = y * z에서
● x를 정의(define), y와 z를 사용(use)이라고 부른다
● 변수 x, y, z가 다음에 어디서 사용될지를 아는 것은 매우 중요한 정보
● 어떤 변수가 다음에 어디서 사용될 것인가를 다음 사용(next-use) 정보라 한다.
그럼 다음에 사용될 y의 값은 프로그램 어디서 정의된 값인가?
● 이 질문의 답을 구하려면 ‘UD 체인(use-definition chaining)’ 계산을 수행해야 한다.
● 이 분석 방법은 피연산자로 각 단순 변수가 나타날 때마다 해당하는 UD 체인을 제공한다.
○ UD 체인은 프로그램의 어느 장소에 변수의 정의가 올바르게 나타나는지를 알려준다.
Peephole Optimization
Peephole (window): 연속적인 명령어들의 집합
Peephole optimization:
● 몇 개의 연속적인 명령어를 하나의 명령어나 더 짧은 명령어로 변환
● 일반적으로 목적 코드가 생성된 다음 수행
중복 명령어 제거
중복되는 로드(LOAD) 명령문이나 저장(STORE) 명령문을 제거하는 방법
예시)
LOAD R1, x // x을 R1에 적재한다.
STORE x, R1 // R1을 x에 저장한다.
저장 명령어를 삭제할 수 있다.
x의 값이 이미 레지스터 R1에 적재되어 있기 때문이다.
Example
다음 목적코드에 대해 중복된 명령어가 있는지 찾아보자.
LOAD R1, y
STORE x, R1
LOAD R1, x
ADD R1, 10
STORE z, R1
R1 = y ( <- )
ㅌ = x ( -> )
R1 = x ( <- ) LOAD R1, x 를 제거하기
도달 불가능한 코드 제거
불필요한 코드(useless code)
● 도달 불가능한 코드
● 죽은 코드
도달 불가능한 코드는 무조건 분기 문장 바로 다음에 오는 라벨이 없는 명령어 같은 것으로, 이는 도달할 수 없기
때문에 제거할 수 있다.
Example
다음 3-주소 코드에 있는 도달 불가능한 코드를 제거해보자.
if A = B then goto L3
A = 2
GOTO L5
L3: if A = B then goto L5
A = 3
L5:
L3 라벨에서 무조건 A = B 이기 때문에 L5 으로 무조건 이동한다.
따라서 라벨이 없는 명령어로 A = 3 제거하기
제어 흐름 최적화
중간 코드 생성은 분기에 대한 분기, 조건 분기에 대한 분기, 분기에 대한 조건 분기를 생성하는데 이러한 불필요한
분기들을 제거할 수 있다.
다음 코드를 살펴보자.
goto L5
⋮
L5 : goto L1
이 코드는 다음과 같이 변환할 수 있으며, 이렇게 함으로써 실행 시간을 줄일 수 있다.
goto L1
⋮
L5 : goto L1
대수학적 간소화
수학적인 대수 법칙을 이용하여 식을 간소화하는 방법
x = y + 0 ⇒ x = y (덧셈의 항등 법칙)
x = 0 + y ⇒ x = y (덧셈의 항등 법칙)
x = y – 0 ⇒ x = y (뺄셈의 항등 법칙)
x = y * 1 ⇒ x = y (곱셈의 항등 법칙)
x + y ⇒ y + x (덧셈의 교환 법칙)
교환 법칙을 만족하는 연산자를 필요할 때마다 적용하여 식을 간소화할 수 있다.
예를 들어 a = 3 * b / 3은 *가 교환 법칙을 만족하여 a = b * 3 / 3이므로 a = b * 1이 되고 결국 a = b가 된다.
세기 감축 (strength reduction)
수행 비용이 높은 연산자를 수행 비용이 낮은 연산자로 대체하는 것
거듭 제곱 연산자는 곱셈 연산자로, 곱셈 연산자는 덧셈 연산자로, 나눗셈 연산자는 곱셈 연산자로 바꾼다.
Example
다음 문장에 대해 각각 세기 감축을 해보자.
y = x ^ 2 => y = x * x
y = x * 2 => y = x + x
y = x / 5 => y = x * 0.2 (곱셈이 훨씬 효율적)
x * 3 = x + x + x <= 이거는 다루지 않음(x * 2 만 알아두기)
하드웨어 명령어 사용
● 어떤 특정 연산을 효율적으로 구현하기 위한 하드웨어 명령어 포함
● 어떤 기계는 자동 증가(auto-increment)와 자동 감소(auto-decrement) 모드는 피연산자로부터 값을
사용하기 전이나 후에 피연산자에 1을 증가시키거나 감 소시킬 때 사용하는데, 하드웨어로 구현하기 때문에
매우 큰 비용을 절감
Local Optimization
지역최적화Local Optimization
● 부분적인 관점에서 비효율적인 코드를 구분해내고 좀 더 효율적인 코드로 개선하는 방법
● 기본적으로 기본 블록 안에서 최적화가 이뤄짐
공통 부분식 제거
● 공통 부분식은 프로그램에서 공통된 부분이 여러 번 나타나는 경우
● 공통 부분식이 한 번만 계산되도록 처리
Example
다음 치환문에서 공통 부분식을 제거해보자.
A = B + C + D;
E = B + C + F;
K = (B + C) * G;
B + C 부분이 공통으로 들어가는 것을 알 수 있음.
따라서 T1 = B + C 저장한다.
A = T1 + D;
E = T1 + F;
K = T1 * G;
복사 전파
● 치환문 f = g가 나타난 이후에 가능하면 f 대신에 g를 사용하는 것.
● 즉 치환문을 삭제하고 삭제된 치환문의 l-값 대신에 r-값을 사용하는 방법
Example)
x = t3;
a[t2] = t5;
a[t4] = x;
goto B2;
a[t4] = t3; 으로 변경한다.
죽은 코드 제거: 어떤 변수가 특정 프로그램 지점 이후 전혀 사용하지 않는 값을 계산하는 문장을 말한다
Example) 다음 코드에서 죽은 코드를 제거해보자.
x = t3;
a[t2] = t5;
a[t4] = t3;
goto B2;
x = t3; 을 제거하면 된다.
상수 폴딩
컴파일 시간에 상수를 포함하는 연산이 계산될 수 있으면 계산을 미리 함으로써 코드를 줄이는 방법
Example) 다음 치환문에서 상수 폴딩을 해보자.
X = 3.14;
Y = 2 * X;
Y = 6.28; 으로 바로 변환하는 것임
Loop Optimization
루프 최적화Loop Optimization
● 실행 시간의 대부분을 차지하므로 최적화에서 매우 중요한 부분
● 코드 이동, 세기 감축, 귀납 변수 최적화, 루프 융합, 루프 교환, 루프 전개
코드 이동 루프 수행 횟수와 상관없이 항상 같은 값을 가진 루프 불변(loop invariant)이 있는 경우,
루프 불변을 그 루프의 바로 전 위치로 이동
Example
다음 반복문에서 코드 이동을 해보자.
for(k = 1; k <= 1000; k++)
c[k] = 2 * (p - q) * (n - k + 1) / (sqrt(n) + n);
(p - q)랑 (sqrt(n) + n) 를 루프 밖으로 빼서 변수로 설정해가지고 하면 좋을듯 하다.
a = p - q;
b = sqrt(n) + n;
f = n - 1;
for(k = 1; k <= 1000; k++)
c[k] = 2 * a * (f - k) / b;
다른 방법으로 ...
a = p - q;
b = sqrt(n) + n;
f = n + 1;
d = 2 * a / b;
for(k = 1; k <= 1000; k++)
c[k] = d * (f - k);
와 같은 방법도 존재한다.
귀납 변수: 루프를 돌 때마다 값이 일률적으로 변하는 변수
귀납 변수 최적화
● 세기 감축으로 최적화할 수 있다.
○ ex) 반복문 제어 변수, 반복문 제어 변수에 특정 형태로 의존하는 변수들
● 선택된 귀납 변수는 레지스터에 할당되어 보다 효율적으로 계산 가능
Example
3-주소 코드의 기본 블록에 대해 귀납 변수 최적화를 해보자.
L1 : j = j - 1
t4 = 4 * j
t5 = a[t4]
if t5 > v goto L1
t4 = 4 * j
L1 : t4 = t4 - 4
t5 = a[t4]
if t5 > v goto L1
루프 융합
● 루프의 오버헤드를 줄이기 위한 방법
● 루프의 범위가 같을 경우, 여러 개의 루프를 하나의 루프로 만듬
Example) 다음 문장에 대해 루프 융합을 해보자.
for (j = 1; j <= 100; j++)
a[j] = b[j] + c[j];
for (j = 1; j <= 100; j++)
b[j] = 5 + c[j];
for(j = 1; j < = 100; j++)
a[j] = b[j] + c[j];
b[j] = 5 + c[j];
루프 교환
● 내부 루프와 외부 루프를 교환
Example) 다음 문장에 대해 루프 교환을 해보자.
for (j = 1; j <= 100; j++)
{
for (k = 1; k <= 200; k++)
{
a [j,k] = j + k;
}
}
위 경우에는 row major 일 경우 효율적인 루프문이다.
column major 일 경우를 고려하여 루프 교환을 하면 아래와 같다.
for (k = 1; k <= 100; k++)
{
for (j = 1; j <= 200; j++)
{
a [j,k] = j + k;
}
}
루프 전개
● 루프의 계산을 빠르게 하기 위해 루프를 펼치는 방법
● 증가와 조건 검사의 횟수가 감소하기 때문에 수행되는 명령어의 수가 줄어듦
○ 제어 변수의 최종 값을 증가시킬 수도 있고, 제어 변수의 값을 바꿔서 구현할 수도 있음
○ store라든가 조건부 분기 명령(conditional jump)은 선행 제어를 어렵게 하므로 이런 경우 효과가
반감될 수 있음
Example
다음 문장에 대해 루프 전개 해보자.
int x;
for (x = 0; x < 100; x++)
{
delete(x);
}
루프 전개는 다음과 같다
int x;
for(x = 0; x < 100; x +=5)
{
delete(x);
delete(x + 1);
delete(x + 2);
delete(x + 3);
delete(x + 4);
}
Global Optimization
전역 최적화 기법
● 프로그램의 전체적인 흐름 분석을 통해 좀 더 효율적인 코드로 만드는 방법
● 지역 최적화와는 달리 기본 블록 간 정보와 흐름 그래프를 이용하고 프로그램 의 전체적인 흐름 분석을 통한
최적화 기법
전역적 공통 부분식 제거
기본 블록 간에 나타나는 공통 부분식에 대해서 단 한 번만 계산하도록 함으로써 코드를 개선
*좀 더 최적화하면 세기 감축이 가능하다. x3 = a / 5 + t를 x3 = a * 0.2 + t로 최적화할 수 있다.
상수 폴딩
하나의 기본 블록 내에 어떤 변수에 대한 정의가 존재하지 않는다면?
이 블록으로 도달할 수 있는 이전 블록 어 딘가에 그 변수에 대한 정의가 있을 것.
이러한 특성을 이용하여 기본 블록 간 상수 폴딩을 수행
도달 불가능한 코드 제거
흐름 그래프의 입구에서부터 도달할 수 없는 기본 블록은 제거가 가능
앞서 상수 폴딩 예시의 경우에는 제어문 이 항상 true이므로 false로 가는 것을 제거할 수 있다.
'학교에서 들은거 정리 > 기초컴파일러구성' 카테고리의 다른 글
Target code generation - (2) (0) | 2021.12.08 |
---|---|
Target code generation - (1) (0) | 2021.12.03 |
Parameter Passing, Optimization (0) | 2021.11.29 |
Structural data type, Runtime Environment (0) | 2021.11.26 |
Intermediate Code Generation (0) | 2021.11.25 |