IT_Programming/C · C++

[펌] C/C++ 퍼포먼스와 footprint

JJun ™ 2009. 6. 25. 13:02

출처: http://flychk.new21.org/zboard/view.php?id=pdataroom&no=4

 

/********************************************************************************/

/*                                본 자료는 한국 게임 개발자 협회 사이트 프로그래밍 강좌란에서  */

/*                                최흥배님의 동의를 얻고  퍼온 자료 입니다.                          */
/*                                                                                                             */

/*                                                           이름: 최흥배 (jacking@korea.com)      */
/*                                                           홈페이지: http://www.digitalsajin.com */
/*********************************************************************************/



C/C++의 최적화 및 FootPrint(공간)에 대해서 매 주의 주말마다 글을 올릴려고합니다.
이 글은 BGDA쪽과 같이 올리고 있습니다.. (부산분들은 자주 들려주세요 ^^;;)
제가 최적화와 FottPrint에 대해서 올리는 글은 "C++ Footprint and Performance Optimization"
라는 책을 보고 제가 정리한 것입니다.
정리라 아주 자세히는 적지 않으니 글을 보고 의문점이 있으면 질문해주세요…
제가 아는한 답변 드리겠습니다. -_-;;;

 

 


1. 곱셈과 shift 연산에 대하여

예)
#include <iostream>
#include "booktools.h" // 이건 시간 측정을 위해 만든 라이브러리 임

long MulOperator()
{
        long I, j =1031;
        for(I=0;I<20000000;I++)
        {
            j*=2;
        }
        return 0;
}


long Mulshift()
{
        long I, j =1031;
        for(I=0;I<20000000;I++)
        {
            j <<=1;
        }
        return 0;
}

위의 코드를 보면 아시는 분은 아시겠지만 원래 곱하기 연산 보다는 Shift 연산이 빠르다는
말이 있어 일반적으로 최적화를 할 때 곱하기를 Shift 연산으로 바꾸는 경우가 많죠..
(저도 학원과 책에서 그렇게 배웄죠..) 그러나 x86(인텔 CPU) 호환 프로세스에서
Microsoft Developer Studio를 사용했어 테스트를 실행하면, 곱하기나 Shift 연산이나
속도가 같습니다. 그 이유는 위 코드를 어셈블리 코드로 보면 컴파일러가 컴파일시 곱하기
연산으로 된것도 Shift 연산으로 코드를 바꾸어 주기 때문입니다.
그러니 코드 분석이 간편한 곱하기 연산으로 하는 것이 훨씬 좋습니다.
그리고 MIPS 에서 곱하기/Shift 샘플을 실행 하면 이 프로세스에 한해서는 곱하기 쪽이
Shift보다 훨씬 빠릅니다. 이 때문에 이와 같은 최적화를 행할 때에는 대상과 컴파일러의
동작을 알 필요가 있습니다.




2. 캐쉬 미스와 페이지 폴트
캐쉬와 페이지 폴트에 관한 건 컴 관련 책에 나와 있으니 일단 넘어 가겠습니다.
(사실은 제가 적기 귀찮았어요-_-;;) 원하시면 다음에 적어 올리죠…

O 데이터 캐쉬 미스 와 페이지 폴트 최소화 방법
ㄱ. 데이터를 사용 빈도에 따라 그룹화 한다.
ㄴ. 시간과 공간의 관계에 따라 데이터를 그룹화 한다
ㄷ. 데이터를 시퀜스(순차적)으로 Access, 저장한다
ㄹ. 사용 빈도가 높은 데이터를 Look 한다.
ㅁ. 가능한 작은 Working Set을 사용한다.

O 명령 캐쉬 미스와 페이지 폴트 최소화 방법
ㄱ. 명령을 사용빈도에 따라 그룹화 한다.
ㄴ. 관련 있는 명령어를 그룹화 한다.
ㄷ. 사용빈도 높은 명령어를 look 한다.




3. 변수의 퍼포먼스

long Float()
{
        long I;
        float j=10.31, k=3.0;

        for(I=0;I<10000000;I++)
        {
                j *= k
        }
        return j;
}

long Double
{
        long i
        Double j=10.31, k=3.0;

        for(I=0;I<10000000;I++)
        {
                j *= k
        }
        return j;
}

