인라인 최적화: 인라인이 20배 만든 이유 호출·SIMD 최적화

함수 호출 비용, 생각보다 크다: inline이 20배를 만든 이유
“함수로 쪼개서 깔끔하게” 코딩하는 게 기본이지만, 성능이 걸린 순간엔 그 함수 호출 자체가 병목이 되기도 해요.
특히 컴파일러의 inlining(인라인 최적화)이 들어가면, 단순히 호출 비용을 줄이는 수준을 넘어 코드가 아예 다른 형태로 변신할 수 있습니다.
1) 함수 호출 오버헤드(부담)는 왜 생길까요?
요약하면, 함수 호출은 “점프 한 번”이 아니라 준비 작업+이동+복귀가 포함된 작은 절차예요.
인자가 조금이라도 복잡해지면 레지스터/스택에 값을 옮기고 저장·복구하는 load/store가 늘어날 수 있고, 호출 규약(calling convention) 때문에 함수 앞뒤로 부가 명령이 붙기도 해요.
그래서 “호출은 싸지만 공짜는 아니다”가 핵심이에요.
특히 루프 안에서 수천만 번 호출되는 아주 작은 함수는, 호출 오버헤드가 본체보다 커져 버릴 수 있습니다.
2) inlining(인라인)이 진짜 무서운 이유: 단순 치환이 아니라 “추가 최적화의 문”을 열어요
요약하면 inline은 단순히 코드를 복붙하는 게 아니라, 컴파일러가 더 공격적인 최적화를 할 수 있게 만드는 트리거가 됩니다.
예를 들어 add(x, y) 같은 작은 함수를 루프 안에서 부르면, 컴파일러가 호출 경계를 넘어서기 어려워져요. 반대로 인라인되면 “루프 전체가 하나의 덩어리”처럼 보이면서 최적화 여지가 확 커집니다.
원문에서도 add3()가 add()를 부르는 코드를 사람이 직접 인라인하면 x+y+z가 되는 것처럼 설명해요.
중요한 포인트는, 이런 변화가 쌓이면 SIMD(벡터 명령) 같은 고급 최적화로 이어질 수 있다는 점입니다.

3) 벤치마크 1: 정수 배열 합산에서 인라인이 “20배 이상” 빨라진 이유
요약하면, 인라인 버전이 빨라진 핵심은 호출 제거가 아니라 SIMD로 16개 정수를 한 번에 처리하도록 바뀐 데 있어요.
원문에서 for (int x : numbers) sum = add(sum, x);처럼 루프마다 add를 호출하면, 어셈블리 레벨에서 매 반복마다 호출/분기 흐름이 들어가고 결과적으로 정수 1개 더하는 데 약 6개 명령, 3사이클 정도를 쓴다고 설명합니다.
그런데 인라인이 되면 컴파일러가 루프를 벡터화(vectorization)해서, 16개 정수를 8개 명령으로 처리하는 SIMD 코드로 바꿔버려요.
즉, “정수 하나당 6개 명령”이 “정수 하나당 0.5개 명령” 수준으로 떨어지고, CPU가 한 사이클에 더 많은 명령을 처리(retire)하면서 체감 성능이 폭발합니다.
원문에선 이 결과로 인라인 버전이 20배 이상 빠르다는 그래프가 나와요.
- 포인트 1: 인라인 = SIMD 최적화로 가는 입장권일 수 있어요.
- 포인트 2: “호출 비용”보다 더 큰 차이는 컴파일러가 전체를 보고 최적화하느냐입니다.
4) SIMD를 막아도 10배: ‘호출 제거’ 자체도 의미가 있어요
요약하면 SIMD를 금지해도 인라인이 약 10배 빠른 결과가 나왔어요.
이건 “벡터화”라는 대박 최적화가 없더라도, 호출/복귀, 레지스터 저장·복구 같은 비용이 루프에서 계속 반복되면 꽤 큰 누적 비용이 된다는 뜻이에요.
실무에서 종종 “벡터화는 환경 따라 안 될 수도” 있는데, 그때도 인라인은 기본 이득을 주는 경우가 많습니다.
특히 아주 짧은 함수가 핫 루프(hot loop) 안에 있다면, 인라인 여부만으로도 병목이 크게 달라질 수 있어요.

