2017년 6월 5일 월요일

Inline함수 수행시간 절감과 그 호출 빈도에 대하여

앞으로 C++ 코드 최적화에 관련된 실험을 하고 그 결과를 포스팅을 하려고 (노력!)합니다. 포스트를 읽어주신 분들께서 소중한 의견 및 코멘트 주시면 배우는 자세로 토론하겠습니다. :-)

C++ 코드의 실행시간 절감에 대해서 얘기 하면 가장 먼저 언급되는 것이 인라인 함수이다. 인라인 함수는 호출된 위치에 코드를 대체삽입하여 프로그램을 수행한다. 따라서 스택을 통해 해당 메모리 위치로 점프하여 수행되는 일반 함수와는 달리 함수호출 비용을 절약할수 있다. 이러한 절약 효과는 빈번하게 호출되는 함수일 수록 증대된다. 하지만 인라인 함수의 비용절약은 프로그램 사이즈의 증가를 댓가로 한다. 따라서 인라인 함수의 사용은 코드량이 매우 짧은 (1~2줄)  함수에 대해서 선호된다. 이러한 사실에 대한 실증적 분석이 도모네님의 블로그에서 잘 설명되었다.

프로그램 사이즈에 따른 인라인 함수의 효과에 대해서 토론한 글은 많이 있는데, 저자가 찾아본 상으로는 "얼마나 빈번하게 호출되는 함수여야 인라인 함수가 비용절감에 효과를 보이는지"에 대해서 토론한 글은 확인 할 수 없었다. 한번의 함수호출을 가지고는 인라인 함수의 효과를 보기 어렵기 때문에 최소한 어느정도 빈번하게 호출되어야 인라인화 할 가치가 있는지 알아보는 것은 의미가 있다.

본 포스트에서는 간단한 예제를 통해서 인라인 함수의 호출 빈번도와 수행시간의 관계에 대해서 알아본다.

실험방법: 
int형 scalar addition를 일반함수와 inline함수 두가지로 구현하여 함수 호출 횟수를 증가시켜가면서 수행시간을 측정하고 비교한다.
- 수행시간 지표: ctime 헤더의 time_t + clock() 를 이용한 CPU 클럭 시간 측정
- 플랫폼: OSX 10.11.6, Intel Core i5 2.6GHz, Xcode 8.2.1, GoogleTest
- GITHUB: https://github.com/jwkanggist/CppOptVerifier

함수구현:
1) 인라인 함수
1
inline int scalarAdd_Inline(const int in1, const int in2) const {return in1 + in2;}

2) 일반함수
1
int CppOptVerifier::scalarAdd(const int in1, const int in2) {return in1 + in2;}


main함수:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include "gtest/gtest.h"
#include "cppOptVerifier.h"

TEST(inlineFuncTest,RunTimeTest1)
{
    CppOptVerifier* test1 = new CppOptVerifier();
    
    const int in1 = 1;
    const int in2 = 1;
    int out = 0;
    
    long int numCallList[] = {100, 1000,  10000, 1000000, 10000000};
    long numOfFunCall = 0;
    time_t startTime_ms = 0;
    time_t endTTime_ms = 0;
    double runtime_sec= 0.0;
    const int ListLen = 5;
    
    for (int n = 0 ; n < ListLen ; n++)
    {
        numOfFunCall = numCallList[n];
        LOGD("NumOfFunCall = %ld",numOfFunCall);
        
        /* For inline function */
        startTime_ms = clock();
        for (int t = 0 ; t < numOfFunCall; t++)
        {
            out = test1->scalarAdd_Inline(in1, in2);// N times call of inline func.
        }
        endTTime_ms = clock();
        
        runtime_sec = (double)(endTTime_ms - startTime_ms)/CLOCKS_PER_SEC*1000.0;// measure runtime of inline func.
        LOGD("[RunTimeCheck]")
        LOGD("-- Inline function: %f sec",runtime_sec);
        
        /* For normal function */
        startTime_ms = clock();
        for (int t = 0 ; t < numOfFunCall; t++)
        {
            out = test1->scalarAdd(in1, in2);// N times call of normal func.
        }
        endTTime_ms = clock();
        
        runtime_sec = (double)(endTTime_ms - startTime_ms)/CLOCKS_PER_SEC*1000.0;// measure runtime of normal func. 
        LOGD("-- Normal function: %f sec",runtime_sec);
    }
    
    if (test1 != nullptr)
    {
        delete test1;
        test1 = nullptr;
    }
}