long Int()
{
        long I;
        int j=1031, k=3;

        for(I=0;I<10000000;I++)
        {
                j *= k
        }
        return j;
}

위의 코드를 실행하면 당근 float와 double에서는 승산(곱하기) 속도는 거의 비슷하지만,
int를 사용하면 몇 배라도 속도가 빠라진다.

일반적으로 int가 가장 속도가 빠르지만, 나눗셈만큼은 다르다. 이것은 나눗셈중에 정수가
몇번이라도 변환되기 때문이다. 여기에서 float랑 double를 int로 바꿀지 어떨까를 판단은
각 산술변수를 몇회 할것인지를 생각할 필요가 있다.  주로 승산,가산,감산에서는 float 형
변수를 사용한 애플리케이션은 int형으로 바꾸는 쪽이 좋다.





4. 변수와 오브젝트의 Scope

O for문에서…
예 1)
void Scope1()
{
        for(int a1=0; a1 < 10; a1++)
        {
                char arr[] = "IAP Team Best";
                int b2 = (int)arr[a1];
        }
}

예 2)
void Scope2(0
{
        int a1, a2;
        char arr[] = "IAP Team Best";

        for(a1=0; a1 < 10; a1++)
        {
                b1 = (int)arr[a1];
        }
}

위의 예제 2개를 보면 예제 1의 틀린점을 쉽게 알수 있을겁니다.
예제 1은 for문안에서 변수를 선언하고 할당을 함으로써 예제 1에 비해 엄청나게
퍼포먼스가 증가해버리죠…
그러나 코딩을 하다 보면 가끔 이런 어처구니 없는 실수를 저질러는 수가 있죠.



O 초기화
예 3)
void IncressAllCounters(DB *baseOne)
{
        for(int i=0; i < baseOne->GetSize(); i++)
        {
                DBElem * pElem = baseOne->NextElem();
        ~

예 4)
void IncressAllCounters(DB * baseOne)
{
        int bSize = baseOne->GetSize();

        for(int i=0; i < bSize; i++)
        {
                DbElem * pElem = baseOne->NextElem();
                ~
일반적으로 프로그래밍을 할 때 위의 예3 처럼 코딩 하시는 분들이 꽤 있을겁니다.
(저도 그렇고요 -_-;;;) 예3는 예4에 비해 퍼포먼스면에서 많은 오버헤드가 있습니다.
예3는 for문이 돌아 갈때마다 매번 baseone->GetSize 함수를 사용하고 예4는 for문
전에 이미 for문을 얼마큼 돌리것인가를 정하고 for문에서는 for문의 한계를 또 계산해
낼 필요가 없죠. 위의 코드를 보면 그 차이를 쉽게 알수 있는데 프로그래머들이 그냥
지나가 버려 퍼포먼스 측면에서 많은 손해를 보시 쉽죠.



O 멤버 변수의 사용
class DB
{
        ~
        int dbSize;
        int i;
        ~
};

예 5)
void DB:: IncressAllCounter(int addedValue)
{
        for(i=0 ; i < dbSize; i++)
        {
                ~

예 6)
void Db::IncressAllCounters(int addedValue)
{
        int iSize = dbSize;
        ~
        for(i=0 i < iSize; i++)
        {
                ~


위 예 5,6의 퍼포먼스의 차이를 아시는 분 ???
자도 오늘 이렇게 2개의 예를 보기 전까지는 그 차이를 생각해 본적이 없었는데
위의 예5의 경우 멤버 변수에 접근하는 것은 로컬의 변수에 접근하는 것보다 2배의
시간이 걸립니다. 그 이유는 멥버 변수의 기본 어드레스를 얻기 위해서, 내부적으로
this 사용 해야 되기 때문입니다.  저도 이부분에 대해서 이 때까지 코딩하면서
걍 사용했는데 이 사실을 아니 뜨끔하더군요…이런것 생각 못했다니 -_-;;;






4. 기본형의 그룹화

그룹화(구조체)화한 형의 작성 방법은 그 형의 footprint랑 퍼포먼스에 영향을 줍니다.

O 구조체
예 7)
#include <iostream>
struct A { char a; long b; char c; long d; };
struct B { char a; char c; long b; long d; };

#pragma pack(push, 1)
struct C { char a; long c; long b; long d; };
#pragma pack(pop)

void main()
{
        cout << "Size of A: " << sizeof(A) << "butes." << endl;
        cout << "Size of B " << sizeof(B) << "butes." << endl;
        cout << "Size of C: " << sizeof(C) << "butes." << endl;
}

위 코드를 실행하면
        Size of A: 16bytes.
        Size of B: 12bytes.
        Size of C: 10bytes.
구조체의 크기가 같지 않은 것은 얼라이먼트(순서)와 관계가 있다.
보통 컴파일러는 롱 워드는 다음의 롱 워드의 경계로 배치한다. 이를테면 새로운
롱 워드는 4메모리 어드레스 밖에 지정 할수 없다.

* 구조체 A의 메모리
어드레스        내용
00        char a
01        스태핑
02        스태핑
03        스태핑
04        long b, byte 0
05        long b, byte 1
06        long b, byte 2
07        long b, byte 3

* 구조체 B의 메모리
00        char a
01        char b
02        스태핑
03        스태핑
04        long b, byte 0
05        long b, byte 1
06        long b, byte 2
07        long b, byte 3

구조체 B의 사이즈가 작은것은 long b는 구조체 선두부터 같은 상대 어드레스로 시작하기
때문에 스태핑의 일부가 char b에 덮여 쓰여졌기 때문이다.
이와 같이 구조체를 설계 할 때에는 변수의 순서를 생각할 필요가 있다.

다음으로 가장 성적이 좋은 구조체 C를 보면 이건 10바이트로 되어 있다. 왜 그럴까..
그것은 #prggame pack 라는 컴파일러 커맨드를 사용했기 때문이다.
이 커맨드를 사용하면 메모리는 다음과 같이 된다.

* 구조체 C의 메모리
00        char a
01        long b, byte 0
02        long b, byte 1
03        long b, byte 2
04        long b, byte 3
05        char c
06        long d, byte 0
07        long d, byte 1
08        long d, byte 2
09        long d, byte 3

push와 pop을 사용하여 현재의 얼라이먼트를 컴파일러 스택에 push하고 강제적으로
새로운 얼라이먼트를 적용하여 뒤에 원래대로 얼라이먼트를 pop하여 원래대로 되돌린다.


O 비트 필드
비트 필드는 작은 공간을 차지하고 빠른 접근을 보장하기 때문에 아주 활용의 용도가 크다.
그러나 사용에는 주의가 필요하다.

예 8)
struct BitField
{
        unsigned rangAOne        :11; // long 1
        unsigned rangATWO       :11;
        unsigned rangBOne        :10;
        unsigned rangAThree      :11; // long 2
        unsigned rangAFour        :11;
        unsigned rangBTwo        :10;
};

예 9)
struct WastingBitField
{
        unsigned rangAOne        :11; // long 1
        unsigned rangATWO       :11;
        unsigned rangBOne        :11; // long 2
        unsigned rangAThree      :11;
        unsigned rangAFour        :10;
        unsigned rangBTwo        :10;
};

예8은 크기가 8바이트 이지만 예9는 크기가 12바이트가 나온다. 그 이유는 비트필드는 롱 워드로 저장되는데
예9의 경우 ranfBOne에서 32비트를 넘어 long 1 에는 22트만 저장되고 10비트는 공백으로 되고
바로 새로운 long 2에 11비트가 저장되기 때문이다.


O 퍼포먼스
비트 필드를 설계 할 때에는 조금 생각하면 공간을 활용 할 수 있고, 변수가 가장 가까운 얼라이먼트 경계로

배치 되어 컴파일러가 비교적 빠르게 코드를 생성한다.

비트 필드에 초기화나 계산에 변수를 사용하면 오버헤드가 아주 크게 되므로 주의 해야된다.

예 10)
struct FaultyBitfield
{
        unsigned a, b :4;
}

위와 같은 잘못된 비트 필드 사용을 주의 해야된다.
위 코드는 32비트의 비트 필드 a와 4비트의 비트 필드 b를 생성하고 있기 때문이다.

비트 필드 구조체는 롱 워드(32비트)를 기본단위로 하는것에 주의 해야 된다.
비트 필드의 어드레스를 사용하면 비트 필드로 참조를 초기화 하는것은 불가능하다.

 

 

 

5. 기본적인 프로그래밍 文

O 선택
if ~ else문
if문에서 식을 결합할 때는 특히 중요하게 생각할 2가지가 있다
* 식은 왼쪽에서 오른쪽으로 식을 평가한다.
* 결과가 결정된다면, 그 이후의 식은 평가되지 않는다.

이것이 중요한 이유는 일부의 식이 평가 되지 않는 것에 의해 시간을 절약 할수 있다.
예를들어, 논리 AND에서 식을 결합하면, 처음에 false가 나오면 그 식에서 평가는 중
지 되어 버린다.

예)
char a[] = "this is a long string of width we want to know the length";
int b = 0;

