인라인 함수
C++ inline의 정의 도달성·ODR 언어 계약과 optimizer의 실제 인라인화 판단을 분리하고 측정 기준을 익힙니다.
함수는 코드 재사용과 모듈화에 큰 이점을 줍니다. 최적화 뒤에도 실제 함수 호출이 남는다면 호출·복귀와 호출 규약에 따른 비용이 생길 수 있지만, 모든 소스 코드의 호출이 반드시 별도 스택 프레임이나 기계어 call 명령으로 이어지는 것은 아닙니다.
C++의 inline을 이해할 때는 다음 두 층을 분리해야 합니다.
- 언어 계약: inline 함수의 선언, 정의 도달성(reachability), 여러 번역 단위에 놓인 정의와 ODR(One Definition Rule).
- optimizer 변환: 특정 호출 지점에서 함수 본문을 실제로 펼칠지, 일반 호출을 남길지에 대한 구현의 비용 판단.
표준은 inline 지정자가 호출 위치의 본문 치환을 선호한다는 뜻도 부여하지만, 구현에 치환을 강제하지 않습니다. 치환하지 않더라도 inline 함수의 언어 규칙은 그대로 적용됩니다.
LANGUAGE CONTRACT · OPTIMIZER TRANSFORM
inline 함수와 실제 인라인화는 같은 결정이 아니다
inline은 호출 치환 선호만 나타내는 키워드가 아니라 정의와 ODR에 영향을 주는 언어 지정자입니다. 호출을 본문으로 펼칠지는 별도의 optimizer 비용 모델이 결정합니다.
헤더 정의는 도달성과 다중 정의 규칙을 만족시킨다
inline 함수로 선언
inline int add(int a, int b)처럼 선언합니다. 표준은 호출 위치 치환을 선호한다는 뜻도 나타내지만 구현에 치환을 강제하지 않습니다.정의를 도달 가능하게 유지
inline 함수가 odr-use된 각 definition domain의 끝에서는 정의가 도달 가능해야 합니다. 일반적인 헤더와 번역 단위 모델에서는 정의를 헤더에 두어 각
.cpp에 포함합니다.ODR의 동일 정의 요건
named module에 붙지 않은 inline 함수는 요건을 만족하는 정의를 여러 번역 단위에 둘 수 있으며, 프로그램에서는 주소가 하나인 단일 entity로 취급됩니다.
.cpp경계를 구분일반 함수는 헤더 선언과 한
.cpp의 정의로 충분합니다. 다른 번역 단위에서 odr-use할 inline 정의를 한.cpp에만 숨기는 것은 “일반 호출 fallback”이 아니라 reachability 규칙 위반입니다.
실제 치환은 호출 지점의 이익과 비용으로 다시 판단한다
분석 가능한 본문 확보
같은 번역 단위에 본문이 있으면 바로 분석할 수 있고, LTO를 사용하면 optimizer가 여러 번역 단위의 정보를 합쳐 cross-TU 최적화를 할 수도 있습니다.
호출 지점별 비용 비교
본문 크기, 호출 빈도, 상수 인자, target과 최적화 옵션, profile 정보, 코드 증가와 instruction-cache 비용을 함께 봅니다.
펼치거나 호출을 유지
이익이 크면
square(x)호출이x * x수준으로 펼쳐질 수 있고, 아니면 실제 함수 호출을 남깁니다.키워드와 변환은 독립
optimizer는
inline이 없는 함수도 펼칠 수 있고 inline 함수의 호출도 남길 수 있습니다. 치환하지 않아도 inline의 reachability와 ODR 규칙은 사라지지 않습니다.
경계: 헤더의 inline 정의는 주로 언어 차원의 정의 배치 계약을 해결합니다. 성능은 최적화된 실제 바이너리와 profile로 확인합니다.
인라인 함수란 무엇인가?
함수 선언에 inline 지정자를 붙이면 inline 함수를 선언합니다.
inline 반환타입 함수이름(매개변수타입1 매개변수이름1, ...) {
// 함수 몸체
}다음 예제의 getMax는 inline 함수이지만, 이 키워드만 보고 실제 기계어가 호출 치환 형태라고 단정할 수는 없습니다.
#include <iostream>
inline int getMax(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int x = 10;
int y = 20;
std::cout << getMax(x, y) << '\n';
std::cout << getMax(50, 30) << '\n';
}소스 코드에서 중요한 규칙은 다음과 같습니다.
inline은 함수의 linkage를 바꾸지 않습니다.- inline 함수가 odr-use된 각 definition domain의 끝에서는 그 정의가 도달 가능해야 합니다.
- 외부 linkage를 가진 inline 함수가 여러 definition domain에 선언되면 inline 선언도 일관되게 도달 가능해야 합니다. 규칙 위반에 대한 진단이 보장되지 않는 경우가 있으므로 linker의 우연한 동작에 기대면 안 됩니다.
- named module에 붙지 않은 inline 함수는 ODR의 일치 요건을 만족하는 정의를 여러 번역 단위에 둘 수 있습니다. 이때도 프로그램에서는 주소가 하나인 단일 entity입니다.
constexpr함수는 암시적으로 inline이며, global module에서 클래스 정의 안에 정의한 멤버 함수도 암시적으로 inline입니다.
이 장에서는 일반적인 헤더와 소스 파일 모델을 다룹니다. C++20 named module에 붙은 inline 함수에는 같은 정의를 여러 번역 단위에 두는 설명을 그대로 적용할 수 없는 추가 규칙이 있습니다.
언어 계약과 optimizer 결과 비교| 질문 | inline 언어 규칙 | 실제 인라인화 |
|---|---|---|
| 무엇을 정하는가? | 선언·정의 reachability와 ODR | 호출을 본문으로 펼칠지 여부 |
| 헤더에 정의하는 이유 | odr-use하는 번역 단위에서 정의를 보이게 하고 허용된 다중 정의를 사용하기 위해 | 같은 번역 단위에서 본문 분석 기회를 주지만 치환을 보장하지 않음 |
inline이 없으면? | 일반 비-template 외부 함수는 보통 프로그램 전체에 하나의 정의를 둠 | optimizer가 이익이 있으면 여전히 펼칠 수 있음 |
inline이 있으면? | 치환 여부와 무관하게 inline의 ODR 규칙을 지켜야 함 | optimizer가 이익이 없으면 일반 호출을 유지할 수 있음 |
| 코드 크기는? | 키워드 자체로 결정되지 않음 | 실제로 여러 호출 지점에 펼쳤을 때 증가할 수 있음 |
optimizer는 호출 지점마다 다시 판단한다
실제 인라인화는 보통 다음 정보를 함께 사용합니다.
- 함수 본문의 크기와 제어 흐름.
- 호출 지점이 실제로 자주 실행되는지에 대한 정적 추정이나 profile 정보.
- caller에서 보이는 상수 인자와 치환 뒤 가능한 상수 전파·분기 제거.
- target, 최적화 수준, PGO(Profile-Guided Optimization)와 LTO(Link-Time Optimization) 설정.
- 본문 복제로 늘어나는 binary 크기와 instruction-cache 비용.
작고 자주 실행되는 함수는 좋은 후보일 수 있지만, 함수 크기나 호출 형태 하나만으로 결과가 정해지지는 않습니다.
- 재귀 호출을 무한히 펼칠 수는 없어도, 비재귀 호출 지점이나 제한된 깊이, 일부 경로를 인라인화할 수 있습니다.
- 함수 포인터나 virtual 호출도 target을 알아내거나 devirtualize할 수 있다면 직접 호출로 바뀐 뒤 후보가 될 수 있습니다.
- 큰
for루프,switch또는 복잡한 제어 흐름은 비용을 높일 수 있지만 절대적인 비인라인 조건은 아닙니다. - 정상적으로 한
.cpp에 정의한 일반 함수도 LTO가 번역 단위 정보를 결합하면 cross-TU 인라인화가 가능할 수 있습니다. 이는 inline 함수의 정의 reachability 규칙을 대신하지 않습니다.
따라서 getMax처럼 짧은 함수도 “인라인화될 가능성이 있는 후보”라고 표현해야 하며, 최종 결과는 해당 compiler·옵션·호출 지점의 최적화 기록이나 생성 코드를 확인해야 합니다.
헤더와 .cpp 경계를 정확히 나누기
보통 각 .cpp는 포함한 헤더 내용과 함께 하나의 번역 단위가 됩니다.
일반 외부 함수는 헤더에 선언하고 한 .cpp에 정의하는 구조가 올바릅니다.
// 선언
int add(int a, int b);#include "my_math.h"
// 프로그램의 한 정의
int add(int a, int b) {
return a + b;
}다른 번역 단위는 본문을 직접 보지 못하므로 일반적으로 out-of-line 호출을 만들 수 있지만, LTO를 사용하면 link-time에 본문을 분석해 인라인화할 수도 있습니다.
반면 여러 번역 단위에서 odr-use하는 inline 함수는 정의가 각 definition domain에서 도달 가능해야 하므로 보통 헤더에 정의합니다.
#pragma once
inline int add(int a, int b) {
return a + b;
}#include <iostream>
#include "my_math.h"
int main() {
int sum = add(10, 20);
std::cout << "합계: " << sum << '\n';
}헤더가 여러 .cpp에 포함되어 정의가 반복되더라도 ODR의 일치 요건을 만족하면 허용됩니다. 단순히 “linker가 중복을 합친다”가 아니라 언어가 하나의 entity로 취급하도록 규정한 것입니다.
다른 번역 단위에서도 inline으로 선언하고 odr-use하는 함수를 한 .cpp에만 정의하는 것은 일반 호출로 자동 전환되는 정상 패턴이 아닙니다. 정의 reachability 규칙을 위반한 ill-formed 프로그램이며 진단이 보장되지 않을 수 있습니다.
함수 template도 보통 인스턴스화 지점에서 정의를 볼 수 있도록 헤더에 둡니다. 하지만 template의 다중 정의 허용은 template·ODR 규칙에서 오며, 그 이유만으로 inline을 붙일 필요는 없습니다.
template <typename T>
T sum(T a, T b) {
return a + b;
}인라인 함수와 함수형 매크로
C와 오래된 C++ 코드에서는 호출 모양을 만들기 위해 함수형 매크로를 사용하기도 합니다.
#define SQUARE(x) ((x) * (x))매크로는 함수가 아니라 preprocessing 단계의 텍스트 치환입니다.
- 타입과 scope 규칙이 약함: 함수 overload, parameter type checking, namespace와 같은 언어 기능을 그대로 제공하지 않습니다.
- 인자를 여러 번 평가할 수 있음:
SQUARE(i++)는i++를 두 번 치환하며, 이 예제는 unsequenced modification으로 undefined behavior가 됩니다. - 진단과 디버깅이 어려움: 오류가 확장된 토큰에서 나타나고 일반 함수처럼 단계별 호출을 추적하기 어렵습니다.
함수는 각 인자 표현식을 한 번씩 평가하고 C++의 type·scope 규칙을 따릅니다. preprocessing이 꼭 필요한 조건부 컴파일, 문자열화 또는 token 결합이 아니라면 일반 함수, constexpr 함수나 함수 template을 먼저 선택합니다.
후보 선정과 측정
실제 인라인화를 검토할 때는 inline 표기부터 늘리지 말고 다음 순서를 따릅니다.
- 실제 target과 release 최적화 옵션, LTO·PGO 설정, 대표 입력을 고정합니다.
- profiler로 hot call site를 찾습니다.
- compiler optimization remark나 생성 assembly에서 실제 치환 여부와 미치환 이유를 확인합니다.
- runtime과 함께 binary 크기, instruction-cache 지표를 비교합니다.
CANDIDATE · COST · EVIDENCE
후보는 heuristic으로 고르고 최종 판정은 측정으로 내린다
함수 크기나 호출 형태 하나만으로 실제 인라인화 여부를 단정할 수 없습니다. optimizer가 보는 문맥과 release 설정을 맞춘 뒤 runtime, code size, cache 영향을 함께 확인합니다.
작고 뜨거우며 caller 문맥이 이익을 만든다
return a + b; 같은 짧은 본문이 hot path에서 반복되고, 상수 전파나 분기 제거처럼 치환 뒤의 추가 최적화가 기대되면 좋은 후보입니다. 본문이 보이거나 LTO가 정보를 가져올 수 있어야 합니다.
크고 차가운 함수는 코드 증가가 이익을 덮을 수 있다
큰 본문을 여러 호출 지점에 복제하면 binary와 instruction-cache 부담, 컴파일 시간, 디버깅 난도가 커질 수 있습니다. “루프 안 호출”이나 inline 표기만으로 hot하다고 가정하지 않습니다.
재귀·간접 호출·복잡한 제어 흐름도 문맥을 본다
- 재귀 호출을 무한히 펼칠 수는 없지만 비재귀 call site, 제한된 깊이 또는 일부 경로는 인라인화될 수 있습니다.
- 함수 포인터나 virtual 호출도 target이 밝혀지거나 devirtualize되면 직접 호출로 바뀌어 후보가 될 수 있습니다.
- 큰 루프나
switch는 비용을 높일 뿐 절대 금지 규칙은 아니며 partial inlining도 가능합니다. - 정상적으로 한
.cpp에 정의한 일반 함수도 같은 번역 단위의 호출이나 LTO 분석에서는 인라인화될 수 있습니다. 이는 inline 정의의 reachability 규칙을 대신하지 않습니다.
함수·template을 텍스트 치환과 혼동하지 않는다
함수는 scope와 type checking을 가지며 인자를 한 번씩 평가합니다. 반면 SQUARE(i++) 같은 매크로는 인자를 두 번 치환해 unsequenced modification 같은 undefined behavior를 만들 수 있습니다.
조건부 컴파일, 문자열화, token 결합처럼 preprocessing이 필요한 경우가 아니라면 일반 함수, constexpr 함수나 template을 먼저 선택합니다.
키워드가 아니라 증거로 유지 여부를 결정한다
비교 조건 고정
실제 target, release 최적화 옵션, LTO·PGO 설정과 대표 입력을 맞춘 기준 binary를 준비합니다.
hot call site 확인
profiler로 시간이 쓰이는 호출을 찾고 compiler optimization remark나 생성 assembly에서 치환·미치환 이유를 확인합니다.
속도와 크기를 함께 비교
runtime뿐 아니라 binary 크기와 instruction-cache 지표도 비교합니다. 개선이 재현되지 않으면 표기나 강제 attribute를 늘리지 않습니다.
판정 순서: 언어 계약을 먼저 맞춘 뒤 optimizer에 후보를 맡기고, profile과 최적화 기록으로 실제 병목에서 이득이 있었는지 확인합니다.
핵심은 두 문장으로 정리할 수 있습니다.
- 헤더의
inline정의는 정의 도달성과 ODR을 위한 언어 계약입니다. - 성능을 위한 실제 인라인화는 optimizer가 호출 지점별로 수행하고, 개발자는 측정으로 결과를 확인합니다.