함수 호출과 inline 확장( expansion )
아래의 예문은 main 함수에서 간단한 함수를 호출하는 코드를 보여줍니다.
#include <iostream>
int add( int a, int b){
return a + b;
}
int main(){
int val = add( 3, 5); // 함수 호출
std::cout << val << '\n';
}
그런데, 이 함수 호출에는 함수의 내용을 실행하는 것 외에 들어가는 추가비용이 있습니다.
이러한 비용에는, 함수의 인수 3
과 5
를 함수에 전달하는 과정에 들어가는 복사 비용, 함수의 결과를 main 함수에 다시 되돌려주는데 필요한 전달 비용, 그리고 함수 호출에 필요한 스택( stack ) 처리의 비용 등이 포함됩니다.
즉, 함수의 호출은 공짜가 아니라는 얘기죠.
그렇지만, 일반적으로 이 비용은 함수의 내용을 처리하는데 드는 비용에 비하면 아주 적습니다.
게다가, 기능을 함수 단위로 묶어서 처리하는 것은, 내용을 이해하기 쉽게 만들 뿐만 아니라, 함수의 코드를 재활용하는 등등의 많은 장점이 있습니다.
그래서, 함수를 사용하는 것은, 함수의 내용이 필요한 곳에서 직접 코드를 작성하는 것보다 바람직합니다.
하지만, 위의 add 함수 호출의 경우는 함수 내용을 실행하는데 드는 비용보다, 오히려 위에서 말한 추가 비용이 더 클 수도 있습니다.
그리고, 아래와 같은 반복문이 들어간 코드를 실행하는 경우에는, 이 add 함수를 호출하는 것이 직접 값을 더하는 코드를 main 함수 안에 구현하는 것보다 낫다고 말할 수 없을 것입니다.
#include <iostream>
int add( int a, int b){
return a + b;
}
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
sum += add( i, i+1);
}
std::cout << sum << '\n';
}
▼출력
1874919424
그래서, C++에서는 이러한 상황을 개선하기 위해 inline 확장( inline expansion )이라는 기법을 사용합니다.
이 기법은, 컴파일 시에 위의 add 같은 함수를 호출하는 코드를 함수의 구현 코드로 대체하는 것을 말합니다.
이 inline 확장을 사용해서 위의 main 함수를 바꿔본다면 다음과 같습니다.
( 이러한 소스 코드의 변경은 컴파일 시에 일어나는 일로, 실제로는 눈에 보이지 않습니다. )
#include <iostream>
int add( int a, int b){
return a + b;
}
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
// add( i, i+1) 함수 호출을 아래의 코드로 대체
sum += i + (i+1);
}
std::cout << sum << '\n';
}
위에서 볼 수 있듯이, 이러한 inline 확장을 사용하더라도, 실제로 코드를 작성하는 과정에서는 함수를 사용하는 것이므로, 여전히 함수 사용의 장점( 기능의 모듈화 )을 가질 수 있습니다.
그리고, 더욱 즐거운 점은 이러한 코드를 대체하는 일을 컴파일러가 자동으로 해준다는 것입니다.
프로그래머는 평상시와 마찬가지로 함수와 함수를 호출하는 코드를 작성하기만 하면 됩니다.
inline 함수
과거에 컴파일러의 성능이 지금보다 부족했을 때는, 위와 같은 inline 확장 최적화 기능이 제대로 동작하지 못했습니다.
그래서, 이러한 inline 확장을 위한 힌트가 필요했습니다.
그래서 도입한 것이 바로 inline 키워드입니다.
#include <iostream>
inline int add( int a, int b){ // inline 함수
return a + b;
}
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
sum += add( i, i+1); // inline 함수 호출
}
std::cout << sum << '\n';
}
과거에, 이 inline 키워드를 사용하여 함수( inline 함수라고 부릅니다 )를 작성하면, 컴파일러는 이것을 고려하여 inline 확장을 수행했습니다.
그러나, 현재의 컴파일러의 최적화 기능은 상당한 수준으로 올라섰습니다.
그래서, 요즘의 컴파일러는 inline 함수가 아니라도, 알아서 inline 확장을 하기도 하고, 반대로 inline 함수라도 inline 확장 요구를 경우에 따라 무시할 때도 있습니다. ( inline 확장이 오히려 성능을 떨어뜨리는 경우 )
그럼에도 불구하고, inline 함수는 현재에도 많이 사용됩니다.
다음의 코드는 C++ 표준 라이브러리의 <cstring> 헤더 파일에 들어있는 함수입니다.
inline char* strchr(char* __s, int __n){
// 구현 내용은 생략
}
이 파일에는 이 함수뿐만 아니라, 모든 함수들이 inline 함수로 되어 있습니다.
이렇게 inline 키워드가 계속 쓰이는 것은, inline 함수의 정의가 중복되는 것을 허락하기 때문입니다.
먼저, "main.cpp" 파일에서 외부 파일에 정의된 add 일반 함수를 사용한다고 가정해 보죠.
이 경우엔 다음과 같이 전방 선언( forward declaration )을 통해서 add 함수에 접근해야 됩니다.
왜냐하면, 함수는 기본적으로 외부 링크( extern linkage ) 속성을 갖기 때문입니다.
// main.cpp ----------------------------------------
#include <iostream>
int add( int a, int b); // 전방 선언
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
sum += add( i, i+1);
}
std::cout << sum << '\n';
}
링크( linkage )에 관한 내용은 여기에서 볼 수 있습니다.
[C++] 전역 변수( global variable )의 종류와 링크( linkage )의 개념
전역 변수의 종류C++에서 지역 변수( local variable )는 함수 내부에 정의된 변수를 말합니다.이에 대응하는, 함수 외부에 정의된 변수를 전역 변수( global variable )라고 합니다.// main.cpp#include int g_var;
codingbonfire.tistory.com
그리고, 실제 add 함수의 코드에 접근할 수 있는 것은 컴파일 시가 아니라, 링크 과정에서 가능합니다.
하지만, inline 확장( expansion )은 컴파일 시에 함수 호출 코드가 함수의 내용으로 대체되는 것이므로, 이 add 일반 함수에 대한 호출은 전혀 inline 확장이 되지 않습니다. ( add 함수의 내용을 전방 선언만으로는 알 수 없으므로 )
그래서, 이 add 함수의 정의를 "main.cpp" 파일에 넣는 것으로, 일단 문제는 해결할 수 있습니다.( 그리고, 컴파일러는 가능하면 최적화도 수행할 것입니다. )
그런데, 이렇게 되면, 이 add 함수는 "main.cpp" 파일에서만 사용할 수 있습니다.
그래서, "functions.h" 헤더 파일에 담아, "main.cpp"와 "functions.cpp" 파일에서 사용할 수 있도록 변경해 봅시다.
// functions.h ------------------------------------
int add( int a, int b){ // 일반 함수
return a + b;
}
// functions.cpp ----------------------------------
#include "functions.h"
int compute(){
return add( 123, 456 );
}
// main.cpp ---------------------------------------
#include <iostream>
#include "functions.h"
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
sum += add( i, i+1);
}
std::cout << sum << '\n';
}
그러면, 컴파일은 제대로 되지만, add 함수를 두 파일에서 정의한 것이 되고, 이것은 프로그램 내에 식별자의 정의는 하나만 있어야 된다는 ODR( one definition rule )을 어긴 것이 되기 때문에 링크 오류가 발생합니다.
그래서, C++ 에선 inline 함수는 ODR에 면제라는 규칙을 만들었습니다.
그리고, 중복되는 정의가 발생하지 않도록 하기 위해서, 링커( linker )는 inline 함수의 정의가 하나만 남도록 여분의 정의들을 삭제하는 작업을 수행합니다.
이런 이유로 위에서 inline 함수가 정의의 중복을 허락한다고 말한 것입니다.
그리고, 이러한 정의의 중복이 있어야( 즉, 함수의 전체 코드가 파일 내에 정의되어 있어야 ) inline 확장 최적화를 수행할 수 있습니다.
왜냐하면, 컴파일러는 inline 확장을 하기 위해, 함수의 구현 코드를 컴파일 시에 알고 있어야 하기 때문입니다.
inline 함수의 조건
정리하자면, inline 함수를 제대로 사용하기 위해서 2
개의 조건이 충족되어야 합니다.
● 첫 번째, 모든 코드 파일에 있는 inline 함수의 정의는 동일해야 한다는 것입니다.
inline 함수는 정의의 중복을 허가합니다.
하지만, 링커( linker )가 이 함수의 정의를 하나만 남겨야 하기 때문에, 함수가 정의가 일치하지 않으면 정의되지 않는 행동( undefined behavior )을 초래하게 됩니다.
● 두 번째, inline 함수가 inline 확장이 되기 위해서 ( 함수의 내용을 알아야 되므로 ), 이를 사용하는 파일에 함수의 전체 코드가 정의되어야 한다는 것입니다.
그래서, inline 함수를 전방 선언( forward declaration )하는 것으로는 불충분합니다.
이 두 가지 조건을 쉽게 충족시키는 방법은, 적당한 헤더 파일에 inline 함수를 정의하고, 이 함수가 사용되는 모든 코드 파일에서, 이 헤더 파일을 포함( #include
)시키는 것입니다.
// functions.h ------------------------------------
inline int add( int a, int b){ // inline 함수
return a + b;
}
// functions.cpp ----------------------------------
#include "functions.h"
int compute(){
return add( 123, 456 );
}
// main.cpp ---------------------------------------
#include <iostream>
#include "functions.h"
int main(){
int sum = 0;
for( int i = 0; i < 100000000; i++){
sum += add( i, i+1);
}
std::cout << sum << '\n';
}
이렇게 헤더 파일을 사용함으로써, inline 함수를 이 함수를 사용하는 파일에 쉽게 전파시킬 수 있습니다.
그리고, 여기서 주목할 만한 것은, 위의 add 함수의 정의가 전체 프로그램 내에 하나만 존재한다는 것입니다.
그럼에도 불구하고 모든 파일에서 전방 선언의 사용 없이 함수에 접근할 수 있고, 가능하다면 컴파일러는 inline 확장까지 수행할 것입니다.
이것이 과거에서부터 지금까지 inline 함수를 사용해 오는 이유라고 할 수 있습니다.
inline 함수의 단점
inline 함수는 최적화( inline 확장을 통해서 )될 수 있다는 장점이 있습니다.
하지만, 이를 달성하기 위한 조건 때문에 구조적인 단점도 가지고 있어야 합니다.
● 첫 번째, inline 함수는 이 함수를 포함하는 모든 파일에 정의되어야 하기 때문에, 그 수만큼의 함수를 컴파일해야 합니다.
따라서, 컴파일 시간이 대폭 늘어납니다.
만약, inline 함수를 사용하는 파일이 10
개 있다면, 이 함수를 10
번 컴파일해야 합니다.
하지만, 일반 함수는 하나의 파일에 정의를 두고, 전방 선언( forward declaration )을 통해서 이 함수에 접근한다면, 단 한 번만 컴파일하면 됩니다.
● 두 번째, 위와 마찬가지 이유로, inline 함수의 코드를 수정해야 하는 경우, 이 함수를 포함하는 모든 파일들을 모두 다시 컴파일해야 합니다.
그리고, 만약 다시 컴파일해야 하는 파일들을 또 다른 파일이 포함하고 있다면, 그 파일까지도 모두 다시 컴파일해야 합니다.
전에도 말했지만, 컴파일하는데 몇 시간 걸리는 경우도 있습니다.
따라서, inline 함수로서 이득을 볼 수 있는 경우( 예를 들면, 간단하면서 반복적으로 호출되는 함수 또는 헤더 파일 위주의 라이브러리 )가 아니면 일반 함수로 정의하는 것이 더 나은 경우가 대부분입니다.
정리
- inline 확장( expansion )은 함수를 호출하는 코드를, 함수의 구현 코드로 대체하는 컴파일러의 최적화( optimization ) 기법입니다.
- inline 함수를 사용하기 위해선, 사용하고자 하는 파일에서 그 함수의 전체 코드를 포함해야 합니다.
- inline 함수는 inline 확장을 통해서 최적화될 수 있습니다.
이 글과 관련 있는 글들