//  비효율적인 AND
if ( ( strlen(a) > 100 ) && (b > 100) )
{
d++;
}

//   효율적인 AND
if ( (b > 100) && ( strlen(a) > 100 ) )
{
d++;
}

위의 예에서 식 (b> 100)이 틀린 경우, 2번째의 if쪽이 첫번째의 if문보다 거의 200배는 빠르다.

OR문도 비슷하다.

char a[] = "this is a long string of width we want to know the length";
int b = 101;

//  비효율적인 OR
if ( ( strlen(a) > 100 ) || (b > 100) )
{
d++;
}

//  효율적인 OR
if ( (b > 100) || ( strlen(a) > 100 ) )
{
d++;
}

OR문은 처음의 식이 TRUE이면 그 다음 식은 평가 하지 않으므로 2번째가 첫번째 보다
200배 가까이 빠르다.

그리고 또 하나 실수하기 쉬운 다음과 같은 식이 있다.

for(int i=0; i< MAX; i++)
  if( (i >= BufferSize) || (EOF == (c = GetNextChar() ) ) )

이 if의 2번째 식은 입력으로 새로운 문자를 얻고, 그 입력의 최후에 도달 했는지
어떻는지 검사 할려는 것이다. 그러나 왼쪼게에 다른 식이 있기 때문에 루프가 반복
될 때마다 GetNextCahr()이 호출되는 것은 끝이 없다.



