- Authors

- Name
- Youngju Kim
- @fjvbn20031
개요
데이터 흐름 분석(Data-Flow Analysis)은 컴파일러 최적화의 핵심 기반 기술입니다. 프로그램의 각 지점에서 특정 성질에 대한 정보를 수집하여, 안전한 최적화가 가능한지 판단합니다.
이번 글에서는 데이터 흐름 분석의 기본 개념, 세 가지 대표적인 분석(도달 정의, 활성 변수, 가용 수식), 반복 알고리즘, 그리고 격자 이론의 기초를 다룹니다.
1. 데이터 흐름 분석의 기본 개념
1.1 분석의 목표
프로그램의 각 지점(point)에서 다음과 같은 질문에 답합니다:
- 변수 x의 값은 어디서 정의되었는가?
- 변수 x의 값이 이후에 사용되는가?
- 수식
a + b가 이미 계산되어 사용 가능한가?
1.2 프로그램 지점
기본 블록의 각 명령어 앞과 뒤를 프로그램 지점이라 합니다.
// 블록 B:
// [지점 1] d1: x = a + b [지점 2]
// [지점 2] d2: y = x * c [지점 3]
//
// IN[B] = 지점 1에서의 정보
// OUT[B] = 지점 3에서의 정보
1.3 전달 함수 (Transfer Function)
각 기본 블록 B에 대해 전달 함수 f_B는 입력 정보를 출력 정보로 변환합니다:
OUT[B] = f_B(IN[B])
대부분의 데이터 흐름 분석에서 전달 함수는 gen/kill 형태입니다:
OUT[B] = gen[B] U (IN[B] - kill[B])
gen[B]: 블록 B에서 새로 생성되는 정보kill[B]: 블록 B에서 무효화되는 정보
1.4 합류 연산 (Meet/Join Operation)
한 블록에 여러 선행 블록이 있을 때, 각 선행 블록의 정보를 합치는 연산입니다.
// 합집합(Union): 어느 경로에서든 성립하면 포함
IN[B] = U OUT[P] (P는 B의 모든 선행자)
// 교집합(Intersection): 모든 경로에서 성립해야 포함
IN[B] = n OUT[P] (P는 B의 모든 선행자)
2. 도달 정의 (Reaching Definitions)
2.1 개념
정의 d가 프로그램 지점 p에 도달한다 = d에서 p까지의 경로가 존재하며, 그 경로에서 같은 변수가 다시 정의되지 않음.
2.2 gen/kill 집합
명령어 d: x = y op z에 대해:
gen = 정의 d 자체
kill = x에 대한 모든 다른 정의
블록 B 전체의 gen/kill:
// 블록 B에 d1, d2, ..., dk 명령어가 순서대로 있을 때
// 뒤의 명령어가 앞의 명령어를 덮어쓸 수 있음
gen[B] = gen(dk) U (gen(dk-1) - kill(dk)) U ...
kill[B] = kill(d1) U kill(d2) U ... U kill(dk)
2.3 데이터 흐름 방정식
OUT[B] = gen[B] U (IN[B] - kill[B])
IN[B] = U OUT[P] (P는 B의 모든 선행자)
합류 연산이 합집합인 이유: 어떤 경로를 통해서든 정의가 도달하면 그 값을 사용할 가능성이 있으므로, 보수적으로 모두 포함해야 합니다.
2.4 예시
// 흐름 그래프:
// B1 -> B2 -> B3
// ^ |
// +-----+
// B1: d1: x = 5 gen={d1}, kill={d2,d4}
// B2: d2: x = x + 1 gen={d2}, kill={d1,d4}
// B3: d3: y = x gen={d3}, kill={}
// d4: x = y + 1 gen={d4}, kill={d1,d2}
// gen[B3] = {d4} U ({d3} - {d1,d2}) = {d3, d4}
// kill[B3] = {d1, d2}
반복 계산:
초기값: OUT[B] = {} (모든 B에 대해)
반복 1:
IN[B1] = {}
OUT[B1] = {d1} U ({} - {d2,d4}) = {d1}
IN[B2] = OUT[B1] U OUT[B3] = {d1} U {} = {d1}
OUT[B2] = {d2} U ({d1} - {d1,d4}) = {d2}
IN[B3] = OUT[B2] = {d2}
OUT[B3] = {d3,d4} U ({d2} - {d1,d2}) = {d3,d4}
반복 2:
IN[B2] = OUT[B1] U OUT[B3] = {d1} U {d3,d4} = {d1,d3,d4}
OUT[B2] = {d2} U ({d1,d3,d4} - {d1,d4}) = {d2,d3}
IN[B3] = OUT[B2] = {d2,d3}
OUT[B3] = {d3,d4} U ({d2,d3} - {d1,d2}) = {d3,d4}
반복 3:
IN[B2] = {d1} U {d3,d4} = {d1,d3,d4} (변화 없음)
-> 수렴!
3. 활성 변수 분석 (Live-Variable Analysis)
3.1 개념
변수 x가 프로그램 지점 p에서 **활성(live)**이다 = p에서 시작하는 어떤 경로에서 x가 재정의되기 전에 사용됨.
3.2 역방향 분석
활성 변수 분석은 역방향(backward) 분석입니다. 프로그램의 끝에서 시작으로 거슬러 올라갑니다.
// 전달 함수 (역방향)
IN[B] = use[B] U (OUT[B] - def[B])
OUT[B] = U IN[S] (S는 B의 모든 후속자)
use[B]: 블록 B에서 정의되기 전에 사용되는 변수def[B]: 블록 B에서 정의되는 변수
3.3 use/def 집합 계산
// 블록 B:
// x = y + z // use: {y, z}, def: {x}
// a = x + 1 // use: {x}, def: {a} (x는 위에서 정의되었으므로 use[B]에 포함 안됨)
// use[B] = {y, z} (정의 전에 사용된 변수)
// def[B] = {x, a} (정의된 변수)
3.4 예시
// B1: a = 1; b = 2
// B2: c = a + b; d = c - a
// B3: d = b + d; (출구)
// use[B1] = {}, def[B1] = {a, b}
// use[B2] = {a, b}, def[B2] = {c, d}
// use[B3] = {b, d}, def[B3] = {d}
// 초기값: IN[B] = {} (모든 B에 대해)
반복 1: (역방향: B3 -> B2 -> B1)
OUT[B3] = {} (출구 블록)
IN[B3] = {b, d} U ({} - {d}) = {b, d}
OUT[B2] = IN[B3] = {b, d}
IN[B2] = {a, b} U ({b, d} - {c, d}) = {a, b}
OUT[B1] = IN[B2] = {a, b}
IN[B1] = {} U ({a, b} - {a, b}) = {}
-> 수렴! (변화 없음)
3.5 활용
- 죽은 코드 제거: 대입
x = expr에서 OUT 지점에서 x가 활성이 아니면 죽은 코드 - 레지스터 할당: 동시에 활성인 변수들이 간섭 그래프의 간선을 형성
4. 가용 수식 (Available Expressions)
4.1 개념
수식 x op y가 프로그램 지점 p에서 가용하다 = p에 도달하는 모든 경로에서 x op y가 계산되었고, 그 이후로 x와 y가 재정의되지 않음.
4.2 데이터 흐름 방정식
OUT[B] = e_gen[B] U (IN[B] - e_kill[B])
IN[B] = n OUT[P] (P는 B의 모든 선행자)
IN[ENTRY] = {}
합류 연산이 교집합인 이유: 모든 경로에서 계산되어야 안전하게 재사용할 수 있습니다.
e_gen[B]: 블록 B에서 계산되고, 이후에 피연산자가 재정의되지 않는 수식e_kill[B]: 블록 B에서 변수 x가 정의될 때, x를 피연산자로 포함하는 모든 수식
4.3 예시
// B1: t1 = a + b; c = t1
// e_gen[B1] = {a+b}
// e_kill[B1] = {c를 포함하는 수식}
// B2: t2 = a + b; d = t2 - c
// e_gen[B2] = {a+b, t2-c}
// e_kill[B2] = {d를 포함하는 수식}
// B3: a = 1
// e_gen[B3] = {}
// e_kill[B3] = {a를 포함하는 모든 수식} = {a+b}
4.4 초기값의 중요성
가용 수식 분석에서는 초기값 설정이 중요합니다:
OUT[ENTRY] = {} // 진입점에서는 아무 수식도 가용하지 않음
OUT[B] = U // 다른 블록은 전체 집합(모든 수식)으로 초기화
// (교집합이므로 큰 값에서 시작해야 올바르게 수렴)
5. 반복 알고리즘
5.1 일반적인 반복 알고리즘
알고리즘: 순방향 데이터 흐름 분석
입력: 흐름 그래프, gen/kill 집합, 합류 연산
출력: 각 블록의 IN/OUT 집합
1. 초기화
OUT[ENTRY] = 초기값
for (모든 블록 B != ENTRY)
OUT[B] = 초기값
2. 반복
changed = true
while (changed):
changed = false
for (모든 블록 B != ENTRY):
IN[B] = meet(OUT[P] for all predecessors P)
oldOUT = OUT[B]
OUT[B] = gen[B] U (IN[B] - kill[B])
if OUT[B] != oldOUT:
changed = true
5.2 수렴 보장
반복 알고리즘이 반드시 종료하는 이유:
- gen/kill 전달 함수는 **단조(monotone)**합니다 - 입력이 커지면 출력도 커짐(또는 같음)
- 데이터 흐름 값의 집합은 유한합니다
- 합류 연산(합집합 또는 교집합)도 단조합니다
따라서 각 반복에서 OUT 값은 한 방향으로만 변화하며, 유한한 범위 내에서 결국 안정 상태에 도달합니다.
5.3 수렴 속도
일반적으로 반복 횟수는 흐름 그래프의 **깊이(depth)**에 비례합니다. 깊이는 루프의 중첩 깊이와 관련됩니다. 대부분의 프로그램에서 2-3번의 반복으로 수렴합니다.
6. 세 가지 분석 비교
| 특성 | 도달 정의 | 활성 변수 | 가용 수식 |
|---|---|---|---|
| 방향 | 순방향 | 역방향 | 순방향 |
| 합류 연산 | 합집합 | 합집합 | 교집합 |
| 전달 함수 | gen U (IN - kill) | use U (OUT - def) | e_gen U (IN - e_kill) |
| 초기값 | 공집합 | 공집합 | 전체집합(Entry 제외) |
| 활용 | 상수 전파 | 죽은 코드 제거 | 공통 부분식 제거 |
7. 격자 이론의 기초
7.1 반순서 집합 (Partially Ordered Set)
데이터 흐름 분석의 수학적 기반은 격자 이론(Lattice Theory)입니다.
반순서 집합은 집합 S와 관계 <=로 구성되며, 다음을 만족합니다:
- 반사성:
a <= a - 반대칭성:
a <= b이고b <= a이면a = b - 추이성:
a <= b이고b <= c이면a <= c
7.2 격자 (Lattice)
반순서 집합에서 임의의 두 원소 a, b에 대해:
- 최소 상한(join): a와 b 모두보다 크거나 같은 원소 중 가장 작은 것
- 최대 하한(meet): a와 b 모두보다 작거나 같은 원소 중 가장 큰 것
이 두 가지가 항상 존재하면 격자라 합니다.
7.3 데이터 흐름 분석과 격자
// 도달 정의 분석의 격자
// 원소: 정의들의 부분집합 (2^D)
// 순서: 집합 포함관계 (부분집합)
// meet 연산: 합집합 (U)
// bottom: 공집합 ({})
// top: 전체집합 (D)
// 가용 수식 분석의 격자
// 원소: 수식들의 부분집합 (2^E)
// 순서: 역방향 집합 포함관계 (상위집합)
// meet 연산: 교집합
// bottom: 전체집합 (E)
// top: 공집합 ({})
7.4 고정점 정리
Tarski의 고정점 정리: 완전 격자 위에서 단조 함수 f는 최소 고정점을 가집니다. 반복 알고리즘은 이 최소 고정점에 수렴합니다.
이것이 반복 알고리즘의 정확성(correctness)과 종료성(termination)을 보장하는 수학적 기반입니다.
7.5 안전성과 정밀성
// 보수적(conservative) 분석:
// - 안전한 쪽으로 근사
// - 최적화 기회를 놓칠 수 있지만, 잘못된 최적화는 하지 않음
// 도달 정의: 실제보다 더 많은 정의가 도달한다고 분석
// -> 상수 전파를 적용하지 못하는 경우가 생길 수 있음 (안전)
// 가용 수식: 실제보다 더 적은 수식이 가용하다고 분석
// -> 공통 부분식 제거를 적용하지 못하는 경우가 생김 (안전)
8. 실용적 고려사항
8.1 비트 벡터 표현
데이터 흐름 값을 비트 벡터로 표현하면 효율적입니다:
// 정의 d1, d2, d3, d4가 있을 때
// gen[B] = {d1, d3} -> 비트 벡터: 1010
// kill[B] = {d2} -> 비트 벡터: 0100
// 전달 함수 계산:
// OUT = gen | (IN & ~kill) (비트 연산으로 한번에 처리)
비트 연산은 하드웨어 수준에서 매우 빠르므로 대규모 프로그램에서도 효율적입니다.
8.2 워크리스트 알고리즘
단순 반복 대신 워크리스트(worklist) 알고리즘을 사용하면 더 효율적입니다:
알고리즘: 워크리스트 기반 반복
1. 모든 블록을 워크리스트에 추가
2. 워크리스트가 비어있지 않은 동안:
a. 블록 B를 워크리스트에서 꺼냄
b. OUT[B]를 다시 계산
c. OUT[B]가 변경되었으면 B의 후속자들을 워크리스트에 추가
변경된 블록의 영향을 받는 블록만 다시 계산하므로 불필요한 연산을 줄입니다.
정리
| 개념 | 설명 |
|---|---|
| 전달 함수 | 기본 블록의 입력 정보를 출력 정보로 변환 |
| gen/kill 집합 | 블록에서 생성/무효화되는 데이터 흐름 정보 |
| 합류 연산 | 여러 경로의 정보를 합치는 연산 (합집합 또는 교집합) |
| 도달 정의 | 변수의 정의가 특정 지점에 도달하는지 분석 |
| 활성 변수 | 변수의 값이 이후에 사용되는지 분석 |
| 가용 수식 | 수식이 이미 계산되어 재사용 가능한지 분석 |
| 반복 알고리즘 | 데이터 흐름 방정식을 고정점까지 반복 계산 |
| 격자 이론 | 데이터 흐름 분석의 수학적 기반 |
데이터 흐름 분석은 컴파일러 최적화의 이론적 기반으로서, 최적화의 안전성을 보장합니다. 격자 이론에 의한 수학적 근거가 있으므로, 반복 알고리즘은 반드시 수렴하며 보수적으로 안전한 결과를 제공합니다. 다음 글에서는 이 분석 기법을 활용한 루프 최적화를 자세히 다루겠습니다.