C++

C++ 인라인(Inline) 함수 총정리

수달정보보호 2024. 4. 4. 19:24

1. 인라인 함수의 개념

C++는 함수 호출 오버헤드를 줄이기 위해 인라인 함수를 제공한다. 인라인 함수는 호출될 때 일렬로 확장되는 함수다. 인라인 함수가 호출되면, 인라인 함수 호출 지점에 인라인 함수의 전체 코드가 삽입되거나 치환된다. 이 치환은 컴파일 시 C++ 컴파일러에 의해 수행된다. 인라인 함수는 그 크기가 작으면, 효율성을 높일 수 있다.

 

구문:

inline return-type function-name(parameters)
{
    // function code
}  

 

단, 인라이닝은 컴파일러에 대한 요청일 뿐, 명령어가 아님을 명심하자. 컴파일러는 인라이닝 요청을 무시할 수 있다.

 

컴파일러는 다음과 같은 상황에서는 인라인을 수행할 수 없다.

① 함수에 루프가 포함된 경우(for, while, do while)
② 함수에 정적 변수가 포함된 경우
③ 함수가 재귀적인 경우
④ 함수 반환 유형이 void 이외이고 반환 문이 함수 본문에 없는 경우
⑤ 함수에 스위치가 포함되어 있거나 문으로 이동하는 경우

 

인라인 함수 예시:

#include <iostream>
using namespace std;
inline int cube(int s) { return s * s * s; }
int main()
{
     cout << "The cube of 3 is: " << cube(3) << "\n";
     return 0;
}

 

출력:

The cube of 3 is: 27

2. 인라인 함수를 사용하는 이유

프로그램이 함수 호출 명령을 실행하면, CPU는 함수 호출 후 명령의 메모리 주소를 저장하고 함수의 인수를 스택에 복사하여 최종적으로 지정된 함수에 제어권을 전달한다. 그러면 CPU는 함수 코드를 실행하고 함수 반환 값을 미리 정의된 메모리 위치 또는 레지스터에 저장한 후 제어권을 호출 함수로 반환한다. 함수의 실행 시간이 호출자 함수에서 호출자 함수로의 전환 시간보다 짧으면 이는 오버헤드가 될 수 있다.


규모가 크거나  복잡한 작업을 수행하는 함수의 경우, 함수 호출의 오버헤드는 보통 함수가 실행하는 데 걸리는 시간에 비해 미미하다. 그러나 일반적으로 사용되는 작은 함수의 경우 함수 호출에 필요한 시간이 실제로 함수의 코드를 실행하는 데 필요한 시간보다 훨씬 더 많은 경우가 있다. 작은 함수의 실행 시간이 전환 시간보다 짧기 때문에 작은 함수의 경우, 이러한 오버헤드가 발생한다.

3. 인라인 함수의 장단점

장점

① 함수 호출 오버헤드가 발생하지 않는다.
② 함수가 호출될 때 스택에 있는 푸시 또는 팝 변수의 오버헤드를 저장한다.
③ 기능에서 반환 호출의 오버헤드를 저장한다.
④ 함수를 인라인화할 때, 컴파일러가 함수의 본문에 대해 컨텍스트별 최적화를 수행하도록 할 수 있다. 이러한 최적화는 정상적인 함수 호출에는 가능하지 않다.
⑤ 인라인 함수는 프리앰블과 리턴이라는 함수보다 적은 코드를 생성할 수 있기 때문에 임베디드 시스템에 유용할 수 있다.

 

단점

① 인라인 함수에서 추가된 변수들은 추가 레지스터를 소모한다. 만약 레지스터를 사용할 변수 번호가 증가하면, 인라인 함수 후에 레지스터 변수 자원 사용률에 오버헤드를 발생시킬 수 있다. 이것은 함수 호출 지점에서 인라인 함수 본체를 대체하면 함수가 사용하는 변수의 총 개수도 함께 삽입된다는 것을 의미한다. 따라서 변수에 사용할 레지스터의 개수도 증가할 것이다. 그리하여 함수 후에 변수 번호를 인라인하면, 레지스터 사용률에 오버헤드를 발생시킬 수 있다.
② 인라인 함수를 너무 많이 사용하면, 동일한 코드의 중복으로 인해 이진 실행 파일의 크기가 커진다.
③ 너무 많은 인라인을 사용하면, 명령어 캐시 적중률이 감소하여 캐시 메모리에서 기본 메모리로 명령어 가져오기 속도가 감소할 수 있다.
④ 인라인 함수는 누군가가 인라인 함수 내부의 코드를 변경하면 컴파일 시간 오버헤드가 증가할 수 있으며, 컴파일러가 변경 사항을 반영하기 위해 모든 코드를 다시 한 번 교체해야 하므로 모든 호출 위치를 다시 컴파일해야 한다. 그렇지 않으면 이전 기능으로 계속된다.
⑤ 인라인 기능은 많은 임베디드 시스템에서는 유용하지 않을 수도 있다. 임베디드 시스템에서는 속도보다 코드 크기가 더 중요하기 때문이다.
⑥ 인라인은 실행 파일의 크기를 증가시킬 수 있기 때문에 인라인 함수는 스레싱을 발생시킬 수 있다. 메모리의 스레싱은 컴퓨터의 성능을 저하시킨다.

4. 인라인 함수와 클래스

클래스 내부에 인라인 함수를 정의할 수도 있다. 사실 클래스 내부에 정의된 모든 함수는 암묵적으로 인라인이긴 하다. 따라서 여기에도 인라인 함수의 모든 제한이 적용된다. 클래스에서 인라인 함수를 명시적으로 선언해야 한다면, 그냥 클래스 내부에 함수를 선언하고 인라인 키워드를 사용하여 클래스 외부에 정의하면 된다.

 