O if에 의한 선택
if ~ else를 여러번 사용하여 식을 평가 할때 선택은 당연히 가장 위에 있는 식이 가장
빠르고 밑에 있을으록 속도가 느려진다. 그렇기 때문에 if문에서 가장 선택될 확율이
높은 식을 제일 위에 놓는다.

예)

if ( a== 1)
{
Do1();     // 가장 빨리 도달하는 코드
}
else if( a == 2)
{
Do2();  // 아직 충분히 빠르다
}
~
~
else if( a == 1000)
{
Do1000(); // 도달하는데 꽤 시간이 걸린다.
}
~
~
else
{
DpDefault(); // 가장 도달하는데 시간이 많이 걸린다.
}



O 점프 테이블
선택 조건의 범위를 연속하는 수치의 범위(0,1,2,3,4 등)으로 변환 할 수 있는 경우에는
점프 테이블을 사용 할 수 있다. 점프 테이블의 사고는, 함수 포인트를 테이블에 삽입해
셀렉터를 그 테이블의 인덱스로써 사용하는 것이다.  다음의 샘플 코드는, 이 앞의
if...else를 사용한 거의 비슷한 처리를 할수 있는 점프 테이블을 구현한다.

// 테이블 정의 :
typedef long (*functs)(char c);
functs JumpTable[] = { DoOne, DoTwo, DoThree .... };

// 테이블 사용 코드 :
long result = JumpTanle[selector](i++);

위 예의 처음 2행의 코드는 점프 테이블을 정의 하고 있다. typedef는 점프 테이블에 포함
될 함수의 배치를 결정한다. 이 경우, 입력으로써 1문자를 받고, 결과로써 long응 반환
하는 함수이다. JumpTable의 배열정의는, 테이블 내의 함수 포인터를 배치하고 있다.
DoOne(), DoTwo(), DoThree()의 각 함수는 다른 장소에서 정의 할 필요가 있고,
입력 함수로써 한 문자를 받고, long을 돌려줘야 된다는 것을 주의 해야 한다.
코드 마지막 행은 점프 테이블의 사용을 나타내고 있다. 변수 i는 변수 selector에 의해
선택된는 함수의 입력이다. 변수 result는 선택 된 함수가 돌려주는 long을 받는다.
i에 문자 a가 저장되고, selector의 값이 2인 경우, 실제로 다음의 함수 호출이 일어난다.

