Featured image of post [Compiler 03] 최적화 플래그: -O2/-O3/-Ofast

[Compiler 03] 최적화 플래그: -O2/-O3/-Ofast

컴파일러 최적화 레벨 -O0~-Ofast의 의미와 켜지는 최적화 종류, -O2 vs -O3 trade-off, 릴리즈/디버그/프로파일 빌드 전략을 다룹니다. 언제 어떤 레벨을 쓸지 판단 기준과 측정 방법을 제시합니다.

최적화 플래그는 코드를 바꾸지 않고도 생성되는 기계어와 성능을 바꾸는 첫 단계입니다. 같은 소스에 -O2와 -O3만 바꿔도 벤치마크 결과가 수 퍼센트에서 수십 퍼센트까지 달라질 수 있으며, 잘못 선택하면 코드 크기 증가로 오히려 느려지는 회귀도 발생합니다. 이 챕터에서는 GCC/Clang의 -O0~-Ofast와 MSVC의 /O1·/O2·/Ox 의미, trade-off, 언제 어떤 레벨을 쓸지/피할지 판단 기준, 그리고 릴리즈/디버그/프로파일 빌드 전략을 정리합니다.

왜 최적화 플래그인가 (동기)

컴파일러는 소스를 중간 표현(IR)으로 바꾼 뒤, 수십 가지 최적화 패스(인라이닝, 루프 최적화, 레지스터 할당, 벡터화 등)를 적용해 기계어를 만듭니다. 어떤 패스를 켤지는 최적화 레벨 한 번으로 제어할 수 있어, 코드 수정 없이 빌드 옵션만으로 성능을 탐색할 수 있습니다. 레벨을 올리면 더 많은 최적화가 켜지지만 컴파일 시간·메모리·코드 크기가 늘고, 드물게 특정 코드에서 성능이 나빠지기도 합니다. 그래서 이 챕터의 핵심은 “무조건 높은 레벨"이 아니라 측정 후 선택입니다.

역사·배경 (최적화 레벨의 등장)

GCC는 초기부터 -O, -O2 같은 숫자 레벨로 최적화 강도를 지정해 왔고, -O3는 더 공격적인 최적화를 묶은 레벨로 자리 잡았습니다. 현행 GCC 공식 문서는 -O3를 다음과 같이 설명합니다.

“Optimize yet more. -O3 turns on all optimizations specified by -O2 and also turns on the following optimization flags: -fgcse-after-reload -fipa-cp-clone -floop-interchange -floop-unroll-and-jam -fpeel-loops -fpredictive-commoning -fsplit-loops -ftree-loop-distribution -ftree-partial-pre -funswitch-loops -fvect-cost-model=dynamic -fversion-loops-for-strides” — GCC Manual(최신판, 2026-10 확인), Optimize Options

이 목록은 버전마다 바뀝니다. 예를 들어 예전 문서에서 -O3 항목에 있던 -finline-functions는 현행 문서에서 -O2부터 켜지고, 루프·SLP 벡터화(-ftree-loop-vectorize, -ftree-slp-vectorize)도 -O2에서 가장 보수적인 비용 모델(-fvect-cost-model=very-cheap)로 켜집니다. -O3는 같은 벡터화를 dynamic 비용 모델로 더 적극적으로 적용하는 차이가 있습니다. 따라서 정확한 목록은 사용 중인 GCC 버전의 문서나 -Q --help=optimizers 출력으로 확인해야 합니다. -Os(크기 최소화)는 임베디드·제한된 환경을 위해 추가되었고, -Ofast는 GCC 4.x 대에서 부동소수점 완화 옵션을 묶은 레벨로 등장했습니다. Clang은 GCC와 호환되는 레벨 이름을 유지하면서 내부적으로 다른 최적화 순서·휴리스틱을 쓰며, MSVC는 /O1, /O2, /Ox 등 자체 명칭을 사용합니다. 오늘날 대부분의 C/C++ 프로젝트는 -O2 또는 /O2를 릴리즈 기본값으로 두고, 필요 시 -O3를 벤치마크와 함께 검증하는 패턴이 널리 쓰입니다.

최적화 레벨 정의 (GCC / Clang)