5) 벤치마크 2: 공백 개수 세기 함수는 왜 ‘긴 문자열’에서 인라인 이득이 거의 없을까요?
요약하면 입력이 커지면 함수 내부 루프 비용이 커져서, 호출 오버헤드가 상대적으로 무시될 정도로 작아지기 때문이에요.
원문은 문자열에서 공백을 세는 count_spaces(std::string_view) 예시를 들고, 길이 1000의 문자열을 넣으면 인라인이 빠르지 않거나 오히려 약간 느릴 수도 있었다고 말합니다(왜 그런지는 확신 못 한다고도 언급).
하지만 0~6자 같은 아주 짧은 문자열에서는 인라인이 “측정 가능하게” 더 빨랐어요.
즉, 함수가 “빠를 수도 느릴 수도” 있는 타입이라면, 인라인 여부는 입력 크기/패턴에 따라 결론이 달라지는 문제가 됩니다.
실제로 댓글에서도 -O3 최적화 레벨에서, 작은 입력은 차이가 나지만 큰 입력은 거의 차이가 없다는 결과가 공유돼요.
6) 실무에서 이렇게 써먹어보세요: 인라인 판단 체크리스트 & 시나리오
요약하면, 핫패스의 작은 함수는 인라인을 적극 고려하고, 문자열/IO처럼 비용이 큰 작업은 “입력 크기 기준”으로 판단하는 게 좋아요.
원문 Takeaway도 딱 두 줄로 정리합니다. 짧고 단순한 함수는 인라인이 강력하고, “입력에 따라 빠르거나 느린 함수”는 입력 크기가 결정 변수라는 것요.
실제 적용 시나리오를 들면 이런 식입니다.
- 로그/메트릭 수집 루프에서 작은 변환 함수가 반복 호출됨
- 함수 호출이 누적되면 병목이 되기 쉬워요.
static inline또는 LTO(링크 타임 최적화) 같은 옵션까지 검토할 가치가 있어요.
- 함수 호출이 누적되면 병목이 되기 쉬워요.
- 파서/인코더에서 아주 작은 유틸 함수가 tight loop에 있음
- 인라인되면 컴파일러가 루프를 통째로 최적화(벡터화 포함)할 수 있어 예상 밖의 큰 점프가 나올 수 있어요.
- 문자열 처리처럼 입력이 큰 작업
- 긴 입력에선 호출 비용이 묻히니, 무작정 인라인하기보다 **짧은 입력(예: 토큰 길이)**에서만 이득이 있는지 벤치로 확인하는 게 안전해요.
마지막으로, Lemire는 벤치마크 소스 코드를 GitHub에 공개해뒀다고 하니, 비슷한 환경(컴파일러/CPU/옵션)에서 직접 돌려보는 게 가장 확실합니다.
마무리: “함수 호출 비용”보다 중요한 건, 컴파일러가 최적화할 수 있는 형태로 만들었는가
함수 호출은 보통 큰 문제가 아니지만, 짧은 함수 + 핫 루프 조합에선 이야기가 달라져요.
특히 inlining은 호출을 없애는 수준을 넘어, SIMD 같은 최적화를 열어 20배 차이까지 만들어낼 수 있다는 걸 이 글이 잘 보여줍니다.
지금 성능 이슈가 있는 코드가 있다면, “호출이 많고 단순한 함수”부터 한 군데 골라서 inline/최적화 옵션/벤치마크를 붙여 전후 어셈블리와 성능을 같이 비교해보세요. 생각보다 빨리 ‘진짜 병목’이 드러날 거예요.





