Featured image of post [Compiler 03] 인라이닝 실패 진단

[Compiler 03] 인라이닝 실패 진단

인라이닝이 되지 않는 원인(가시성, ODR/ABI, 코드 크기), GCC -fopt-info-inline·Clang -Rpass=inline 리포트 확인 방법, inline·LTO와 Tr.02(인라이닝 유도) 연계를 다룹니다. 진단 후 같은 TU·헤더 인라인·LTO로 대처하는 판단 기준을 제시합니다.

**인라이닝(inlining)**은 함수 호출을 호출부에 코드를 펼쳐 넣는 최적화입니다. µs 단위 최적화에서 핫 루프 안에 함수 호출이 남아 있으면, 그것은 매 반복마다 push/pop, 스택 프레임 설정, 반환 주소 저장 등의 고정 비용이 더해진다는 의미입니다. 이 비용은 함수 본체가 작을수록 상대적으로 큽니다. 예를 들어 1 nanosecond 짜리 연산을 하는 함수를 호출할 때, 함수 호출 오버헤드가 2~5 ns라면 본체보다 오버헤드가 더 큰 상황입니다.

인라이닝은 컴파일러가 자동으로 하는 것이 원칙이지만, 특정 조건이 맞지 않으면 인라인되지 않습니다. 이 챕터에서는 컴파일러 관점에서 실패 원인을 진단하고, 리포트와 어셈블리로 직접 확인하는 방법을 다룹니다.

인라이닝 실패의 주요 원인

컴파일러가 함수 호출을 인라인하지 않고 그대로 두는 이유는 크게 세 가지로 나눌 수 있습니다.

가시성(다른 번역 단위)

일반 컴파일 모델에서는 각 TU가 독립적으로 컴파일됩니다. 호출하는 쪽 TU에는 “이 함수의 몸체"가 보이지 않고, 선언만 보이는 경우가 많습니다. 컴파일러는 다른 오브젝트에 있는 구현을 볼 수 없으므로, 그 경계를 넘는 크로스-TU 인라이닝을 하지 못합니다. 헤더에 정의가 있는 인라인 함수나 템플릿 인스턴스는 호출하는 TU에서 본문이 보이므로 인라인 후보가 됩니다. 따라서 핫 경로에 있는 작은 함수는 호출하는 TU에서 정의가 보이도록 헤더에 두거나, 같은 .cc에 두는 것이 유리합니다.

ODR/ABI

ODR(One Definition Rule) 위반이 있거나, 서로 다른 컴파일 옵션·ABI로 컴파일된 코드가 섞이면, 컴파일러가 “같은 함수"로 확신하지 못해 인라인을 포기할 수 있습니다. 공유 라이브러리 경계(DLL/SO)를 넘는 호출은 보통 인라인되지 않고, 호출 규약에 따른 호출 비용이 남습니다. 헤더 전용 라이브러리나 LTO를 쓰면 이런 경계가 완화됩니다.

코드 크기 한도

인라이닝을 무한히 하면 코드 크기가 커져 I-cache 압박과 컴파일 시간이 늘어납니다. 컴파일러는 내부 휴리스틱(함수 크기, 호출 횟수, 루프 내부 여부 등)으로 “인라인할 가치가 있는지"를 판단하고, 한도(-finline-limit 등)를 넘으면 인라인을 하지 않습니다. 작고 자주 호출되는 함수일수록 인라인되기 쉽고, 크고 복잡한 함수는 호출로 남는 경우가 많습니다.

인라이닝 리포트 확인 방법

컴파일러가 인라이닝 결정 과정을 리포트로 출력하도록 할 수 있습니다. 이를 통해 “어떤 함수가 인라인됐는지”, “왜 안 됐는지"를 텍스트로 확인할 수 있습니다.

GCC: -fopt-info-inline

1
2
# 인라이닝 리포트 출력 (stderr로 나옴)
g++ -O2 -fopt-info-inline foo.cc -o foo 2>&1 | grep "inlined\|not inlined"

출력 예시:

