Search

매크로 함수와 인라인 함수

Created
2020/06/19
tag
C
씹어먹는 C 언어
매크로 함수
인라인 함수

1. 매크로 함수

#include <stdio.h> #define square(x) x * x int main() { printf("%d", square(3)); return 0; }
C
복사
매크로 함수는 위 그림에서 나타난 것과 같이 #define을 통해서 정의하게 된다. #으로 나타난 구문들은 컴파일러가 처리하기 이전에 Pre-Processor라고 하는 전처리기가 처리해준다. #define을 통해서 정의한 구문들은 전처리기에 의해 주어진 그대로 치환하게 된다. (비슷한 예로 #include로 작성된 부분은 전처리기에 의해 인자로 준 헤더 파일의 내용을 그대로 가져와 붙여 넣게 된다.)
예를 들면, square(3)이라고 printf 안에 작성한 것은 전처리기에 의해 3 * 3이라는 구문으로 바뀌게 된다. 하지만 이런 매크로 함수는 이용할 때 굉장히 조심해야 한다. 매크로 함수를 처리하는 전처리기컴파일러와 달리 그렇게 똑똑한 친구가 아니기 때문에 단순히 구문을 구문으로 치환할 뿐이다.
#include <stdio.h> #define square(x) x * x int main() { printf("%d", square(3 + 1)); return 0; }
C
복사
위 그림을 보면 왜 전처리기가 그렇게 똑똑한 친구가 아닌지 알 수 있다. 일반적으로 square(3 + 1)에 대해서 예측하면 16이 나올 것 같지만 위 구문은 7이 나오게 된다. 찬찬히 생각해보자. 전처리기매크로 함수를 별다른 작업 없이, 그저 구문에 대한 치환만 일어난다고 했다.
따라서 square(3 + 1)3 + 1 * 3 + 1과 같이 단순히 치환만 되는 것이다. 이렇게 치환된 구문은 컴파일러에 의해서 컴파일 되고, 실행해보면 1 * 3이 먼저 처리되면서 나머지 31을 더하기 때문에 7이라는 값이 나오게 되는 것이다. 위 구문을 정확히 처리하기 위해선 (x) * (x)로 치환하도록 수정하여 정의해야 한다.
그렇다면 모든 매크로 함수를 정의할 때, 제시한 것처럼 괄호로 묶는 것으로 끝일까? 다른 예시를 한 번 살펴보자.
#include <stdio.h> #define expression(x) (x) * 3.0 int main() { printf("%lf", 1.0 / expression(3.0)); return 0; }
C
복사
위 예시는 입력 받은 수에 3.0을 곱하여 역수를 출력하는 것을 의도하여 작성한 매크로 함수이다. 이전에 제시되었던 문제 때문에 인자가 들어가는 부분에 괄호를 쳐준 것을 확인할 수 있다.
입력 값으로 3.0을 넣었다고 가정해보자. 그렇다면 예상되는 결과 값은 1.0 / 9.0가 소수점으로 표현 된다고 생각할 것이다. 하지만 실제 결과는 1.0이 나오게 된다. 이것 역시 단순히 치환이 일어나기 때문에 발생하는 현상이다. 위 구문에 대해서 3.0을 넣게 되면 1.0 / 3.0 * 3.0 이 되므로 1.0이 나오는 것이다.
따라서 의도한 대로 계산이 되기 위해선 아래와 같이 전체 수식을 괄호로 묶어주어야 원하는 대로 결과 값이 나오게 된다.
#include <stdio.h> #define expression(x) ((x) * 3.0) int main() { printf("%lf", 1.0 / expression(3.0)); return 0; }
C
복사
매크로 함수는 편리한 기능을 제공한다. 하지만 원하는 방향으로 결과를 뽑기 위해선 괄호를 여럿 묶어야 하기도 하고, 실수로 괄호를 묶지 않는 경우에는 예기치 못한 오류를 나타낼 수도 있다. 이런 오류들은 에러를 내는 것이 아니기 때문에 굉장히 찾아내기가 까다로울 수도 있다. 따라서 이런 문제들을 해결하기 위해서 전처리기가 아닌, 상대적으로 전처리기보다 더 똑똑한 컴파일러에 의해서 처리되도록 해결책을 내놓게 된다. 바로 인라인 함수를 이용하는 것이다.

2. 인라인 함수

#include <stdio.h> static inline int square(int x) { return x * x; } int main() { printf("%d", square(3 + 1)); return 0; }
C
복사
매크로 함수에서 제곱 연산을 수행했던 것과 마찬가지로 위와 같이 인라인 함수를 통해서 제곱 연산을 수행해보면 어떤 결과가 나올까? 인라인 함수는 매크로 함수처럼 전처리기에 의해서 처리되지 않고 컴파일러에 의해서 처리 되기 때문에 조금 더 똑똑하게 동작한다.
매크로 함수에서 동작했던 것처럼 단순히 구문을 치환만 하는 것이 아니라 연산자 우선 순위를 따지고 이를 계산하여 함수의 인자로 들어가게 된다. 따라서 3 + 1의 결과로 4인라인 함수에 들어가고 return 값으로 16이 나오게 된다.
즉, 인라인 함수는 일반적으로 함수를 정의하여 사용하는 것과 비슷한 방식으로 작동하여 원하는 결과 값을 뽑을 수 있도록 도와준다. 그렇다면 위 인라인 함수는 일반적으로 함수를 정의하여 사용하는 것과 어떤 차이가 있길래 사용하는 것일까?
작성도 일반 함수와 비슷하다. 심지어 인라인 함수는 일반 함수처럼, 입력으로 들어오는 값에 대한 타입 확인 및 return 되는 값에 대한 타입 확인도 가능하다.
함수 호출 방식에 대해서 생각해보면 일반 함수는 함수가 호출 되면, 호출된 함수를 메모리에서 찾아낸다. 호출된 함수를 메모리에서 찾아내면 함수에 인자를 전달하게 되고, 인자를 전달하여 해당 함수를 모두 수행하고 나면 return 값을 함수를 호출한 곳으로 가지고 오게 된다. 하지만 인라인 함수는 이런 왔다갔다 하는 과정 없이, 함수를 호출한 곳에서 해당 함수의 내용을 수행할 수 있도록 해준다. 따라서 인라인 함수를 사용할 수 있다면 비교적 더 빠른 성능을 내게 된다.
그렇다고 무조건 인라인 함수를 쓰는 것이 좋은 성능을 내는 것은 아니다.
인라인 함수를 수행하는 시간보다 함수를 호출하고 결과를 return 받는 시간이 더 짧다면 컴파일러인라인 함수를 사용하는 것을 자체적으로 무시하게 된다.
똑같은 내용을 수행하는데 인라인 함수가 더 오래 걸릴 수도 있는 이유는 인라인 함수가 다양한 타입의 인자를 갖고 있을 때, 이 타입들에 대해서 오버로딩을 해야 하기 때문에 함수를 호출하는 것보다 더 오래 걸릴 수도 있는 것이다. 또한 이런 경우 타입에 함수 호출 구문이 치환되면서 일어나기 때문에 추가적으로 메모리 할당이 필요하고 메모리 측면에서는 조금 비효율적일 수 있다.
결론적으로는 간단한 구문에 대해서 인라인 함수를 사용하는 것이 조금 더 성능적으로 우위일 수 있다.
최근에 사용되는 컴파일러들은 인라인으로 선언하진 않았지만 간단한 함수들이라면 자체적으로 인라인 함수로 만들어 사용하기도 한다.

3. Reference