컴퓨터공부/C & C++ & STL

Writing Efficient C and C Code Optimization

achivenKakao 2009. 3. 23. 22:31

4.2 나눗셈 그리고 나머지

표준적인 프로세서에서의 분모와 분자의 32bit 나눗셈은 20~140의 실행 사이클을 가지고 있다. 나눗셈을 이용하면 다음과 같은 시간이 소비된다.
Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator) 
= C0 + C1 * (log2 (numerator) - log2 (denominator)). 
 
널리 쓰이는 버젼은 약 20+4.3N의 사이클을 보여준다. ARM 뿐만 아니라 프로세서를 막론하고 이런 연산은 피하는게 바람직하다. 나눗셈연산은 가능하다면 곱셈으로 대체해서 사용하기 바란다.

예를들어 (a/b) > c 는 b * c가 integer 범위안이라는 것을 안다면 a > (c * b)로 다시 쓰일 수 있다.

4.6 배열을 이용한 index 생성

특정값에 대응되는 문자를 변수에 입력하는 코드를 만든다면 다음과 같이 switch 문을 사용할 것이다.
switch ( queue ) { 
  case 0 :   letter = 'W'; 
     break; 
  case 1 :   letter = 'S'; 
     break; 
  case 2 :   letter = 'U'; 
     break; 
} 
 
혹은 if else 문을 사용할 수도 있을 것이다.
 if ( queue == 0 ) 
   letter = 'W'; 
 else if ( queue == 1 ) 
   letter = 'S'; 
 else 
   letter = 'U'; 
 

다음과 같이 문자의 배열을 인덱스화 하면 더 빠른 접근이 가능하다. - 사용하기도 쉽다 -
static char *classes="WSU"; 
letter = classes[queue]; 
 
 

4.8 Using Aliases

아래의 코드를 보기 바란다.
  void func1( int *data )    { 
      int i; 
 
     for(i=0; i<10; i++)  
     { 
            anyfunc( *data, i); 
     } 
  } 
 

*data 가 결코 변하지 않는다고 하더라도, anyfunc 함수를 호출하는 컴파일러는 이걸 알 수가 없다. 그래서 변수가 사용
될 때마다 메모리로 부터 다시 읽어들이게 된다. 이 문제는 지역변수를 하나더 둠으로써 해결할 수 있다.
  void func1( int *data ) 
  { 
      int i; 
      int localdata; 
 
      localdata = *data; 
      for(i=0; i<10; i++) 
      { 
          anyfunc ( localdata, i); 
      } 
  } 
 

5.1 전역 변수

전역 변수는 절대 레지스터에 할당할 수 없다. 포인터를 사용하여 간접적으로 할당하거나 함수호출을 이용해서 전역변수를
변환할 수 있다. 따라서 컴파일러는 전역변수의 값을 레지스터에 올려서 캐쉬할 수 없게 되고 때문에 글로벌 변수를
이용할 때마다 다시 읽어들이는 오버로드가 생기게 된다. 그러므로 가능하면 글로벌 변수를 직접 호출하는 대신에,
로컬변수를 이용해서 필요한 연산을 하고 그 결과를 글로별 변수에 할당하는 방법을 사용해야 한다.


 
int f(void); 
int g(void); 
int h(void); 
int errs; 
void test1(void) 
{ 
  errs += f(); 
  errs += g(); 
  errs += h(); 
} 
void test2(void) 
{ 
  int localerrs = errs; 
  localerrs += f(); 
  localerrs += g(); 
  localerrs += h(); 
  errs = localerrs; 
} 
 
test1은 매번 전역변수를 로드해야 한다. 반면 test2의 경우 레지스터에 등록된 localerrs에 값을 저장하고 마지막에
한번만 전역변수에 접근함을 알 수 있다.




5.4 Pointer chains

구조체내의 정보에 접근하려다 보면 포인터의 chain을 사용해야 할 때가 있다. 다음과 같은 경우다.
 
typedef struct { int x, y, z; } Point3; 
typedef struct { Point3 *pos, *direction; } Object; 
 
void InitPos1(Object *p) 
{ 
   p->pos->x = 0; 
   p->pos->y = 0; 
   p->pos->z = 0; 
} 
 
이럴 경우 p->pos 를 다른 포인터에 할당해서 접근하도록 하자. 이렇게 하면 p->pos 가 캐쉬되므로 좀더 효율적으로
작동하게 된다.
void InitPos2(Object *p) 
{  
   Point3 *pos = p->pos; 
   pos->x = 0;  
   pos->y = 0; 
   pos->z = 0; 
} 
 