최적화 레벨은 “어떤 패스를 얼마나 공격적으로 켤지"를 한 번에 지정하는 옵션입니다. 큰 흐름은 -O0(없음) → -O1(기본) → -O2(균형, 릴리즈 권장) → -O3(공격적) 으로 점점 더 많은 최적화가 켜지고, 여기서 갈라져 나온 -Os는 크기를, -Ofast는 부동소수점 완화를 통한 속도를 우선합니다. 각 레벨의 목적과 켜지는 최적화 방향은 다음과 같습니다.

  • -O0: 최적화 없음. 디버깅 시 변수·소스 라인 대응이 정확하고 컴파일이 빠르지만, 생성 코드는 느립니다. 디버그 빌드 기본값으로 적합합니다.
  • -O1: 기본적인 최적화만 적용. 컴파일 시간을 크게 늘리지 않으면서 일부 성능 이득을 얻을 때 사용합니다. 인라인은 제한적, 루프·레지스터 최적화가 일부 켜집니다.
  • -O2: 대부분의 릴리즈 빌드 권장 레벨입니다. 인라이닝, 루프 최적화, 레지스터 할당 등 대다수 최적화가 켜지고, -O3에 비해 공격적인 변환은 적어 코드 크기·컴파일 시간·회귀 위험이 상대적으로 낮습니다.
  • -O3: -O2에 더해 더 공격적인 인라이닝, 루프 언롤링, 이전 반복의 계산을 재사용하는 predictive commoning, 루프 분배·언스위칭·필링 같은 루프 변환이 추가됩니다. 수치 연산·벡터화에 유리한 코드에서는 -O2 대비 눈에 띄게 빨라질 수 있지만, 코드 크기가 커지고 컴파일 시간·메모리 사용량이 늘며, 드물게 성능 회귀가 나옵니다.
  • -Os: 코드 크기를 줄이는 방향으로 최적화합니다. 인라이닝·언롤링을 억제해 바이너리 크기를 줄이므로, 임베디드·캐시가 극히 제한된 환경에서 고려합니다. Clang과 GCC 12 이상에는 크기를 더 공격적으로 줄이는 -Oz도 있습니다(인코딩이 짧다면 실행 명령어 수가 늘어나는 것도 감수합니다).
  • -Og: GCC 문서가 “Optimize while keeping in mind debugging experience"라고 설명하는 디버깅 친화 레벨입니다. 디버깅을 방해하지 않는 최적화만 켜므로, -O0보다 빠르면서도 단계 실행·변수 조사가 비교적 정확합니다.
  • -Ofast: -O3에 더해 부동소수점 연산의 표준 준수를 완화합니다(-ffast-math 계열). 수치적으로 더 빠른 코드가 나올 수 있지만, 반올림·연산 순서에 따라 결과가 달라질 수 있어 일반 코드에서는 사용을 피하는 것이 안전합니다.

MSVC 대응

MSVC는 /O1(크기 최소화), /O2(속도 최대화, 기본 릴리즈), /Od(최적화 끔, GCC의 -O0에 해당) 등으로 비슷한 역할을 합니다. 이름 때문에 /Ox를 “/O2보다 강한 최대 최적화"로 오해하기 쉽지만, Microsoft 문서는 “/Ox is a strict subset of the /O2 optimizations"라고 명시합니다. 즉 /Ox는 /O2에서 일부(예: 함수 수준 링크 /Gy, 문자열 풀링 /GF)를 뺀 조합이므로, 속도가 목적이면 /O2를 쓰면 됩니다. /O2가 GCC/Clang의 -O2에 대응하는 기본 릴리즈 설정으로 널리 쓰이며, Visual Studio 프로젝트 속성에서 “Optimization"을 “Maximize Speed (/O2)“로 두면 됩니다.

최적화 레벨별로 켜지는 최적화 (개념)

레벨이 올라갈수록 인라이닝·루프 최적화·벡터화 등이 더 공격적으로 적용됩니다. 개념적으로는 아래와 같습니다.

flowchart LR
  subgraph low ["-O0 / -O1"]
    A["최소 최적화"]
    B["디버깅 용이"]
  end
  subgraph mid ["-O2"]
    C["인라이닝·루프·레지스터"]
    D["일반 릴리즈 권장"]
  end
  subgraph high ["-O3 / -Ofast"]
    E["공격적 인라이닝·언롤링"]
    F["코드 크기·회귀 주의"]
  end
  low --> mid --> high