구문:

class S
{
public:
    inline int square(int s) // redundant use of inline
    {
        // this function is automatically inline
        // function body
    }
};

 

사실 위의 스타일은 나쁜 프로그래밍 스타일로 간주된다. 가장 좋은 프로그래밍 스타일은 그냥 클래스 내부에 함수의 프로토타입을 작성하고, 함수 정의에 인라인으로 지정하는 것이다. 아래와 같이 말이다.

 

class S
{
public:
    int square(int s); // declare the function
};

inline int S::square(int s) // use inline prefix
{
}

 

예시:

// C++ Program to demonstrate inline functions and classes
#include <iostream>

using namespace std;

class operation {
     int a, b, add, sub, mul;
     float div;

public:
     void get();
     void sum();
     void difference();
     void product();
     void division();
};
inline void operation ::get()
{
     cout << "Enter first value:";
     cin >> a;
     cout << "Enter second value:";
     cin >> b;
}

inline void operation ::sum()
{
     add = a + b;
     cout << "Addition of two numbers: " << a + b << "\n";
}

inline void operation ::difference()
{
     sub = a - b;
     cout << "Difference of two numbers: " << a - b << "\n";
}

inline void operation ::product()
{
     mul = a * b;
     cout << "Product of two numbers: " << a * b << "\n";
}

inline void operation ::division()
{
     div = a / b;
     cout << "Division of two numbers: " << a / b << "\n";
}

int main()
{
     cout << "Program using inline function\n";
     operation s;
     s.get();
     s.sum();
     s.difference();
     s.product();
     s.division();
     return 0;
}

 

출력:

Enter first value: 45
Enter second value: 15
Addition of two numbers: 60
Difference of two numbers: 30
Product of two numbers: 675
Division of two numbers: 3 

 

그런데 여기서 매크로 이야기를 꺼내보려 한다. C에 익숙한 이용자라면, C언어가 매크로를 사용한다는 것을 이미 잘 알고 있을 것이다. 전처리기는 매크로 코드 내에서 직접 모든 매크로 호출을 대체한다. 그런데 매크로 대신에, 항상 인라인 기능을 사용하는 것이 좋다. C++ 매크로의 생성자는 C++에서 거의 필요하지 않으며, 심지어 오류가 발생하기 쉽다. C++에서 매크로를 사용하는 데는 몇 가지 문제가 있다. 매크로는 클래스의 개인 구성원에게 접근할 수 없으며, 매크로는 함수 호출처럼 보이지만 실제로는 그렇지 않다.

 

예시:

// C++ Program to demonstrate working of macro
#include <iostream>
using namespace std;
class S {
     int m;

public:
     // error
#define MAC(S::m)
};

 

출력:

Error: "::" may not appear in macro parameter list
 #define MAC(S::m)

 

C++ 컴파일러는 인라인 함수의 인수 유형을 확인하고, 필요한 변환이 올바르게 수행되는지 확인한다. 그러나 전처리기 매크로는 이것을 할 수 없다. 또 하나는 매크로는 전처리기에서 관리하고, 인라인 함수는 C++ 컴파일러에서 관리한다는 것이다. 우리가 기억해야 할 것은, 클래스 내부에서 정의된 모든 함수가 암묵적으로 인라인이고, C++ 컴파일러는 이러한 함수의 인라인 호출을 수행하는 것이 사실이지만, 함수가 가상인 경우 C++ 컴파일러는 인라인을 수행할 수 없다는 것이다. 그 이유는 컴파일 시간 대신에 런타임에 해결되기 때문이다. 가상은 런타임까지 기다리는 것을 의미하고, 인라인은 컴파일러가 어떤 함수가 호출될지 모르는 경우 인라인을 어떻게 수행할 수 있는지를 의미한다. 추가로, 함수 호출 시 소요되는 시간이 함수 본체의 실행 시간에 비해 많을 경우에만 함수를 인라인으로 만드는 것이 유용하다는 것도 기억해야 한다.

 

인라인 함수가 전혀 효과가 없는 예시;

inline void show() 

    cout << "value of S = " << S << endl; 
}

 

위의 함수는 실행하는 데 생각보다 오랜 시간이 걸린다. 일반적으로 입출력 연산을 수행하는 함수는 상당한 시간을 소모하기 때문에 인라인으로 정의하지 않는 것이 좋다.  show() 함수의 인라인은 I/O 문이 걸리는 시간이 함수 호출의 오버헤드를 훨씬 초과하기 때문에 기술적으로 그 가치가 제한적이라 할 수 있다. 함수를 인라인으로 확장하지 않으면, 컴파일러를 사용하는 컴파일러에 따라 경고가 표시될 수 있다.

자바와 C# 같은 프로그래밍 언어는 인라인 기능을 지원하지 않는다. 하지만 자바에서 컴파일러는 하위 클래스에서 최종 메서드를 재정의할 수 없기 때문에 작은 최종 메서드가 호출되면 인라인을 수행할 수 있으며, 컴파일 시 최종 메서드에 대한 호출이 해결된다.

C#에서 JIT 컴파일러는 작은 함수 호출을 인라인화하여 코드를 최적화할 수도 있다. 마지막으로 염두에 두어야 할 점은 인라인 함수는 C++의 핵심적인 기능이라는 점이다. 물론 인라인 함수를 적절히 사용하면 성능이 향상될 수 있지만, 인라인 함수를 멋대로 사용하면 좋은 결과를 얻을 수 없기는 하지만 말이다. 그러니 모든 함수를 인라인으로 만들지는 말아야 한다. 위에서도 말했지만, 인라인 함수는 가능한 작은 크기로 유지하는 것이 좋다.

728x90