코드가 좀더 보기 좋아진다는 효과도 노릴 수 있다.

6.2 더욱 빠른 for 문

다음은 0부터 10까지의 숫자를 연산하기 위해서 for 문을 사용한 일반적인 예다.
for (i = 0; i < 10; i++) {...} 
 
i는 0,1,2,3,4,5,6,7,8,9 로 1씩 증가할 것이다.

가능하면 아래와 같이 숫자를 감소시키는 방향으로 for 문을 사용하라.
for (i = 10; i--;) {...} 
 
첫번재 코드보다 두번째 코드가 더 빠른 수행능력을 보여준다.

두번째 코드는 i가 0이 아니면 i를 감소시키고 다음 코드를 진행하라의 의미인데, 조건 검사의 경우
0인지 아닌지를 비교하는데 더 작은 시간이 소비되기 때문이다. 그러므로 두번째 코드는 아래와 같이 재작성할 수 있다.
두번째 예제코드 보다는 아래의 코드가 더 보기 쉬우므로, 아래의 코드를 사용하는게 가독성 측면에서 유리할 것이다.
for (i = 10; i ; i--) { } 
혹은  
for (i = 10; i!=0; i--) { } 
 
이들은 모두 동일한 수행능력을 보여준다.  

6.4 함수 루프

함수는 호출되기 위한 분명한 오버헤드가 존재한다. 실행해야될 함수가 있는 포인터만 변경하는게 아닌,
값들을 stack에 push하는 것과 새로운 변수의 할당과 같은 작업이 수행되기 때문이다. 때문에 루프에서 함수를
호출하는 등의 코드는 작성하지 않는게 좋다. 이런류의 코드는 반대로 함수에서 루프를 수행하도록 변경하는걸 추천한다.

for(i=0 ; i<100 ; i++)  
{  
    func(t,i);  
}  
-  
-  
-  
void func(int w,d)  
{  
    lots of stuff.  
} 
 

위의 코드는 아래처럼 바꿀 수 있다. 동일한 일을 좀더 빠르게 수행할 수 있다.
func(t);  
-  
-  
-  
void func(w)  
{  
    for(i=0 ; i<100 ; i++)  
    {  
        //lots of stuff.  
    }  
} 
  

6.5 Population count - 비트 계수하기

아래의 코드는는 주어진 값에 1bit가 몇개인지를 검사하는 코드다. 0000 1010 이라면 2를 리턴하는 식이다.
이러한 비트필드는 일정한 범위의 값이 참인지 거짓인지를 빠르게 체크하기 위해서 널리 사용될 수 있다.

다음과 같이 1씩 오른쪽으로 쉬프트 하면서, & 연산을 한다.
int countbit1(uint n) 
{ 
  int bits = 0; 
  while (n != 0) 
  { 
    if (n & 1) bits++; 
    n >>= 1; 
   } 
  return bits; 
} 
 

이 코드는 다음과 같이 4만큼 쉬프트 하는 식으로 바꿔서, 성능을 높일 수 있다.
int countbit2(uint n) 
{ 
   int bits = 0; 
   while (n != 0) 
   { 
      if (n & 1) bits++; 
      if (n & 2) bits++; 
      if (n & 4) bits++; 
      if (n & 8) bits++; 
      n >>= 4; 
   } 
   return bits; 
} 

7.3 인라인 함수

__inline키워드를 이용하면 함수를 인라인화 할 수 있게 된다. 이것은 일종의 매크로 처럼 작용을 하며,
함수가 호출되는 대신 함수의 본체가 직접 치환이 되어 버린다. 이렇게 함으로써, 함수를 호출하는데 드는
비용을 줄일 수 있게 된다. 반면 코드가 모두 치환되어 버리므로, 코드의 크기가 커지게 된다.
    __inline int square(int x) { 
       return x * x; 
    } 
 
    #include <MATH.H> 
 
    double length(int x, int y){ 
        return sqrt(square(x) + square(y)); 
    } 
 
인라인 함수를 이용함으로써 얻을 수 있는 이득은 다음과 같다.
  • 함수호출을 위한 비용이 발생하지 않는다. 코드를 직접 읽어들이기 때문이다.
  • 일반적으로 인자를 넘기는 방식은 변수를 복사하는 것보다 적은 비용이 든다.
  • 만약 인자가 상수라면 컴파일러는 더욱 높은 수준의 최적화를 할 수 있을 것이다. 







출처 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/COptimization