int main(int argc, char* argv[])
{
    printf("Successful Running in Google Test\n");
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

실험결과:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Successful Running in Google Test
[----------------Google Test START---------------------]
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from inlineFuncTest
[ RUN      ] inlineFuncTest.RunTimeTest1
MEX_LOG_DEBUG:       NumOfFunCall = 100
MEX_LOG_DEBUG:       [RunTimeCheck]
MEX_LOG_DEBUG:       -- Inline function: 0.003000 sec
MEX_LOG_DEBUG:       -- Normal function: 0.001000 sec
MEX_LOG_DEBUG:       NumOfFunCall = 1000
MEX_LOG_DEBUG:       [RunTimeCheck]
MEX_LOG_DEBUG:       -- Inline function: 0.003000 sec
MEX_LOG_DEBUG:       -- Normal function: 0.006000 sec
MEX_LOG_DEBUG:       NumOfFunCall = 10000
MEX_LOG_DEBUG:       [RunTimeCheck]
MEX_LOG_DEBUG:       -- Inline function: 0.042000 sec
MEX_LOG_DEBUG:       -- Normal function: 0.052000 sec
MEX_LOG_DEBUG:       NumOfFunCall = 1000000
MEX_LOG_DEBUG:       [RunTimeCheck]
MEX_LOG_DEBUG:       -- Inline function: 3.019000 sec
MEX_LOG_DEBUG:       -- Normal function: 3.883000 sec
MEX_LOG_DEBUG:       NumOfFunCall = 10000000
MEX_LOG_DEBUG:       [RunTimeCheck]
MEX_LOG_DEBUG:       -- Inline function: 27.977000 sec
MEX_LOG_DEBUG:       -- Normal function: 30.644000 sec

결론: 함수코드가 매우 짧고 (1~2 line) 매우 빈번하게 (1만번 이상) 호출되는 함수가 아니라면 인라인화에 따른 함수호출비용 절감의 효과는 미미하다. 

고찰: 
- 전통적인 임베디드 시스템에서는 수행시간 보다 프로그램 사이즈가 더 비용이 크기 때문에  코드 크기를 작게하는것에 더 집중해 왔다. 하지만 안드로이드/iOS 모바일 시스템의 경우 배터리 소모가 소비자에게 더욱 민감하게 느껴지기 때문에 전통적인 정책이 계속 유지가 될 것인지는 명확하지 않다. 개인적으로 최근 스마트폰의 하드웨어 개선에 비해서 배터리 효율의 개선은 미미한 상황이기 때문에 앱개발자들이 수행시간 절감에 더 민감해지고 있다고 느끼고 있다. (나 또한 그렇다!)

----------------------------------------------------------------
인라인 함수 사용을 위한 기타  Remarks:
1) 프로그래머가 inline 선언해도 컴파일러가 인라인화를 거부할 수 있다고 한다. 그리고 프로그래머가 inline을 선언 안해도 컴파일러가 스스로 판단해서 효율적인 경우 인라인화를 할 수도 있다고 함!

2) 하지만 강제로 inline화하는 방법은 존재 (나무위키)

3) 인라인함수의 진가는 함수호출 비용을 제거하는 것이 아니고 인라인화 되어 코드가 삽입된 후에 컴파일러에 의한 추가 최적화가 가능해 진다는 것에 있다. 다시말해서 함수화 되어 있는 경우 컴파일러가 main함수와 일반함수를 전역적으로 최적화 할수 없지만 main함수에 인라인화되어 함수코드가 삽입되고 나면 그 후 전역적 최적화가 가능하다.

4) C++의 인라인 함수는 항상 C의 매크로 보다 낮다
- 매크로는 컴파일러 최적화가 불가능하다.
- 매크로는 함수명 충돌과 같은 문제에 취약하다.

5) 클래스 선언안에서 정의된 함수는 함상 인라인화 된다.

참고링크:
- 도모네님 블로그: http://blog.naver.com/PostView.nhn?blogId=netrance&logNo=110141513547 





댓글 없음:

댓글 쓰기