1
2
foo.cc:5:10: note: Inlining add (a call from compute).
foo.cc:12:8: note: Not inlining helper into process: --param max-inline-insns-single limit reached.
  • Inlining add (a call from compute)compute에서 add를 인라인했다는 메시지
  • not inlined ... limit reached — 코드 크기 한도 초과로 인라인 거부

더 자세한 정보가 필요하면 -fopt-info-all 또는 -fopt-info-optimized로 전체 최적화 리포트를 볼 수 있습니다.

Clang: -Rpass=inline

1
2
3
4
5
# 인라이닝된 함수와 실패 이유 출력
clang++ -O2 -Rpass=inline foo.cc -o foo 2>&1

# 인라이닝 실패(거부) 이유만 보기
clang++ -O2 -Rpass-missed=inline foo.cc -o foo 2>&1

출력 예시:

1
2
foo.cc:5:10: remark: 'add' inlined into 'compute' with (cost=-30, threshold=225) [-Rpass=inline]
foo.cc:12:8: remark: 'big_func' not inlined into 'caller' with (cost=340, threshold=225) [-Rpass-missed=inline]
  • cost=-30, threshold=225 — cost가 threshold보다 낮으면 인라인됩니다. cost가 높으면(함수가 크면) 인라인을 거부합니다.
  • -Rpass-missed=inline으로 실패 케이스만 필터링하면 노이즈가 줄어듭니다.

어셈블리로 최종 확인

리포트와 함께 어셈블리에서 직접 call 유무를 확인합니다. 챕터 06의 방법대로 -S로 어셈블리를 뽑아서, 해당 호출 지점에 call _Z...가 남아 있으면 인라인되지 않은 것이고, 인라인되면 호출이 사라지고 해당 함수 본문이 펼쳐져 들어가 있습니다.

LTO를 켜면 링크 시점에 인라이닝이 일어나므로, 단일 TU 어셈블리만 보면 “인라인 안 됨"처럼 보이더라도 최종 바이너리에서는 인라인되었을 수 있습니다. LTO 사용 시에는 LTO 최적화 리포트(GCC -flto-report, Clang의 적절한 플래그)나 최종 디스어셈블리를 확인하는 것이 정확합니다.

inline / attribute / LTO가 가시성에 미치는 영향

  • inline: C++에서 inline은 “여러 TU에 정의가 있어도 ODR만 지키면 된다"는 의미에 가깝고, “반드시 인라인해라"는 힌트가 아닙니다. 다만 인라인 후보로 고려되기는 하고, 헤더에 정의를 두면 모든 TU에서 본문이 보이므로 인라인 기회가 생깁니다.
  • attribute((always_inline)): GCC/Clang에서 “가능한 한 인라인해라"는 강한 힌트를 줍니다. 크기가 커져도 인라인을 시도하므로, 정말 작고 핫한 함수에만 쓰는 것이 좋습니다.
  • LTO: 링크 시점에 모든 TU의 IR이 있으므로 가시성 제약이 사라집니다. 다른 오브젝트에 있는 함수도 인라인할 수 있어, “다른 .cc에만 있는 작은 함수"가 LTO에서 비로소 인라인되는 경우가 많습니다.

가능하면 핫 경로는 같은 TU에 두거나 헤더 인라인 + LTO로 정리하고, 인라이닝 리포트로 실제로 인라인되었는지 확인하는 흐름을 권장합니다.

실전 시나리오: 인라이닝이 안 될 때 점검 순서

  1. 가시성: 해당 함수 정의가 호출하는 TU에서 보이는가? 헤더에 인라인 정의가 있거나, 같은 .cc에 있으면 인라인 후보가 됨. 다른 .cc에만 있으면 LTO를 켜거나 해당 함수를 헤더/같은 TU로 옮긴다.
  2. 리포트 확인: GCC -fopt-info-inline, Clang -Rpass=inline으로 인라인되지 않은 이유(크기 한도·가시성 등)를 확인한다.
  3. 어셈블리: -S로 해당 호출 지점에 call이 남아 있는지 본다. LTO를 켠 경우에는 최종 바이너리 또는 LTO 단계 산출물을 확인한다.
  4. 코드 구조 변경: Tr.02의 인라이닝 유도(헤더 인라인·final·같은 TU 배치)를 적용한 뒤 다시 2~3을 확인한다.