long result = DoThree('a');

점프 테이블의 구현은 어느 선택기(테이블 엔트리)에 대해서도 같은 속도이다.
결점은 선택 범위가 연속해야만 하고, 디폴트를 수동으로 코드화 해야된다.



O switch 문
switch문은 소스 코드 중에서 어떻게 사용되었는가에 의해 컴파일러는 다음의 몇가지
방법에서 switch문을 해석한다.

점프 테이블로써의 switch
case가 완전하게 연속하고 있는 switch는, 점프 테이블로써 구현 되어진다.
예를 들어 다음과 같은 경우 선택식의 값을 점프 테이블의 인덱스로써 사용 할수
있다

switch (a)
{
  case 0 :
  case 1 :
  case 2 :
  case 3 :
  case 4 :
  case 5 :
  default
}

case는 함수가 아닌 분기로써 구현된다. 함수 포인터를 구현하는 것 보다 분기를 구현하는쪽이
마이크로프로세스의 오버헤드가 아주 낮기 때문이다. 이것은 분지중은 컨텍스트를 스택에
push 하고 뒤에 스택으로부터 pop 할 필요가 없기 때문이다.
switch case 쪽이 점프 테이블 보다 오버 헤드가 작다는건 이해하고 있는것이 좋다.
그렇지만 선택된 case에서 함수를 호출한다면 처음에 case로 점프하고 다음으로 함수를
호출 할 필요가 있기 때문에 오히러 오버헤드가 함수 포인터를 사용한 점프 테이블보다
더 증가 한다.


() if..else로써의 switch
switch (a)
{
  case 55 :
  case 300 :
  case 6 :
  case 12 :
  case 79 :
  case 43 :
  default
}

이와 같은 case의 set은 컴파일러는 어느 case를 실행 해야되는가를 정확하게 결정하기
위해 몇까지의 if..else문을 사용할 필요가 있다. 이와 같은 것은 어느 정도의 오버헤드
는 있지만 좋은 컴파일러는 필요한 비교의 횟수를 최소한 하도록 if..else값을 그룹화
한다. 처음의 비교에 따라 선택식이 처음의 csae(55)보다 크다고 알려지면, case 6, 12, 3
과 선택식을 비교할 필요가 없다는것은 말 할 필요가 없다. if..else구조는, 불 필요한
비교를 실행하지 않도록 그룹화 하는 식이 사용된다.

보통 switch에서는 가장 빨리 선택되는 것이 default 이지만 switch가 if..else 구조로
된 경우는 그렇지 않다.

결론 : 모든 case가 연속된 범위로 되어 있는 case문을 설계하는 아주 좋다.
       또한 가능하면 가장 실행 횟수가 많은 선택기를 default에서 사용한면 좋다.


  * 선택 실행의 비교
(1) 함수 호출의 오버헤드가 없는 경우의 비교

예)
  
#include <iostream.h>
#include <ctime>

#include "BookTools.h"

#define N 50000000


int j =1;

long do_switch()
{
  long i=0, k=0;

  for (i = 0; i < N; i++)
  {
      switch(j)
      {
         case 1: k+=1;
                 break;
         case 2: k+=3;
                 break;
         case 3: k+=5;
                 break;
         case 4: k+=7;
                 break;
         case 5: k+=11;
                 break;
         case 6: k+=13;
                 break;
         case 7: k+=17;
                 break;
         case 8: k+=19;
                 break;
         case 9: k+=23;
                 break;
         default:k+=27;
                 break;
      }
  }
//  cout << "SWITCH K= " << k << endl;
  return(k);
}

long do_ifelse()
{
  long i=0, k=0;

  for (i = 0; i < N; i++)
  {
      if (j==1)
         k+=1;
      else if (j == 2)
         k+=3;
      else if (j == 3)
         k+=5;
      else if (j == 4)
         k+=7;
      else if (j == 5)
         k+=11;
      else if (j == 6)
         k+=13;
      else if (j == 7)
         k+=17;
      else if (j == 8)
         k+=19;
      else if (j == 9)
         k+=23;
      else
         k+=27;
  }
//  cout << "IF ELSE K= " << k << endl;
  return(k);
}

