C++/컴파일러의 변수에 대한 최적화


Q: C++ 컴파일러가 사용하는 변수에 대한 최적화 기법들의 종류

컴파일러는 실행 속도를 높이고 메모리 사용량을 줄이기 위해 다양한 최적화를 수행합니다. **변수**와 관련된 대표적인 기법은 다음과 같습니다.

  1. **상수 전파 (Constant Propagation)**
    변수가 컴파일 타임에 상수 값임을 알 수 있을 때, 그 값을 사용처에 전파합니다.
       const int a = 10;
       int b = a + 5;     // → int b = 15;

  2. **데드 코드 제거 (Dead Code Elimination)**
    결과가 프로그램의 관찰 가능한 동작에 영향을 주지 않는 코드(변수 할당 포함)를 제거합니다.

  3. **공통 서브식 제거 (Common Subexpression Elimination)**
    동일한 연산이 반복될 때 한 번만 계산하고 결과를 재사용합니다.

  4. **루프 불변 코드 이동 (Loop-Invariant Code Motion)**
    루프 안에서 값이 변하지 않는 연산을 루프 밖으로 빼냅니다.

  5. **레지스터 할당 (Register Allocation)**
    자주 사용되는 변수를 CPU 레지스터에 유지해 메모리 접근을 최소화합니다. (그래프 색칠 + spilling 기법 사용)

  6. **스칼라 교환 / 배열 스칼라화 (Scalar Replacement / Scalarization)**
    배열이나 구조체 요소 중 자주 접근되는 부분을 임시 스칼라 변수로 치환하여 메모리 접근과 주소 계산을 줄입니다.

이 기법들은 대부분 -O2 / -O3 수준에서 자동으로 적용되지만, 과도한 최적화가 의도하지 않은 동작을 유발할 수 있어 volatile이나 std::atomic 등을 사용해 제한하기도 합니다.



Q: 상수 전파에 대해서 자세히

**상수 전파(Constant Propagation)**는 변수가 컴파일 타임에 알려진 상수 값임을 파악한 뒤, 그 값을 모든 사용 지점에 전파하여 연산을 단순화하는 기법입니다.

const int a = 10;
const int b = 20;
int c = a + b;   // → int c = 30;
int d = a * b;   // → int d = 200;

상수 전파는 보통 **Reaching Definition 분석**과 결합되어 수행되며, const가 없어도 변수가 실제로 변경되지 않는다는 것이 증명되면 적용 가능합니다.



Q: 상수 전파와 상수 폴딩의 차이?

- **상수 폴딩 (Constant Folding)**
컴파일 타임에 **상수 리터럴**로만 구성된 표현식을 미리 계산합니다.

  int y = 5 * 2;   // → int y = 10;

- **상수 전파 (Constant Propagation)**
변수에 상수 값이 할당된 사실을 추적해 그 값을 사용처까지 **전파**합니다.

  const int x = 5;
  int y = x * 2;   // → int y = 10;

즉, 상수 폴딩은 “리터럴만 있는 식”을 계산하는 것이고, 상수 전파는 “변수에 상수가 들어있는 사실”을 퍼뜨리는 것입니다. 두 기법은 거의 항상 함께 사용되며, 상수 전파 후에 폴딩이 추가로 적용되는 경우가 많습니다.



Q: const 키워드가 없는 변수에도 상수 전파가 되는가?

**네, 가능합니다.**
컴파일러는 정적 분석(특히 데이터 흐름 분석)을 통해 변수가 실제로 변경되지 않는다는 것을 증명하면 const 없이도 상수 전파를 수행합니다.

int a = 10;
int b = 20;
int c = a + b;   // → 가능하면 int c = 30;

하지만 분기나 포인터, 함수 호출 등으로 값이 변할 “가능성”이 조금이라도 있으면 안전하게 최적화를 포기합니다.

int a = 10;
if (condition) a = 15;
int c = a + 20;  // 상수 전파 불가



Q: 데드 코드 제거에 대해서.

**데드 코드 제거(Dead Code Elimination, DCE)**는 실행 결과에 영향을 주지 않는 코드를 제거하는 기법입니다.

- 절대 도달하지 않는 코드 (unreachable code)
- 쓰여지기만 하고 읽히지 않는 변수 할당
- 호출되지 않는 함수 (링커 단계에서도 적용 가능)

int unused_function() { return 42; }  // main에서 호출 안 하면 제거 가능

int main() {
    int x = 100;     // 사용 안 되면 할당 자체 제거 가능
    return 0;
}

**주의**: side-effect가 있는 연산(입출력, volatile 쓰기, atomic 연산 등)은 절대 데드 코드로 간주되지 않습니다.



Q: 데드 코드 제거가 과도한 최적화로 인해 예상치 못한 동작을 유발하는 예시

데드 코드 제거 자체는 거의 문제를 일으키지 않습니다.
그러나 **volatile 미사용 시** 또는 **side-effect 없는 무한 루프**가 제거되는 경우 문제가 됩니다.

volatile int ready = 0;

while (!ready) {
    // busy wait
}

volatile이 없으면 컴파일러가 루프를 제거하거나 무한 루프로 판단해 최적화할 수 있습니다 → 프로그램이 멈추는 버그 발생 가능.

또 다른 대표 사례는 **empty infinite loop**가 제거되는 경우입니다. (C++ 표준상 side-effect 없는 루프는 제거 가능)



Q: 공통 서브식 제거 (CSE)

동일한 연산이 여러 번 등장할 때 한 번만 계산합니다.

int a = x * y;
int b = x * y + z;
int c = x * y * z;

// 최적화 후
int t = x * y;
int a = t;
int b = t + z;
int c = t * z;



Q: 루프 불변 코드 제거 (Loop-Invariant Code Motion)

루프 안에서 변하지 않는 계산을 루프 밖으로 이동

for (int i = 0; i < n; ++i) {
    int x = a * b;           // 불변
    result[i] = x * i;
}

// 최적화 후
int x = a * b;
for (int i = 0; i < n; ++i) {
    result[i] = x * i;
}

---

Q: 스칼라 교환 (Scalar Replacement)

배열/구조체 요소를 임시 스칼라 변수로 치환 → 메모리 접근 감소 + 레지스터 활용 향상

현대 컴파일러는 주로 **SROA (Scalar Replacement of Aggregates)** 형태로 구조체를 스칼라들로 분해합니다.
사용자가 직접 임시 변수를 만드는 예제는 실제 자동 최적화 모습과 다소 다릅니다.



Q: 다른 주요 최적화 기법

- 함수 인라이닝 (Inline Expansion)
- 루프 언롤링 (Loop Unrolling)
- 자동 벡터화 (Auto-Vectorization + SIMD)
- 캐시 친화적 접근 패턴 유도 (타일링, 차원 순서 최적화 등)

**트리 셰이킹(Tree Shaking)**은 C++ 네이티브 컴파일러에서는 거의 쓰이지 않는 용어입니다. 주로 JavaScript 번들러에서 모듈 단위 dead code 제거를 지칭합니다. C++에서는 링커의 dead code elimination이나 section folding 등이 비슷한 역할을 합니다.


Show Comments