실제로 켜지는 플래그 목록은 컴파일러·버전마다 다르므로, gcc -O2 -Q --help=optimizers나 문서로 확인합니다(개별 플래그 확인 방법은 아래 「개별 최적화 플래그와 확인 방법」 절 참고).

-O2 vs -O3 trade-off

대부분의 선택은 -O2와 -O3 사이에서 일어나며, 둘의 차이는 단순한 “속도 차이"가 아니라 속도·코드 크기·컴파일 시간·회귀 위험의 묶음입니다. 핵심은 -O3가 항상 빠른 것은 아니라는 점으로, 코드 크기가 커지면 명령어 캐시(I-cache) 압박으로 오히려 느려질 수 있습니다.

축-O2-O3
속도대부분의 코드에서 충분히 빠름벡터화·루프 변환에 민감한 핫 루프에서 0%~두 자릿수 % 개선 가능
코드 크기기준루프 복제(unswitch·peel·versioning)로 텍스트 세그먼트 증가
컴파일 시간기준추가 루프 패스로 증가, 대형 TU에서 체감
회귀 위험낮음드물지만 I-cache 압박·과한 버전 분기로 느려지는 사례 존재

이 네 축은 서로 독립적이지 않습니다. -O3가 켜는 루프 언스위칭·필링·버전 분기는 같은 루프를 여러 벌 복제해 각 경로를 빠르게 만드는 대신 코드를 키우고, 커진 코드는 핫 경로가 여러 곳에 흩어진 서버 코드에서 I-cache 미스로 이득을 상쇄합니다. 반대로 핫스팟이 소수의 수치 루프에 몰려 있으면 크기 증가는 무시할 만하고 벡터화 이득만 남습니다. 그래서 -O3로 갈지는 “프로파일에서 시간이 몇 개의 루프에 집중되는가"로 먼저 가늠하고, 집중도가 높을 때 동일 벤치마크로 -O2 vs -O3를 비교해 결정하는 것이 합리적입니다. 시간이 넓게 퍼진 코드라면 -O2를 유지하고 PGO(챕터 03)로 핫 경로만 공격적으로 최적화하는 편이 대개 낫습니다.

-Ofast와 부동소수점

-Ofast는 -O3에 -ffast-math 계열 옵션을 더한 레벨입니다. -ffast-math는 -fno-math-errno, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans, -fassociative-math 등 여러 하위 옵션의 묶음으로, 부동소수점 연산의 표준 준수를 완화합니다. 예를 들어 연관 법칙을 가정하면 (a + b) + c와 a + (b + c)를 같은 것으로 간주해 순서를 바꿀 수 있어 벡터화가 더 잘 들어가지만, 부동소수점에서는 순서에 따라 결과가 달라질 수 있습니다.

따라서 재현성이 중요한 시뮬레이션·금융·규제 제출 코드에서는 -Ofast를 쓰지 않고 -O3까지로 제한합니다. 신호 처리·이미지 처리처럼 오차 허용 범위가 넓은 도메인에서는 -Ofast로 속도 이득을 취하는 경우가 있습니다. 위험을 줄이려면 -O3에 -ffast-math 전체 대신 -fno-math-errno 같은 개별 옵션만 선택적으로 켜는 방법도 있습니다.

빌드 명령과 크기·시간 비교 (예시)

동일한 소스에 다른 최적화 레벨을 적용해 컴파일 시간과 바이너리 크기를 비교하는 예시입니다.

1
2
3
4
5
6
7
8
9
# 단일 TU 컴파일 시간 비교 (GCC/Clang)
g++ -O0 -c example.cc -o example_o0.o   # 가장 빠름
g++ -O2 -c example.cc -o example_o2.o
g++ -O3 -c example.cc -o example_o3.o # 가장 느림

# 실행 파일 크기 비교 (텍스트 세그먼트)
g++ -O2 example.cc -o app_o2 && size app_o2
g++ -O3 example.cc -o app_o3 && size app_o3
# -O3 쪽 text 영역이 더 클 수 있음

빌드 시스템에서는 빌드 타입으로 레벨을 묶어 관리합니다. CMake의 표준 빌드 타입은 다음과 같습니다.