int main()
{
cout << "Selected   Switch     If Else\n";
cout << "-----------------------------\n";
for (j = 1; j < 10; j++)
{
   cout << j << "          "<< time_fn(do_switch);
   cout << "       " << time_fn(do_ifelse) << endl;
}
j=10;
cout << "Default     "<< time_fn(do_switch);
cout << "       " << time_fn(do_ifelse) << endl;
return(0);
}


테스트 결과

선택된 사례 switch의 결과  if..else의 결과
1  1400   700
2  1400   1050
3  1420   1400
4  1410   1760
5  1390   2110
6  1420   2460
7  1400   2810
8  1410   3170
9  1400   3520
default  890   3480

결론 : 가능하면 switch의 default에 가장 잦 사용하는 코드를 적어야 될 것이다.
         if..else의 경우 가장 자주 사용하는 것은 구조 중 처음에 배치해야 된다.
         switch의 앞에 if..else문을 1~2개 사용하는 경우가 가장 좋다.

2) 함수 호출의 오버헤드가 있는 경우의 비교

예)
#include <iostream.h>
#include <ctime>

#include "BookTools.h"


#define N 50000000


int j =1;

long aa()
{
return(1);
}
long bb()
{
return(3);
}
long cc()
{
return(5);
}
long dd()
{
return(7);
}
long ee()
{
return(11);
}
long ff()
{
return(13);
}
long gg()
{
return(17);
}
long hh()
{
return(19);
}
long ii()
{
return(23);
}
long jj()
{
return(27);
}

long do_switch()
{
  long i=0, k=0;

  for (i = 0; i < N; i++)
  {
      switch(j)
      {
         case 1: k+=aa();
                 break;
         case 2: k+=bb();
                 break;
         case 3: k+=cc();
                 break;
         case 4: k+=dd();
                 break;
         case 5: k+=ee();
                 break;
         case 6: k+=ff();
                 break;
         case 7: k+=gg();
                 break;
         case 8: k+=hh();
                 break;
         case 9: k+=ii();
                 break;
         default:k+=jj();
                 break;
      }
  }
//  cout << "SWITCH K= " << k << endl;
  return(k);
}

long do_ifelse()
{
  long i=0, k=0;

  for (i = 0; i < N; i++)
  {
      if (j==1)
         k+=aa();
      else if (j == 2)
         k+=bb();
      else if (j == 3)
         k+=cc();
      else if (j == 4)
         k+=dd();
      else if (j == 5)
         k+=ee();
      else if (j == 6)
         k+=ff();
      else if (j == 7)
         k+=gg();
      else if (j == 8)
         k+=hh();
      else if (j == 9)
         k+=ii();
      else
         k+=jj();
  }
//  cout << "IF ELSE K= " << k << endl;
  return(k);
}

long do_jumptable()
{
// Initialisation (only once!!)
typedef long (*fn)();
fn table[10] = {aa, bb, cc, dd, ee, ff, gg, hh, ii, jj};
        long i=0, k=0;

   for (i = 0; i < N; i++)
  k+=table[j-1]();      // Jumptable is zero-based!!
  
//  cout << "JUMP TABLE K= " << k << endl;
  return(k);
}

int main()
{

cout << "Selected   Switch     If Else    Jump Table\n";
cout << "-------------------------------------------\n";
for (j = 1; j < 10; j++)
{
   cout << j << "          "<< time_fn(do_switch);
   cout << "       " << time_fn(do_ifelse);
   cout << "       " << time_fn(do_jumptable) << endl;
}
j=10;
cout << "Default     "<< time_fn(do_switch);
cout << "       " << time_fn(do_ifelse);
cout << "       " << time_fn(do_jumptable) << endl;
return(0);
}


테스트 결과

선택된 사례 switch결과 if..else결과 점프테이블 결과
1  2630  1940  1920
2  2640  2290  1980
3  2640  2620  1930
4  2640  2980  1930
5  2620  3350  1930
6  2620  3700  1920
7  2650  4010  1930
8  2620  4730  1920
9  2650  4730  1920
default  1930  4740  1930

예상대로 switch와 점프 테이블은 디폴트 이외의 사례의 결과가 일정하다.
if..else는 처음 2개의 사례에서는 switch보다 빠르지만, 점프테이블 보다는 느리다.


