이번에는 아주 간단하게 매크로 함수, 인라인 함수이 둘의 차이점에 대해서 알아보도록 하겠다.
매크로 함수는 전처리기 단계에서 처리되는 코드이다. 예를 들어
#include <iostream>
#define SQUARE(x) ((x) * (x))
int main(void)
{
std::cout << SQUARE(5) << std::endl;
return 0;
}
위의 코드는 컴파일 전, 전처리 과정을 거치면 다음과 같이 변경된다.
#include <iostream>
int main(void)
{
std::cout << ((3) * (3)) << std::endl;
return 0;
-> 이처럼 함수의 몸체부분이 함수호출 문장을 완전히 대체했을 경우, "함수가 인라인화 되었다"라고 표현한다.
생긴 것은, 함수같지만 실제로는 함수가 아닌 문자열 치환이기 때문에
우리가 일반적으로 작성하는 함수보다 빠른 속도를 가진다는 이점이 있다.
하지만, 이에 따라 한계도 분명 존재하는데...
1. 디버깅이 불편하다. (문자열 치환이기때문에 양날의 검)
2. 괄호 누락 등으로 오류 발생 가능성 (한 줄에 다 작성해야 함)
3. 복잡한 함수를 정의하는데 한계
이에, 복잡하고 정의하기 까다로운 매크로 함수 대신, 일반 함수처럼 정의가 가능한 inline함수가 C++에 존재한다.
인라인 함수는 inline 키워드를 사용해 함수의 내용을 함수 호출부에 복사하도록 한다.
inline int square(int x) {
return x * x;
}
컴파일러는 이 함수를 호출하는 곳에 실제로 x * x 코드를 붙여넣는다.
→ 마치 매크로 함수처럼 보이지만, 타입 안정성과 디버깅이 매크로함수보다 사용하기 편해보인다.
하지만 이 인라인 함수도 마냥 좋은점만 있는 것은 아니다.
inline은 요청일 뿐, 컴파일러가 반드시 인라인화시켜준다는 보장이 없다.
매크로를 이용한 함수의 인라인화는 전처리기에 의해서 처리되지만,
inline 키워드를 통한 인라인화는 컴파일러에 의해서 처리가 된다. 따라서 컴파일러는 함수의 인라인화가 오히려 성능에 해가 된다고 판단할 경우, 키워드 자체를 무시해버리기도 한다.
함수는 매 호출 시 다음 과정을 거치게 되는데
함수 호출 ▶ 인자 복사 (또는 참조) ▶ 스택 프레임 생성 ▶ 리턴
이러한 오버헤드가 생기기 때문에, 매우 자주 호출되는 짧은 함수라면 인라인 함수에 비해 상대적으로 느릴 수 있다.
하지만 요즘 컴파일러는 최적화를 매우 잘 하기 때문에, 경우에 따라 큰 차이가 없기도 하다.
이제 매크로 함수, 인라인 함수, 일반 함수 이 셋의 성능을 비교해보도록 하자.
테스트 내용: 제곱 연산을 10억 번 반복해 걸리는 시간 측정
Code
#include <iostream>
#include <chrono>
// 매크로 함수
#define SQUARE_MACRO(x) ((x) * (x))
// 인라인 함수
inline int square_inline(int x) {
return x * x;
}
// 일반 함수
int square_normal(int x) {
return x * x;
}
int main() {
const int loop_count = 1000000000;
volatile int result = 0; // 최적화 방지용
// 매크로 함수
auto start_macro = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loop_count; ++i)
result = SQUARE_MACRO(i);
auto end_macro = std::chrono::high_resolution_clock::now();
// 인라인 함수
auto start_inline = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loop_count; ++i)
result = square_inline(i);
auto end_inline = std::chrono::high_resolution_clock::now();
// 일반 함수
auto start_normal = std::chrono::high_resolution_clock::now();
for (int i = 0; i < loop_count; ++i)
result = square_normal(i);
auto end_normal = std::chrono::high_resolution_clock::now();
// 출력
auto duration_macro = std::chrono::duration_cast<std::chrono::milliseconds>(end_macro - start_macro).count();
auto duration_inline = std::chrono::duration_cast<std::chrono::milliseconds>(end_inline - start_inline).count();
auto duration_normal = std::chrono::duration_cast<std::chrono::milliseconds>(end_normal - start_normal).count();
std::cout << " Macro function time: " << duration_macro << " ms\n";
std::cout << " Inline function time: " << duration_inline << " ms\n";
std::cout << " Normal function time: " << duration_normal << " ms\n";
return 0;
}
결과

결과를 보면 우선,
매크로 함수: 전처리 단계에서 치환되기 때문에 함수 호출 자체가 없어서 제일 빠른 성능을 보여주고 있다.
인라인 함수: inline 요청에 따라 컴파일러가 함수 호출 없이 코드를 치환시켜줌. 하지만 매크로를 못따라감.
일반 함수: 매 호출마다 스택 프레임 생성 후 리턴 과정 때문에 오버헤드가 큰듯..
하지만, 인라인 함수랑 일반 함수랑 별로 큰 차이가 없어 보이는데, 그냥 매크로 함수를 쓰던지, 일반 함수를 쓰는게 더 나아 보인다....