1
2
3
4
# CMake에서 Release/Debug 설정
cmake -DCMAKE_BUILD_TYPE=Release ..   # 보통 -O3 또는 -O2
cmake -DCMAKE_BUILD_TYPE=Debug ..     # -O0 -g
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..  # -O2 -g

측정과 비교

레벨 선택은 추측이 아니라 측정으로 합니다. 동일한 벤치마크 스위트를 각 플래그로 빌드해 반복 실행하고, 한두 번의 실행이 아니라 평균·표준편차(또는 백분위)를 비교해야 의미 있는 판단이 됩니다. 벤치마크가 핫패스를 잘 커버하지 못하면 레벨 차이가 드러나지 않습니다.

  • 컴파일 시간: 동일 소스에 -O0 / -O2 / -O3를 적용해 측정합니다. 대형 TU에서 차이가 큽니다.
  • 실행 파일 크기: size로 텍스트·데이터·bss 크기를 비교합니다. -Os < -O2 < -O3 순으로 커지는 경향이 있습니다.
  • 벤치마크: 각 플래그 빌드를 반복 실행해 평균·분포를 비교합니다.
  • 회귀 테스트: 릴리즈 브랜치에서는 레벨을 고정하고, 코드 변경 시 동일 벤치마크로 성능 하락이 없는지 CI에서 주기적으로 확인합니다.

수치를 해석하는 예를 들면, 동일 벤치마크를 -O2와 -O3로 각각 10회 실행해 -O2: 100µs, -O3: 85µs를 얻었다면 약 15% 개선입니다. 이때 (1) 표준편차·백분위로 차이가 통계적으로 의미 있는지 확인하고, (2) 같은 스위트의 다른 케이스에서 -O3가 느려진 구간이 없는지 보고, (3) -O3 바이너리의 텍스트 세그먼트가 크게 늘었는지 확인합니다. 모든 구간에서 이득이 있거나 동등하고 크기 증가가 수용 가능하면 -O3를 채택하고, 한 구간이라도 나빠지면 원인(예: I-cache 압박)을 분석하거나 해당 모듈만 -O2로 둡니다. (측정값은 CPU·컴파일러·플래그에 따라 다릅니다.)

간단한 예시로 레벨별 차이 확인

작은 합산 루프 하나로도 레벨 차이를 체감할 수 있습니다. 아래 코드를 -O0와 -O2로 빌드해 실행 시간을 비교해 봅니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// sum.cc: 단순 합산 루프 (벤치마크용 최소 예시)
// 빌드: g++ -O0 sum.cc -o sum_o0 && ./sum_o0
//       g++ -O2 sum.cc -o sum_o2 && ./sum_o2
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <numeric>
#include <vector>

uint64_t sum(const std::vector<int>& v) {
  uint64_t s = 0;
  for (size_t i = 0; i < v.size(); ++i)
    s += v[i];
  return s;
}

int main() {
  std::vector<int> v(10'000'000);
  std::iota(v.begin(), v.end(), 0);  // 0, 1, 2, ... 로 채움

  auto t0 = std::chrono::steady_clock::now();
  uint64_t total = 0;
  for (int rep = 0; rep < 20; ++rep)  // 반복해 측정 잡음을 줄임
    total += sum(v);
  auto t1 = std::chrono::steady_clock::now();

  // 결과를 출력해야 컴파일러가 계산 자체를 제거하지 못함
  std::printf("total=%llu, %.1f ms\n",
              static_cast<unsigned long long>(total),
              std::chrono::duration<double, std::milli>(t1 - t0).count());
}

어셈블리 수준에서 차이를 보려면 g++ -O0 -S sum.cc -o sum_o0.s와 g++ -O2 -S sum.cc -o sum_o2.s를 비교하거나 Compiler Explorer에 붙여 넣으면 됩니다.

-O0에서는 v.size()·v[i] 호출이 매 반복 실제 함수 호출로 남고 s가 매번 메모리에 저장되므로 느립니다. -O2에서는 인라이닝으로 호출이 사라지고 s가 레지스터에 머물며, 현행 GCC는 저비용 벡터화까지 적용해 훨씬 빠른 기계어를 생성합니다. 실제 측정 시에는 Google Benchmark, nanobench 등으로 반복 실행 후 평균·분포를 보고, 대형 TU에서는 time g++ -O0 -c big.cc vs -O2로 컴파일 시간 차이도 확인할 수 있습니다.

