최적화 플래그는 코드를 바꾸지 않고도 생성되는 기계어와 성능을 바꾸는 첫 단계입니다. 같은 소스에 -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 같은 개별 옵션만 선택적으로 켜는 방법도 있습니다.
빌드 명령과 크기·시간 비교 (예시)
동일한 소스에 다른 최적화 레벨을 적용해 컴파일 시간과 바이너리 크기를 비교하는 예시입니다.
| |
빌드 시스템에서는 빌드 타입으로 레벨을 묶어 관리합니다. CMake의 표준 빌드 타입은 다음과 같습니다.
| |
측정과 비교
레벨 선택은 추측이 아니라 측정으로 합니다. 동일한 벤치마크 스위트를 각 플래그로 빌드해 반복 실행하고, 한두 번의 실행이 아니라 평균·표준편차(또는 백분위)를 비교해야 의미 있는 판단이 됩니다. 벤치마크가 핫패스를 잘 커버하지 못하면 레벨 차이가 드러나지 않습니다.
- 컴파일 시간: 동일 소스에 -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로 빌드해 실행 시간을 비교해 봅니다.
| |
어셈블리 수준에서 차이를 보려면 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에서는 릴리즈 빌드로 회귀를 측정합니다.
| 빌드 시스템 | 릴리즈 | 디버그 | 릴리즈 + 디버그 정보 |
|---|---|---|---|
| CMake | CMAKE_BUILD_TYPE=Release (GCC/Clang 기본 -O3 -DNDEBUG) | CMAKE_BUILD_TYPE=Debug (-g) | CMAKE_BUILD_TYPE=RelWithDebInfo (-O2 -g -DNDEBUG) |
| Meson | buildtype: ‘release’ (-O3) | buildtype: ‘debug’ (-O0 -g) | buildtype: ‘debugoptimized’ (-O2 -g) |
| Make | CXXFLAGS=-O2 (직접 지정) | CXXFLAGS=-O0 -g | CXXFLAGS=-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 전환을 검토하는 흐름은 다음과 같습니다.
- 벤치마크 준비: 핫패스를 잘 커버하는 벤치마크(또는 통합 테스트의 성능 구간)를 확보합니다.
- 빌드: 동일 소스로 -O2와 -O3 각각 빌드해 두 바이너리를 만듭니다.
- 실행·측정: 같은 벤치마크를 여러 번 실행해 평균·표준편차(또는 백분위)를 기록합니다.
- 크기 확인:
size로 텍스트 세그먼트 증가를 봅니다. 크게 늘고 지연이 중요한 경로가 여러 개면 I-cache 압박 가능성을 염두에 둡니다. - 판단: 속도 이득이 명확하고 다른 구간의 회귀가 없으면 -O3로 전환하고, 이득이 미미하거나 회귀가 있으면 -O2를 유지합니다.
- 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~-O3 | GCC/Clang 최적화 레벨; 숫자가 클수록 더 많은·공격적인 최적화 적용 |
| -Os | 크기 최소화 방향 최적화; 인라이닝·언롤링 억제 |
| -Ofast | -O3 + 부동소수점 표준 완화(-ffast-math 계열) |
| IR | Intermediate Representation; 컴파일러 내부 중간 표현 |
| I-cache | Instruction cache; 명령어 캐시; 코드 크기가 크면 미스 증가 가능 |
| 회귀 | 최적화 레벨을 올렸을 때 특정 코드에서 성능이 나빠지는 현상 |
| RelWithDebInfo | Release with Debug Info; -O2 수준 + 디버그 심볼 |
참고 자료
- GCC: Optimize Options — -O0~-O3, -Os, -Ofast 설명.
- Clang: clang command guide — -O0~-O3, -Os, -Oz, -Og, -Ofast의 동작.
- MSVC: /O Options — /O1, /O2, /Ox.
다음 장에서는
LTO(Link-Time Optimization)와 ThinLTO 개념, GCC/Clang/MSVC에서의 활성화 방법, LTO on/off 성능·크기 검증을 다룹니다.
→ LTO와 ThinLTO 실전 적용 (챕터 02)
![Featured image of post [Compiler 03] 최적화 플래그: -O2/-O3/-Ofast](/post/compiler-optimization/optimization-flags-o2-o3-ofast/wordcloud_hu_49e552ed75045497.webp)
![[Compiler 03] Introduction: Low-latency 컴파일러·빌드 최적화](/post/compiler-optimization/getting-started-compiler-build-performance-tuning/wordcloud_hu_54fcb9b6f9bee677.webp)
![[Compiler 03] 최적화 플래그: -O2/-O3/-Ofast](/post/compiler-optimization/optimization-flags-o2-o3-ofast/wordcloud_hu_7e53824b01fed297.webp)
![[Compiler 03] LTO와 ThinLTO 실전 적용](/post/compiler-optimization/lto-thinlto-practical/wordcloud_hu_660aa372ab0415ad.webp)
![[Compiler 03] PGO 고급 워크플로우](/post/compiler-optimization/pgo-workflow-advanced/wordcloud_hu_d830eb9ddd44a5a6.webp)
![[Compiler 03] GCC vs Clang vs MSVC 최적화 차이](/post/compiler-optimization/compiler-comparison-gcc-clang-msvc/wordcloud_hu_f9e92dca98d719c3.webp)
![[Compiler 03] AutoFDO 워크플로우: 샘플링 기반 프로파일 최적화](/post/compiler-optimization/autofdo-workflow-sampling-based/wordcloud_hu_d305107584078a19.webp)
![[Compiler 03] C++20 Modules와 빌드·성능](/post/compiler-optimization/cpp20-modules-build-performance/wordcloud_hu_fb6a0090a25cd0b2.webp)
![[Compiler 03] CPU 기능별 함수 다중 버전](/post/compiler-optimization/function-multiversioning-cpu/wordcloud_hu_f5001b04a04f9a62.webp)
![[Compiler 03] Sanitizer 성능 오버헤드](/post/compiler-optimization/sanitizer-overhead-performance/wordcloud_hu_6031575f5f1179fb.webp)