생성된 어셈블리를 보면 인라이닝·벡터화·호출 비용을 직접 확인할 수 있습니다. 이 챕터에서는 도구 사용법과 해석 포인트를 구체적인 어셈블리 예시와 함께 다룹니다. 생성 코드를 읽는 일은 고급 최적화의 기본기로, 컴파일러가 실제로 무엇을 만들었는지 확인하지 않으면 추측에 의존하게 됩니다.
“This is an optimization manual for advanced assembly language programmers and compiler makers.” — Agner Fog, Optimizing subroutines in assembly language
산출물 확인 도구
컴파일 산출물을 보는 도구들은 각기 다른 시점의 결과를 보여 줍니다. 어떤 단계에서 무엇을 보는지 먼저 이해하는 것이 중요합니다.
- -S: 컴파일러가 어셈블리 파일(.s)을 생성하도록 합니다.
g++ -S -O2 foo.cc또는clang++ -S -O2 foo.cc로 소스에서 곧바로 .s를 얻을 수 있습니다. 이것은 단일 TU의 결과이므로, 다른 .cc에 있는 함수의 크로스-TU 인라이닝은 반영되지 않습니다. - -fverbose-asm (GCC): 어셈블리 안에 변수명·소스 라인 등 주석을 넣어 읽기 쉽게 합니다.
-S와 함께g++ -S -O2 -fverbose-asm foo.cc형태로 사용합니다. - objdump -d: 이미 만들어진 오브젝트·실행 파일을 디스어셈블합니다.
objdump -d a.out또는objdump -d -M intel a.out(인텔 문법)으로 기계어와 어셈블리 니모닉을 볼 수 있습니다. - Godbolt (Compiler Explorer): 웹에서 컴파일러·플래그를 바꿔 가며 소스 → 어셈블리를 비교할 수 있습니다. 여러 컴파일러·버전을 한 화면에서 비교할 때 유용하며, 소스 라인과 어셈블리 라인을 색상으로 매핑해 줍니다.
LTO를 켠 경우에는 단일 .cc만 -S로 뽑으면 크로스-TU 인라이닝이 반영되지 않습니다. 최종 링크된 바이너리를 objdump하거나, LTO 단계에서 나오는 합쳐진 IR/어셈블리를 보는 것이 전체 그림을 보는 데 필요합니다.
구체적인 어셈블리 예시: 인라인 전/후 비교
인라이닝이 어셈블리에 어떻게 나타나는지 구체적으로 살펴봅니다. 다음 소스를 기준으로 합니다.
| |
-O0(최적화 없음)으로 컴파일 시 compute의 어셈블리 (개념적):
| |
call _Z3addii — 이 줄이 함수 호출이 남아 있다는 신호입니다. push/pop 오버헤드가 발생합니다.
-O2로 컴파일 시 compute의 어셈블리:
| |
call이 사라지고 lea + add + ret 세 명령으로 압축됩니다. 이것이 인라이닝된 상태입니다. 함수 경계가 없어지면서 컴파일러가 전체를 한 번에 최적화할 수 있습니다.
구체적인 어셈블리 예시: 벡터화 확인
루프가 벡터화되었는지 어셈블리에서 확인하는 방법입니다.
| |
-O2 -march=native로 컴파일 시 (AVX2 지원 머신에서 GCC/Clang):
| |
vmovups, vmulps — v 접두사가 붙은 명령이 SIMD 벡터 연산의 신호입니다. ymm* 레지스터는 256비트(8 × float), xmm*은 128비트(4 × float), zmm*은 512비트(16 × float)입니다.
벡터화가 안 된 경우 (-O0 또는 루프 패턴이 벡터화를 막는 경우):
| |
movss, mulss — ss(single scalar)가 붙은 명령은 한 번에 하나의 float만 처리합니다.
Godbolt 활용 워크플로우
단계별 사용법:
- godbolt.org에 소스를 붙여 넣습니다.
- 우측 컴파일러 탭에서
x86-64 gcc 13.2또는x86-64 clang 17.0등을 선택합니다. - 컴파일러 옵션에
-O2 -march=native(또는-O2 -mavx2)를 입력합니다. - 소스 라인을 클릭하면 해당 라인이 생성한 어셈블리 구간이 강조됩니다.
- “Add new compiler” 버튼으로 GCC와 Clang을 나란히 두고 비교합니다.
-O2와-O3를 번갈아 입력해 레벨 차이도 확인합니다.
Godbolt에서는 로컬 환경 없이 빠르게 실험할 수 있어, “이 패턴이 벡터화되는지”, “인라인이 됐는지"를 즉시 확인하기에 유용합니다.
함수 경계와 호출 규약
**호출 규약(calling convention)**은 인자 전달(레지스터 vs 스택), callee-saved vs caller-saved 레지스터, 스택 정렬 등을 정의합니다. x86-64에서는 보통 첫 몇 개 인자는 레지스터로 넘기고, 나머지는 스택에 넣습니다. 함수가 호출될 때마다 반환 주소 저장·레지스터 저장·스택 프레임 설정·복귀 시 복원이 일어나므로, 작은 함수가 자주 호출되면 이 비용이 누적됩니다. 인라인되면 이 경계가 사라져 호출/반환 오버헤드가 없어지고, 컴파일러가 호출부와 함께 추가 최적화를 할 수 있습니다. 어셈블리에서 call이 보이면 “함수 경계가 있다"고 보면 되고, 핫 루프 안에 call이 많으면 인라이닝 또는 함수 합치기를 검토할 만합니다.
hot 함수 형태 해석
- 인라인 여부: 해당 호출 지점에
call이 없고, 그 함수의 본문에 해당하는 명령들이 그 자리에 펼쳐져 있으면 인라인된 것입니다.call _Z...가 반복되면 인라인되지 않은 호출이 남아 있는 것입니다. - 불필요한 저장/복원: 호출 전후로 많은
push/pop또는 스택에 대한 저장이 보이면, 레지스터 압박이나 호출 규약으로 인한 비용이 큽니다. 인라인되면 이런 저장이 줄어들거나 사라질 수 있습니다. - 분기: 조건문·루프가 복잡하게 나뉘어 있으면 분기 예측 실패 비용이 클 수 있습니다. 프로파일과 함께 “실제로 자주 타는 경로"가 짧고 단순한지 확인하면 도움이 됩니다.
벡터화/루프 형태
- SIMD 명령: x86에서는
vmulps,vaddpd,vpaddd등 벡터 연산이 보이면 해당 루프가 벡터화된 것입니다. 레지스터 이름이xmm*,ymm*,zmm*이면 128/256/512비트 SIMD를 쓰는 것입니다. - 루프 언롤링: 같은 연산이 여러 번 반복되어 한 번에 처리되는 형태로 펼쳐져 있으면 언롤링된 것입니다. 루프 백엣지(분기)가 줄어든 대신 코드가 길어집니다.
- 루프가 사라짐: 상수 횟수 작은 루프는 완전히 펼쳐져서 루프 제어 분기가 없어질 수 있습니다.
벡터화가 안 된 루프는 스칼라 연산(add, mul 등)만 나열되고, 인덱스 증가·조건 점검 분기가 반복됩니다. 벡터화·언롤링이 성능에 중요하다면 이 챕터와 챕터 01(최적화 플래그), 04(컴파일러 비교)를 함께 참고해 플래그·컴파일러를 조정하면 됩니다.
루프 언롤링 확인
루프 언롤링은 루프 바디를 여러 번 복사해 반복 횟수를 줄이는 최적화입니다. 어셈블리에서 같은 명령 패턴이 연속해서 나타나면 언롤링된 것입니다.
| |
실전 진단 절차
- 핫 함수를 profiler(perf, VTune)로 찾는다.
- 해당 함수만 포함한 최소 소스를 만든다.
-S -O2 -fverbose-asm또는 Godbolt로 어셈블리를 뽑는다.call유무로 인라이닝 여부,vmov/vadd유무로 벡터화 여부를 확인한다.- 원하는 최적화가 안 됐다면 이유(가시성, 루프 패턴, 플래그)를 확인하고 수정한다.
- LTO 환경에서는
objdump -d ./app | grep -A 30 "<func_name>"으로 최종 바이너리를 확인한다.
용어 정리
| 용어 | 설명 |
|---|---|
| 호출 규약(calling convention) | 인자 전달(레지스터/스택), callee/caller 저장 레지스터, 스택 정렬 등; 플랫폼별로 정의됨 |
| SIMD | Single Instruction Multiple Data; 벡터 연산. x86에서는 xmm/ymm/zmm 레지스터 사용 |
| vmovups | AVX/AVX2 비정렬 벡터 로드/스토어 명령; v=벡터, mov=이동, u=unaligned, ps=packed single |
| vmulps | AVX/AVX2 벡터 곱셈; 8개 float를 동시에 곱 |
| lea | Load Effective Address; 주소 계산을 덧셈·곱셈에 활용하는 명령 |
판단 기준: 언제 어셈블리를 볼지
| 상황 | 권장 | 비권장 |
|---|---|---|
| 인라이닝·벡터화 확인 | -S·objdump·Godbolt로 호출·SIMD 확인 | 추측만으로 결론 |
| LTO 사용 시 | 최종 바이너리 또는 LTO 단계 산출물 확인 | 단일 TU -S만 보고 판단 |
| hot 함수 형태 | call·push/pop·분기 밀도 확인 | 소스만 보고 최적화 |
자주 하는 실수
- LTO 켠 상태에서 단일 TU -S만 보고 판단: LTO를 쓰면 인라이닝이 링크 시점에 일어나므로, 한 .cc만 -S로 뽑으면 크로스-TU 인라인이 반영되지 않는다. 최종 바이너리를 objdump하거나 LTO 단계 산출물을 봐야 전체 그림이 맞다.
- 어셈블리 해석 없이 추측: “인라인됐을 것이다”, “벡터화됐을 것이다"라고 가정하지 말고, call 유무·SIMD 명령·루프 형태를 실제로 확인한 뒤 최적화 전략을 세운다.
학습 성과 목표
- -S, objdump -d, Godbolt로 컴파일 산출물을 확인할 수 있다.
- 함수 경계·호출 규약이 성능에 미치는 영향과, 인라인 시 사라지는 비용을 설명할 수 있다.
- 어셈블리에서 인라인 여부·SIMD·루프 언롤링을 해석할 수 있다.
비판적 시각: 한계와 트레이드오프
어셈블리를 보는 것은 진단 도구이지, 그 자체가 최적화는 아니다. “어셈블리가 짧으면 빠르다” 같은 단순 규칙은 없고, 호출이 많아도 그 함수가 핫하지 않으면 영향이 작을 수 있다. 프로파일로 실제 병목을 확인한 뒤, 해당 경로의 어셈블리를 보고 인라인·벡터화·분기 배치를 판단하는 순서를 지키는 것이 효율적이다.
핵심 요약
| 항목 | 요약 |
|---|---|
| 도구 | -S, -fverbose-asm, objdump -d, Godbolt |
| 확인 포인트 | call 유무(인라인), SIMD 명령(벡터화), 루프 형태 |
| LTO | 단일 TU -S는 불완전; 최종 바이너리 또는 LTO 산출물 확인 |
다음 장에서는
함수 멀티버저닝(CPU 기능별 다중 버전), GCC/Clang target 속성, 디스패치 비용을 다룹니다.
→ 함수 멀티버저닝 (챕터 07)
![Featured image of post [Compiler 03] 어셈블리 레벨 코드 생성 분석](/post/compiler-optimization/code-generation-analysis-assembly/wordcloud_hu_6cc9ba39d0f6d4a2.webp)
![[Compiler 03] GCC vs Clang vs MSVC 최적화 차이](/post/compiler-optimization/compiler-comparison-gcc-clang-msvc/wordcloud_hu_f9e92dca98d719c3.webp)
![[Compiler 03] 인라이닝 실패 진단](/post/compiler-optimization/inlining-diagnostics/wordcloud_hu_cf70a482efcb51cc.webp)
![[Compiler 03] 어셈블리 레벨 코드 생성 분석](/post/compiler-optimization/code-generation-analysis-assembly/wordcloud_hu_63d6f8980bdcdf0f.webp)
![[Compiler 03] CPU 기능별 함수 다중 버전](/post/compiler-optimization/function-multiversioning-cpu/wordcloud_hu_f5001b04a04f9a62.webp)
![[Compiler 03] 컴파일러 intrinsics 카탈로그](/post/compiler-optimization/compiler-intrinsics-catalog/wordcloud_hu_e930fa57b2241981.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] Introduction: Low-latency 컴파일러·빌드 최적화](/post/compiler-optimization/getting-started-compiler-build-performance-tuning/wordcloud_hu_54fcb9b6f9bee677.webp)