릴리즈/디버그/프로파일 빌드 전략

빌드 목적에 따라 레벨을 나누는 것이 기본 원칙입니다. 디버그는 정확한 단계 실행을, 릴리즈는 성능을, 프로파일 수집은 계측 비용과 대표성의 균형을 우선합니다.

  • 디버그: -O0에 -g를 함께 써서 단계 실행·변수 조사에 적합하게 유지합니다. -O0 빌드가 너무 느려 테스트가 오래 걸리면 GCC 문서가 편집·컴파일·디버그 주기에 권장하는 -Og를 대안으로 씁니다.
  • 릴리즈: -O2를 기본으로 하고, 핫 경로가 수치·벡터화에 민감하면 -O3를 시도한 뒤 벤치마크로 검증합니다. 회귀가 없을 때만 -O3를 채택합니다.
  • 프로파일 수집: PGO의 instrumented 빌드는 최종 릴리즈와 같은 최적화 레벨로 둡니다. GCC는 계측 컴파일과 프로파일 사용 컴파일에 같은 최적화 옵션을 요구합니다(챕터 03).

LTO·PGO와의 조합도 같은 원리입니다. LTO(챕터 02)를 쓸 때도 각 TU는 먼저 -O2/-O3로 IR을 생성하고 링크 시점에 합쳐 추가 최적화하므로 -O2 -flto 또는 -O3 -flto 조합이 일반적이며, PGO(챕터 03)는 계측 빌드와 최종 빌드에 같은 -O 레벨을 써야 합니다. 어느 경우든 동일 벤치마크로 측정해 회귀가 없는지 확인합니다.

빌드 시스템별로는 빌드 타입을 다음과 같이 매핑하고, CI에서는 릴리즈 빌드로 회귀를 측정합니다.

빌드 시스템릴리즈디버그릴리즈 + 디버그 정보
CMakeCMAKE_BUILD_TYPE=Release (GCC/Clang 기본 -O3 -DNDEBUG)CMAKE_BUILD_TYPE=Debug (-g)CMAKE_BUILD_TYPE=RelWithDebInfo (-O2 -g -DNDEBUG)
Mesonbuildtype: ‘release’ (-O3)buildtype: ‘debug’ (-O0 -g)buildtype: ‘debugoptimized’ (-O2 -g)
MakeCXXFLAGS=-O2 (직접 지정)CXXFLAGS=-O0 -gCXXFLAGS=-O2 -g

표에서 보듯 CMake와 Meson의 Release는 GCC/Clang에서 -O3가 기본이므로, “릴리즈 = -O2"를 원한다면 set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")처럼 프로젝트에서 명시적으로 고정해야 환경에 덜 흔들립니다.

개별 최적화 플래그와 확인 방법

최적화 레벨은 수십 개의 개별 -f* 플래그를 묶은 것입니다. 현행 GCC 기준으로 -O2는 -finline-functions·-foptimize-sibling-calls와 저비용 벡터화 등을 켜고, -O3는 여기에 -funswitch-loops·-fpeel-loops·-floop-interchange 같은 루프 변환과 dynamic 비용 모델 벡터화가 더해진 묶음으로 이해할 수 있습니다(GCC 버전에 따라 경계가 이동합니다). 실무에서는 보통 -O2 또는 -O3만 지정하고 개별 플래그를 건드리지 않는 편이 유지보수에 유리하지만, 특정 최적화에서만 버그가 재현된다면 해당 최적화만 끄는 -fno-* 플래그로 회귀를 피할 수 있습니다.

어떤 최적화가 실제로 켜지는지 확인하려면 컴파일러 도움말을 사용합니다.

  • GCC: gcc -O2 -Q --help=optimizers로 -O2에서 켜지는/꺼지는 최적화 목록을 보고, -O3와 diff해 차이를 비교합니다.
  • Clang: clang -O2 -mllvm -debug-pass=Structure(버전에 따라 다름)로 패스 구성을 보거나 공식 문서로 -O2/-O3 차이를 확인합니다.
  • MSVC: 프로젝트 속성 “Optimization” 하위에서 “Inline Function Expansion”, “Enable Intrinsic Functions” 등 개별 옵션을 확인합니다.

한눈에 보기: 최적화 레벨 비교