O loop(반복문)
루프는 몇회 반복하여도 한번만 코딩하기 때문에 소스 코드를 비교적 컴팩트 하게 보여
주는 방법이다. 여기에 퍼포먼스에 대한 루프의 위험이 숨겨져 있다. 루프의 내용중
비효율적인 부분이 있다면 루프의 횟수만큼 증가한다. 쓸데없는 문장이 하나 있는 루프가
1만번 이상 반복된다면, 당연히 쓸데없는 문장이 1만번 실행되는 것이다.
그렇지만, 루프는 가장 퍼포먼스를 개량 시키기 쉬운 부분이다.

루프의 중지
다음의 예와 같이 루프는 종료조건이 붙으면서 구현되는 일이 많다.

for( j = 0; j < arraySize; j++)
{ ~~ }

while(q < nrOfElems)
{ ~~ }

do { ~~ }
while (countDown > 0);

위의 문장은 구조적으로는 바르지만, 비효율적이다. 프로그램의 실행 중 배열의 각 요소의
평균 엑세스 회수가 어느쪽이라도 같다면, 이와 같이 기술된 루프는 평균적으로 50%는 쓸데
없이 반복되는 것이다. 루프가 정말 모든 배열요소를 반복 할 필요가 있는 것은 "찾고 있다"
하는 요소가 때때로 배열의 최후의 위치에 있는 경우뿐이다. 루프에 제 2의 중지항을 추가
하고, 목적 요소가 발견됐다면 중지 할 수 있도록 하는것은 이 때문이다.


플러그에 의한 루프 중지
void DB::FindId(int searchId)
{
int Found = 0;

for(int i=0; i < Index && !Found; i++)
{
    if(base.getId(i) == searchId)
    {
          // 발견된 경우
         Found = 1;
    }
    -- other loop statements --
}

base.ExecuteElement(i);
~~

위의 것은 도중에서 중지 할수 있지만 한가지 단점이 있다. for문은 반복할때마다
Found의 값을 검사해야되는 오버헤드가 생긴다. 그래서 아래와 같이 break를 사용
하는 것이 좋다

break에 의한 루프 중지
void DB::FindId(int searchId)
{
for(int i=0; i < Index; i++)
{
    if(base.getId(i) == searchId)
    {
          // 발견된 경우
          break;
    }
    -- other loop statements --
}

base.ExecuteElement(i);
~~


실제로 함수의 대부분을 복잡한 루프가 차지 하고 있다. 함수의 결과는 발견된 요소에
포인터(또는 참조)인 경우가 많다. 이와 같은 경우 발견된 오브젝트를 바로 돌려주는
루프가 중지하는 것보다 더욱 좋다는 것은 말 할 필요가 없을것이다. 아래는 그와 같은
예를 나타내고 있다.

void DB::FindId(int searchId)
{
for(int i=0; i < Index; i++)
{
    if(base.getId(i) == searchId)
    {
           // 발견된 경우
           return base.GetObjectRef(1);
    }
    -- other loop statements --
}

}


루프의 일부를 스킵
루브 내의 불필요한 문장의 실행을 스킵해 루프의 반복을 고속화하는 방법이다.

void DBServiceProblems(int thershold)
{
int size = baseSize; // 멤버변수를 지역변수에 복사

for(int i=0; i < size; i++)
{
   EL * element = GetObject(i) // 지역변수 포인터

   if(element->GetPressure() < thershold) // 스킵조건
   {
      // 관계 없는 요소인 경우
      continue;
   }
  -- other loop statements --
}
}


[ 선택과 반복의 정리 ]


1. 선택
if..else문은 처음 2개까지는 빠르지만 뒤에는 점차 느려진다. 디폴트 사례(가장 뒤에 나오는 if문)가

가장 늦다. switch문은 어느 선택사례에 대해서도 같은 속도이지만, default만은 다른 사례보다도
빠르다. switch가 if..else문보다 늦은 경우느 사례가 2개까지이다. 함수를 호출 하는 경우는 점프테이블

쪽이 switch 보다 빠르다.

2. 루프
루프는 몇백회 반복하기 때문에 하나의 문장을 최적화 하는것은 실질적으로 몇백회 최적화 하는 것이다. break 문과 continue문을 사용하여, 루프의 반복 사이즈와 회수를 가능한 줄이는게 필요하다.

=================================================================