한눈에 보기: 인라이닝 실패 원인

원인설명대처
가시성다른 TU에 구현만 있고 호출 TU에 본문이 안 보임같은 TU·헤더 인라인·LTO
ODR/ABI정의 불일치·DLL/SO 경계헤더 전용·LTO·ABI 통일
코드 크기휴리스틱 한도 초과함수 축소·always_inline(주의)

판단 기준: 언제 진단할지

상황권장비권장
핫 경로에 호출 비용 의심인라이닝 리포트·어셈블리 확인추측만 하고 코드 변경
LTO 켠 뒤에도 인라인 안 됨ODR·크기 한도·리포트 확인LTO만 의존
인라인 유도 후 검증-fopt-info-inline 등으로 재확인변경 후 측정 생략

자주 하는 실수

  • always_inline 과다 사용: __attribute__((always_inline))를 여러 함수에 걸면 코드 크기가 불어나 I-cache 압박과 컴파일 시간이 늘어난다. 정말 작고 핫한 함수 한두 개에만 쓰고, 나머지는 컴파일러 휴리스틱에 맡기는 것이 좋다.
  • LTO만 믿고 가시성 무시: LTO가 크로스-TU 인라이닝을 해 주지만, ODR 위반·ABI 불일치·크기 한도에 걸리면 인라인되지 않을 수 있다. 인라인이 안 될 때는 리포트와 어셈블리로 원인을 확인한 뒤 코드 구조(같은 TU·헤더 인라인)를 보완한다.
  • 변경 후 측정 생략: 헤더로 옮기거나 LTO를 켠 뒤 “이제 인라인되겠지"라고 가정하지 말고, -fopt-info-inline 또는 -Rpass=inline으로 실제로 인라인되었는지 확인하고, 필요하면 -S로 호출이 사라졌는지 본다.

학습 성과 목표

  • 인라이닝 실패의 가시성·ODR/ABI·코드 크기 원인을 설명하고, 각각에 대한 대처를 말할 수 있다.
  • GCC -fopt-info-inline, Clang -Rpass=inline으로 인라이닝 리포트를 확인할 수 있다.
  • Tr.02(인라이닝 유도)과 연계해, 진단 후 코드 구조·LTO로 인라이닝을 유도할 수 있다.

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

인라이닝은 무조건 많을수록 좋은 것이 아니다. 과도한 인라이닝은 코드 크기·컴파일 시간을 늘리고 I-cache를 압박할 수 있다. 컴파일러는 크기 한도와 호출 빈도를 보고 “인라인할 가치"를 판단하므로, 항상_inline으로 강제하기보다는 핫 경로를 같은 TU·헤더에 두고 LTO로 기회를 주는 쪽을 우선한다. “인라인 안 됨 = 무조건 나쁨"이 아니라, 측정 후 병목이 호출 비용일 때만 집중적으로 유도하는 것이 합리적이다.

핵심 요약

항목요약
실패 원인가시성(다른 TU)·ODR/ABI·코드 크기 한도
진단-fopt-info-inline(GCC), -Rpass=inline(Clang), -S 어셈블리
대처같은 TU·헤더 인라인·LTO·always_inline(선택)

Tr.02(언어 최적화)과의 연계

Low-latency C++ 언어 최적화 트랙(Tr.02)에서는 인라이닝을 유도하는 코드 구조를 다룹니다. 예를 들어 작은 함수를 헤더에 두기, 템플릿으로 구현을 호출부에 노출하기, final로 devirtualization을 유도하기 등입니다. 이 챕터(컴파일러 트랙)는 “왜 인라인되지 않았는지"를 진단하는 쪽에 초점을 둡니다. 코드 구조를 바꾼 뒤에도 인라이닝이 안 되면, 위의 가시성·ODR·크기 한도를 리포트와 어셈블리로 확인해 보면 됩니다.

다음 장에서는

어셈블리 레벨 코드 생성 분석, -S·objdump·Godbolt 사용, 함수 경계·호출 규약·hot 함수 형태 해석을 다룹니다.

코드 생성 분석 (챕터 06)