지금까지의 정의·트레이드오프를 한 표로 정리하면 다음과 같습니다. 인라이닝·벡터화 열은 “얼마나 공격적인지"의 개념적 방향이며, 실제 켜지는 플래그는 컴파일러·버전에 따라 다릅니다.

레벨목적인라이닝/벡터화코드 크기컴파일 시간권장 용도
-O0없음거의 없음작음빠름디버그 빌드
-O1기본제한적보통보통빠른 빌드 + 일부 이득
-O2균형일반적보통보통일반 릴리즈 기본
-O3최대 속도적극적·언롤링큼느림수치/벡터화 핫패스, 벤치 검증 후
-Os크기 최소억제작음보통임베디드·제한된 캐시
-Ofast수치 최대-O3+추가큼느림신호/과학 계산만, 재현성 불필요 시

판단 기준: 언제 쓸지 / 언제 피할지

상황권장비권장
일반 릴리즈 빌드-O2-O0, -Ofast(일반 코드)
수치·벡터화가 중요한 핫 루프-O3 (벤치로 검증 후)-O3 무검증 채택
디버깅·단계 실행-O0, 느리면 -Og-O2 이상
임베디드·바이너리 크기 제한-Os 또는 -O2-O3
금융·규제·재현성 중요-O2 또는 -O3 (fast-math 없음)-Ofast
PGO 1단계(instrumented)최종 빌드와 같은 레벨최종 빌드와 다른 레벨(GCC coverage mismatch)

실전 시나리오: -O2에서 -O3 전환 검토

릴리즈가 -O2로 설정되어 있고 핫패스에 수치 연산·루프가 많은 프로젝트에서 -O3 전환을 검토하는 흐름은 다음과 같습니다.

  1. 벤치마크 준비: 핫패스를 잘 커버하는 벤치마크(또는 통합 테스트의 성능 구간)를 확보합니다.
  2. 빌드: 동일 소스로 -O2와 -O3 각각 빌드해 두 바이너리를 만듭니다.
  3. 실행·측정: 같은 벤치마크를 여러 번 실행해 평균·표준편차(또는 백분위)를 기록합니다.
  4. 크기 확인: size로 텍스트 세그먼트 증가를 봅니다. 크게 늘고 지연이 중요한 경로가 여러 개면 I-cache 압박 가능성을 염두에 둡니다.
  5. 판단: 속도 이득이 명확하고 다른 구간의 회귀가 없으면 -O3로 전환하고, 이득이 미미하거나 회귀가 있으면 -O2를 유지합니다.
  6. CI 반영: 선택한 레벨로 릴리즈 빌드 후, CI에서 -O2와 -O3 결과를 함께 수집해 -O3에서만 느려지는 회귀를 자동으로 감지합니다. 일정 비율 이상 나빠지면 실패로 처리하고 빌드 로그·벤치 결과를 아티팩트로 보관합니다.

자주 하는 실수

  • -O3를 검증 없이 릴리즈에 넣는 경우: 일부 코드에서 코드 크기 증가로 느려질 수 있으므로, 동일 벤치마크로 -O2 vs -O3를 비교한 뒤 채택한다.
  • -Ofast를 일반 비즈니스 로직에 쓰는 경우: 부동소수점 결과가 바뀔 수 있어, 재현성·규제가 중요한 코드에서는 사용하지 않는다. 테스트가 비트 단위 기대값을 비교한다면 허용 오차(epsilon)를 두거나 해당 모듈만 -O3로 둔다.
  • 디버그 빌드에 -O2 이상을 쓰는 경우: 변수가 “optimized out"으로 사라지거나 라인 대응이 어긋나 디버깅이 어려워진다. 디버깅 시에는 -O0 또는 -O1 + -g를 유지하고, 릴리즈 동작 재현이 필요하면 RelWithDebInfo(-O2 -g)를 쓰되 일부 변수는 여전히 보이지 않을 수 있음을 받아들인다.

비판적 시각: 한계와 트레이드오프

최적화 레벨은 높을수록 좋다가 아닙니다. -O3는 코드 크기를 키워 I-cache 미스를 늘릴 수 있고, -Ofast는 수치 결과를 바꿉니다. “릴리즈 = -O3"로 고정하기보다, 프로젝트의 핫패스와 벤치마크로 한 번씩 측정한 뒤 선택하는 것이 안전합니다. 레거시 프로젝트라면 이미 -O2로 안정적으로 돌고 있을 때 무리하게 -O3로 올리지 말고, 성능 이득이 명확할 때만 단계적으로 전환합니다. 이미 -O3인데 특정 TU만 문제를 일으키면 그 TU만 -O2로 컴파일하도록 예외를 둘 수 있습니다(CMake: set_source_files_properties(... COMPILE_FLAGS -O2)).

