본문 바로가기
C++ 200제/코딩 IT 정보

[C++ 최적화] Const, 가상함수, 변수 등으로 속도 최적화

by vicddory 2017. 12. 1.

[C++ 최적화] Const, 가상함수, 변수 등으로 속도 최적화


C++ 최적화 : 속도 최적화(Speed Optimizations)

모든 애플리케이션은 CPU 사이클이 시간 때문에 중요하다. 이 포스팅에선 C++ 최적화 중 속도를 위한 몇 가지 지침을 제시한다.


C++ 변수 속도 최적화


C++ 최적화 : 긴 인수 리스트를 압축하기 위해 클래스 사용

함수의 인수 리스트가 길 때, 함수 호출 오버헤드가 증가한다. 런타임 시스템은 스택을 인수들의 값으로 초기화해야 한다. 당연히 이 동작은 인수가 많을수록 오래 걸려 C++ 최적화에 방해된다.


예를 들어, 아래 함수를 100,000,000번 수행하면 내 컴퓨터에서는 평균 8.5초가 소요된다.


1
2
3
4
5
6
void retrieve(const string& title, //5 arguments 
              const string& author, 
              int ISBN,  
              int year, 
              bool&  inStore)
{} 
cs


인수 리스트를 단일 클래스로 압축하고 그것을 참조로 전달하면 평균 5초 걸린다.

물론 실행하는데 오랜 시간이 걸리는 함수들을 위하여 스택 초기화 오버헤드는 무시할만 하다. 그러나 빈번하게 호출되는 짧고 빠른 함수들을 위하여 긴 인수 리스트를 단일 객체로 압축하고 그것을 참조에 의해 전달하는 것은 성능을 향상시킬 수 있다.


C++ 최적화 : 레지스터 변수들(Register Variables)

저장장소 specifier register는 객체가 프로그램에서 다량으로 사용될 것이라고 컴파일러에게 힌트를 주기 위해 사용될 수 있다.


예를 들면, 루프 카운터들은 레지스터 변수들로 선언될 수 있는 좋은 후보들이다.


1
2
3
4
5
6
7
8
9
10
11
12
void f()
{
    int *= new int[3000000];
    register int *p2 = p; //store the address in a register
 
    for (register int j = 0; j<3000000; j++)
    {
        *p2++ = 0;
    }
    //...use  p    
    delete [] p;
}
cs


레지스터 변수들이 레지스터에 저장되지 않으면, 루프 실행시간의 상당한 양이 메모리로부터 변수를 패한다. 새로운 값을 레지스터 변수에 할당하고 다시 메모리에 저장하는 일을 반복함으로써 시간을 낭비해 C++ 최적화를 방해한다.


C++ 가상함수


레지스터 변수는 머신의 레지스터에 저장해야 오버헤드를 줄일 수 있다. 그러나 해당 레지스터는 오직 컴파일러에 대한 권고사항이라는 것을 기억하라.


함수 인라인화에서처럼 컴파일러는 객체를 머신 레지스터에 저장하는 것을 거부할 수 있다. 더군다나, 현대의 컴파일러들은 루프 카운터들을 통해 C++ 최적화하고 그것들을 어떤 식으로든 머신의 레지스터들로 옮긴다. (레지스터 저장 장소 명세는 기본형들로 제한되지 않는다. 모든 유형의 데이터를 담을 수 있다.)


만일 객체가 너무 커서 레지스터에 적합하지 않다면, 컴파일러는 캐시 메모리와 같은 더 빠른 메모리 영역에 객체를 저장한다. (캐시 메모리는 메인 메모리보다 약 10배 정도 더 빠르다) 레지스터 저장장소 specifier를 가지고 함수 파라미터들을 선언하는 것은 인수들을 스택상으로 보내는 것 보다 머신의 레지스터들 상으로 보내기 위한 권고이다.


예를 들면,


1
void f(register int j, register Date d);
cs


C++ 최적화를 위해 이렇게 사용할 수 있다.

C++ 최적화 : 상수객체들을 const로서 선언하기(Declaring Constant Objects as const)

컴파일러는 const를 일반적인 메모리가 아닌 머신 레지스터에 저장한다. 당연히 수행 속도 향상에 도움이 된다.


C++ 최적화 : 가상 함수들의 실행시간 오버헤드(Runtime Overhead of Virtual Functions)

가상 함수가 객체의 포인터나 참조를 통해 호출될 때 호출은 반드시 추가적인 실행시간 페널티를 부과하지 않는다. 만일 컴파일러가 호출을 정적으로 분해할 수 있다면 추가적인 오버헤드도 없다. 더구나 매우 짧은 가상 함수는 이러한 경우에 인라인화될 수 있다.


다음의 예는 가상 멤버함수들의 호출을 정적으로 분해할 수 있다:


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
#include <iostream>
 
using namespace std;
 
class V 
{
public:  
    virtual void show() const { cout<<"I'm V"<<endl; }
};
 
class W : public V 
{
public:
    void show() const { cout<<"I'm W"<<endl; }
};
 
void f(V & v, V *pV) 
{
    v.show();   
    pV->show();  
}
 
void g()
{
    V v;
    f(v, &v);
}
 
int main()
{
    g();
    return 0;
}
cs


만일 전체 프로그램이 단일 단위속에 나타난다면 컴파일러는 main()에서의 함수 g()의 호출의 인라인 치환을 수행해 C++ 최적화에 도움이 된다.


g()내부에서 f()의 invocation 또한 인라인화될 수 있으며, 그렇기에 f()로 전달되는 인수들의 동적 타입들이 컴파일 시간에 알려진다. 컴파일러는 f()내부에서의 가상 함수 호출들을 정적으로 분해할 수 있다. (모든 컴파일러들이 실제로 모든 함수 호출들을 인라인화한다는 보장은 없다)


그러나, 어떤 컴파일러들은 확실히 f() 인수들의 동적타입이 컴파일 시간에 결정될 수 있다. 그래서 이러한 경우에 동적 바인딩의 오버헤드를 피할 수 있다는 이점을 취한다.


[C++최적화] Const, 가상함수, 변수 등으로 속도 최적화


C++ 최적화 : 함수 객체들 대 함수 포인터들(Function Objects Versus Function Pointers)

함수 포인터들 대신에 함수 객체들을 사용하는 이점들은 genericity와 더 쉬운 유지로 제한되지 않는다. 더구나 컴파일러들은 함수 객체의 호출을 인라인화할 수 있으며 그것에 의해서 C++ 최적화하여 성능을 그 이상으로 향상시킬 수 있다(함수 포인터 호출을 인라인화하는 것은 거의 불가능하다).


C++ ConstC++ Const


오버로드된 postfix ++은 두 가지 이유에서 prefix보다 훨씬 덜 효율적이다. 그것은 임시적인 사본의 생성을 요구하며, 그 사본을 값에 의해 반환한다. 그러므로, 당신이 객체의 postfix 및 prefix 연산자들 사이에서의 선택이 자유로울 때 마다 prefix 버전을 선택하라.


[C++ 최적화] Const, 가상함수, 변수 등으로 속도 최적화

댓글