자주 묻는 질문

  • Q: -Os와 -O2 중 무엇을 써야 하나요? A: 바이너리 크기나 I-cache가 극히 제한된 환경(일부 임베디드)이면 -Os를, 그렇지 않으면 보통 -O2가 유리합니다.
  • Q: Clang에서 -Ofast를 쓰니 경고가 나옵니다. A: Clang 문서에 따르면 -Ofast는 Clang 19부터 deprecated이며(제거 일정은 미정), -O3 -ffast-math로 의도를 명시하도록 권합니다. 필요한 완화 옵션만 개별로 켜는 편이 안전합니다.
  • Q: 컴파일 시간을 줄이려면? A: -O3 대신 -O2를 쓰면 줄어듭니다. 캐시·증분 빌드는 챕터 12에서 다룹니다.

Tr.02(인라이닝)과의 연계

Low-latency C++ Language Optimization 트랙(Tr.02)에서는 인라이닝 유도를 다룹니다. 인라이닝이 되려면 컴파일러가 호출 지점에서 함수 정의를 볼 수 있어야 하고 최적화 레벨이 -O1 이상이어야 하며, -O0에서는 인라이닝이 거의 켜지지 않습니다. 따라서 “인라이닝을 유도하는 코드 배치·final·LTO"는 -O2 또는 -O3와 함께 쓸 때 효과가 납니다. 이 챕터는 “어떤 레벨을 켤지"를, Tr.02은 “그 레벨에서 인라이닝이 실제로 일어나도록 코드를 어떻게 쓸지"를 다룹니다.

학습 성과 목표와 평가 기준

이 글을 읽은 후 다음을 할 수 있어야 합니다. -O0~-Ofast와 MSVC /O1·/O2·/Ox의 의미·권장 용도를 설명하고, -O2 vs -O3 trade-off(속도·코드 크기·컴파일 시간·회귀)를 근거로 레벨을 선택하며, -Ofast의 부동소수점 위험을 들어 언제 피할지 판단하고, 릴리즈/디버그/프로파일 빌드 전략을 설계해 CI에서 회귀를 측정할 수 있습니다. 아래 항목으로 스스로 점검해 봅니다.

  • -O2와 -O3를 선택할 때 고려할 네 요소(속도·코드 크기·컴파일 시간·회귀)를 나열할 수 있는가?
  • -Ofast를 써도 되는 경우와 피해야 하는 경우를 구분할 수 있는가?
  • 디버그와 릴리즈에 서로 다른 레벨을 적용하는 이유를 설명할 수 있는가?
  • 동일 벤치마크로 -O2 vs -O3를 비교하는 절차를 단계별로 말할 수 있는가?
  • CI에서 성능 회귀를 감지하려면 어떤 빌드·실행·비교가 필요한지 말할 수 있는가?
  • MSVC의 /O1·/O2·/Ox가 GCC/Clang의 어떤 레벨에 대응하는지 설명할 수 있는가?

용어 정리

용어설명
-O0~-O3GCC/Clang 최적화 레벨; 숫자가 클수록 더 많은·공격적인 최적화 적용
-Os크기 최소화 방향 최적화; 인라이닝·언롤링 억제
-Ofast-O3 + 부동소수점 표준 완화(-ffast-math 계열)
IRIntermediate Representation; 컴파일러 내부 중간 표현
I-cacheInstruction cache; 명령어 캐시; 코드 크기가 크면 미스 증가 가능
회귀최적화 레벨을 올렸을 때 특정 코드에서 성능이 나빠지는 현상
RelWithDebInfoRelease with Debug Info; -O2 수준 + 디버그 심볼

참고 자료

다음 장에서는

LTO(Link-Time Optimization)와 ThinLTO 개념, GCC/Clang/MSVC에서의 활성화 방법, LTO on/off 성능·크기 검증을 다룹니다.

→ LTO와 ThinLTO 실전 적용 (챕터 02)