원본 : http://kldp.org/node/79109
원문 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/COptim...
이 문서는 계속 추가/수정 됩니다.

1 소개
얼마전에 모바일기기에서 일정수준의 품질을 유지하면서 실행되는 JPEG라이브러리를 만드는 프로젝트를 진행한적이 있었다. 이 프로젝트를 진행하면서, 여러가지 방법으로 프로그램을 더 빨리 만들 수 있다는 사실을 경험적으로 알게 되었다. 이 문서는 C로된 코드를 속도와 메모리 양측모두에서 최적화하기 위한 경험적인 정보들을 포함하고 있다.

물론 여러분은 C 코드를 최적화 하는 방법에 대한 참고문서를 어렵지 않게 획득할 수 있을 것이다. 그러나 대부분의 문서가 팁수준에서 문제에 접근할 뿐으로, 컴파일러나 기계수준에서 어떻게 프로그래밍을 해야 하는지에 대한 정보는 담고 있지 않다.

보통 프로그램의 속도를 높이게 되면 코드의 크기가 늘어나게 된다. 코드의 크기가 늘어나면 프로그램이 복잡해지고, 읽고 이해하기 어려워진다. 메모리 자원이 넉넉한 개인PC혹은 서버 컴퓨터라면 문제가 되지 않겠지만 PDA와 같은 제한된 메모리 자원을 가진 기기일 경우 심각한 문제가 될 수 있다. 1%의 속도향상을 위해서 코드의 크기가 10%만큼 늘어난다면 분명 문제가 될 것이다. 이런 이유로 속도와 코드크기 모두에 대한 최적화를 수행하기로 결정을 했다.

2 선언
내가 진행하는 프로젝트가 ARM 플랫폼에서 진행된 관계로, ARM 최적화와 관련된 팁들이 필요했었다. 나는 인터넷을 통해서 ARM 최적화와 관련된 많은 문서를 검색하고 이중 유용한 것들 중심으로 수집해서 테스트를 했었다. 그러나 대부분의 문서들이 나에게는 도움이 되지 않았음을 고백한다. 이러한 실수를 줄이기 위해서 유용하고 효과적인 몇개의 팁만을 모으기로 결정했다.

3 어디에 필요한가
토론의 주제를 명확히 하고 넘어가자. 컴퓨터 프로그램을 최적화하기 위한 가장 중요한 것은 프로그램을 이루는 각각의 모듈중 어느 부분이 느리게 작동하거나, 큰 메모리를 소비하는지를 찾아내는 것이다. 이들 각각의 부분을 최적화하면 프로그램이 전체적으로 빨라질 것이기 때문이다. 이러한 모듈단위의 최적화는 최적화를 위한 부분을 비교적 쉽게 찾고, 쉽게 해결할 수 있다는 장점을 가진다.

The optimizations should be done on those parts of the program that are run the most, especially those methods which are called repeatedly by various inner loops that the program can have.

일반적으로 경험이 풍부한 프로그래머들은 아주 쉽게 프로그램이 요구하는 최적화될 필요가 있는 핵심을 쉽게 찾아낼 수 있을 것이다. 가장 좋은 최적화 방법은 경험많은 프로그래머를 고용하는 것이다. 그러나 경험많은 프로그래머는 매우 비싸며, 경험이 많다고 해도 더 좋은 결과를 위해서는 최적화를 위한 좋은 툴을 사용할 필요가 있다. Visual C++ 과 같은 통합 개발환경은 함수단위로 프로그램의 소비시간을 측정할 수 있는 profiler를 제공한다. 리눅스의 경우에는 gprof와 같은 profiler를 사용할 수 있다. 혹은 Intel Vtune와 같은 프로그램을 사용할 수 있는데, 이들 프로그램을 사용하면 프로그램의 어느부분이 가장 많은 시간을 소비하는지를 확인할 수 있다. 개인적인 경험으로 루프 혹은 third party 라이브러리 메서드를 호출하는 영역이 프로그램을 느리게 하는 경우가 많았다.

4 정수
우리가 사용할 값이 음수가 아니라면 int 형대신에 unsigned int형을 사용해야 한다. 어떤 프로세스들은 unsigned integer의 연산이 signed 연산보다 매우 빠르다. 또한 나누기/나눗셈 작업의 경우에도 음수가 필요 없다면 unsigned 를 명시해주는게 좋다.

루프에 사용될 변수라고 한다면, 다음과 같이 깔끔하고 효율적으로 선언할 수 있을 것이다.

register unsigned int variable_name;

기억해야할 또다른 점은 floating point 연산은 매우 느리다라는 점이다. floating point 데이터 타입은 자바와 함께 하는 컴퓨터과학문 서를 참고하기 바란다. 척 봐도 floating point 숫자는 다루기가 꽤나 복잡하다는 것을 알 수 있을 것이다. 만약 여러분이 소숫점 2자리까지의 정확도를 유지하는 회계프로그램을 만든다면, 모든 값에 x100을해서 int 형으로 바꾼다음 연산을 하도록 한다. 가능하면 외부의 수학라이브러리를 사용하지 않도록 한다. FPUs와 같은 라이브러리는 매우 느리다.

5 나눗셈 그리고 나머지
표준적인 프로세서에서의 분모와 분자의 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)로 다시 쓰일 수 있다.

6 Combining division and remainder
나눗셈 (x/y) 그리고 나머지(x%y)둘다 종종 필요한 케이스이다
그러한 케이스에 비추어보아 나눗셈펑션을 컴파일러에 결합하는것이좋다 왜냐하면 나눗셈펑션은 항상 나눈값과 나머지를 리턴하기 필요하다 만약둘다 필요하다면 우리는 이와같은 예제를 같이 쓸수있어야한다

int func_div_and_mod (int a, int b) {
        return (a / b) + (a % b);
    }

7 2의 배수로 나누기
나누기를 할 때 2의 배수를 분자로 함으로써, 코드를 더 효율적으로 만들 수 있다. 이경우에 컴파일러는 나누기 연산대신에 shift 연산을 할 수 있기 때문이다. shift 연산은 가장빠른 연산중의 하나다. 그러므로 가능하면 2의 배수로 나눌 수 있도록 스케일을 조절할 필요가 있다. (예를 들어 66으로 나누어야 한다면 64로 나눌 수 있도록 스케일을 조절하라).

typedef unsigned int uint;

uint div32u (uint a) {
   return a / 32;
}
int div32s (int a){
   return a / 32;
}

이경우에도 signed 값보다는 unsigned 로 나누어질 수 있도록 함수를 조절할 필요가 있다. signed의 경우에는 더많은 시간이 소비된다. 왜냐하면 오른쪽으로 쉬프트 시킬경우 가장왼쪽의 비트를 0으로 만들어주는 연산이 한번더 들어가기 때문이다.

#include <stdio.h>

int main()
{
  unsigned int a = 1024;
  unsigned b, c;
  b = a/32;    // --- 1
  c = a >> 5;  // --- 2
}

1과 2는 동일한 결과를 보여주며, 컴파일러내에서도 동일하게 shift 처리된다. 다음은 intel 프로세서에서 gcc로 컴파일된 어셈블리어중 1과 2에 해당되는 부분의 코드다.

movl    $1024, -12(%ebp)
movl    -12(%ebp), %eax
shrl    $5, %eax           # b = a / 32
movl    %eax, -8(%ebp)
movl    -12(%ebp), %eax
shrl    $5, %eax           # c = a >> 5

8 배열을 이용한 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];

9 나머지 연산자의 대체
우리는 나눗셈의 나머지를 알기 위해서 나머지 연산자 %를 사용한다. 이경우 % 연산대신 판단문을 사용해서 시간을 줄일 수 있다. 아래의 두 코드를 비교해 보기 바란다.

uint modulo_func1 (uint count)
{
   return (++count % 60);
}

uint modulo_func2 (uint count)
{
   if (++count >= 60)
  count = 0;
  return (count);
}

if 문은 나머지 연산자보다 빠른코드를 생성한다. 주의 할점은 2번째 함수의 경우 0에서 60사이의 값에 대해서만 제대로 측정이 된다는 점이다.

10 전역 변수
전역 변수는 절대 레지스터에 할당할 수 없다. 포인터를 사용하여 간접적으로 할당하거나 함수호출을 이용해서 전역변수를 변환할 수 있다.

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

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에 값을 저장하고 마지막에 한번만 전역변수에 접근함을 알 수 있다.

11 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);
      }
  }

12 데이터 타입
C 컴파일러는 char, short, int, long, float, double 등의 다양한 원시 데이터 타입을 제공한다. 필요한 영역에 필요한 수준의 데이터 타입을 사용하도록 하자.

13 지역변수
가능하면 지역변수로 char 이나 short를 사용하지 않도록 한다. char와 short가 사용될 경우 컴파일러는 값을 저장하기 위해서 8bit 혹은 16bit를 할당한 후, 남는 크기를 줄이는 작업을 하게 된다. 이는 24bit, 16bit 만큼을 shift 시키는 연산을 하게 됨을 의미한다. 그러므로 입력되는 데이터가 8 혹은 16 비트라고 하더라도, 32bit로 연산을 하도록 함수를 만들 필요가 있다.

int wordinc (int a)
{
   return a + 1;
}

short shortinc (short a)
{
    return a + 1;
}

char charinc (char a)
{
    return a + 1;
}

3번째 코드가 가장 빠른결과를 보여줄 것이라고 생각할지도 모르지만, 1번째 코드가 가장 빠르게 작동한다.

14 포인터
구조체를 그대로 넘길경우 구조체의 모든 값이 스택에 올라가기 때문에 느리게 작동한다. 그래서 구조체의 포인터를 넘기는 경우가 많다. 나는 수 kbyte의 구조체를 넘기는 프로그램을 본적이 있다. 이런 경우 포인터를 쓰도록 하자.

포인터를 통해서 구조체를 넘길때, 구조체의 멤버를 수정할일이 없다면 상수로 선언해서 넘기도록 하자.

void print_data_of_a_structure ( const Thestruct  *data_pointer)
{
   ...printf contents of the structure...
}

이렇게 하면 컴파일러는 인자로 넘어온 포인터가 수정할 수 없는 외부 구조체라는 것을 알게 된다. 이렇게 되면, 값이 사용될 때마다 다시 읽혀질 필요가 없어지게 된다. 또한 이러한 코드는 실수로 구조체 멤버의 변수를 바꾸는 것과 같은 실수를 하지 않도록 해준다.

15 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;
}

코드가 좀더 보기 좋아진다는 효과도 노릴 수 있다.

16 Binary Breakdown
여러개의 조건을 검사하다 보면, if와 else if를 여러개 사용하는 경우가 생긴다.

if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
} else if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8)

{
}

이경우 2개로 나누어서 조건 검사를 하도록 한다.

if(a<=4) {
    if(a==1)     {
    }  else if(a==2)  {
    }  else if(a==3)  {
    }  else if(a==4)   {
 
    }
}
else
{
    if(a==5)  {
    } else if(a==6)   {
    } else if(a==7)  {
    } else if(a==8)  {
    }
}

이렇게 하면 최악의 경우 비교횟수가 절반이 됨을 알 수 있다. 필요에 따라서는 아래와 같이 3중루프 코드로 만들 수도 있다. 좀더 빠르게 동작하긴 하겠지만 코드가 보기 어려워진다는 단점이 생긴다.

if(a<=4)
{
    if(a<=2)
    {
        if(a==1)
        {
            /* a is 1 */
        }
        else
        {
            /* a must be 2 */
        }
    }
    else
    {
        if(a==3)
        {
            /* a is 3 */
        }
        else
        {
            /* a must be 4 */
        }
    }
}
else
{
    if(a<=6)
    {
        if(a==5)
        {
            /* a is 5 */
        }
        else
        {
            /* a must be 6 */
        }
    }
    else
    {
        if(a==7)
        {
            /* a is 7 */
        }
        else
        {
            /* a must be 8 */
        }
    }
}

17 Switch 대신 lookup table 를 사용하라
switch는 다음과 같은 경우 사용한다.

* 여러개의 함수중 하나를 호출해야할 필요가 있을 때
* 다양한 리턴값을 넘겨받고 이를 처리해야 할때
* 여러개의 코드중 하나를 실행시켜야 할때

예를 들어서 조건값을 입력받아서 거기에 맞는 문자열을 리턴하는 아래와 같은 코드가 있다고 가정해보자.

char * Condition_String1(int condition) {
  switch(condition) {
     case 0: return "EQ";
     case 1: return "NE";
     case 2: return "CS";
     case 3: return "CC";
     case 4: return "MI";
     case 5: return "PL";
     case 6: return "VS";
     case 7: return "VC";
     case 8: return "HI";
     case 9: return "LS";
     case 10: return "GE";
     case 11: return "LT";
     case 12: return "GT";
     case 13: return "LE";
     case 14: return "";
     default: return 0;
  }
}

위의 코드는 아래와 같이 좀 더 효율적인 코드로 만들 수 있다. 덤으로 보기에도 편하다.

char * Condition_String2(int condition) {
   if ((unsigned) condition >= 15) return 0;
      return
      "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" +
       3 * condition;
}

첫번째 루틴은 240byte가 필요하지만 두번째 루틴은 72바이트만 소모되고 있다.

18 루프
루프는 모든 프로그램에서 사용되는데, 많은 경우 루프에서 과다한 시간을 소비하게 된다. 여러번 실행되는 루프틔 특성상 조그마한 시간의 낭비가 게속 누적되기 때문이다.

18.1 Loop termination
루프를 종료시키기 위한 검사는 항상 count-down-to-zero 방식을 사용하도록 한다. 이것은 좀더 적은 시간을 소비한다. 아래의 두개의 예제는 동일한 일을한다. 다른점이 있다면 첫번째 코드는 루프를 증가시킨다는 점이고 두번째는 루프를 감소시킨다는 점이다.

int fact1_func (int n)
{
    int i, fact = 1;
    for (i = 1; i <= n; i++)
      fact *= i;
    return (fact);
}

int fact2_func(int n)
{
    int i, fact = 1;
    for (i = n; i != 0; i--)
       fact *= i;
    return (fact);
}

18.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--) { }

이들은 모두 동일한 수행능력을 보여준다.

18.3 Loop jamming

18.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.
    }
}

18.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;
}

18.6 Earyl loop breaking
루프를 사용하다보면, 일정 조건이 만족되면 뒤의 프로세스가 더이상 필요 없어지는 경우가 있다. 이 경우에는 break를 이용해서 루프를 벗어나도록 한다.

found = FALSE;
for(i=0;i<10000;i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
    }
}

if( found ) printf("Yes, there is a -99. Hooray!\n");

위의 코드는 -99가 포함되어 있는지 아닌지를 확인하는 프로그램이므로, 일단 발생이 되었다면, 루프를 돌 필요가 없다. 아래와 같이 break 문으로 빠져나가면 쓸데없는 루프의 낭비를 줄일 수 있다.

    found = FALSE;
    for(i=0; i<10000; i++)
    {
        if( list[i] == -99 )
        {
            found = TRUE;
            break;
        }
    }
    if( found ) printf("Yes, there is a -99. Hooray!\n");

18.7 Loop unrolling

19 함수 디자인
함수를 작고 가볍게 많드는건 좋은 생각이다. 이렇게 함으로써 컴파일러는 register 할당과 같은 영역에서 좀더 쉽게 최적화 할수 있게 된다.

19.1 함수 호출 Overhead
프로세서에서 함수의 호출은 예상과 달리 그리 큰 비용이 들지는 않는다. 함수가 호출되면 register에 함수의 인자를 넘기게 된다. 이 인자들은 char, short, int, float, structure등 이 올 수 있다. 이들 인자는 실제 4개만을 전달할 수 있다는 한계를 가진다. 이 이상으로 인자가 넘어가게 되면, stack를 이용해서 함수의 인자를 넘기게 된다. 당연히 함수를 호출함에 있어서 OverHead가 발생하게 된다. 함수호출시 발생하는 인자의 제한에 대해서는 Linux에서의 Assembly문서를 참고하기 바란다.

예제코드

    int f1(int a, int b, int c, int d) {
       return a + b + c + d;
    }

    int g1(void) {
       return f1(1, 2, 3, 4);
    }


    int f2(int a, int b, int c, int d, int e, int f) {
      return a + b + c + d + e + f;
    }

    ing g2(void) {
     return f2(1, 2, 3, 4, 5, 6);
    }

6개의 인자를 사용하는 f2와 g2함수는 스택에 저장되어 있는 인자를 꺼내기 위해서 2번의 메모리 접근이 더 발생하게 된다.

19.2 가능한 인자의 수를 줄여라
그러므로 가능한 적은 수의 인자를 넘겨받도록 함수를 설계할 필요가 있다.

* 4개 이하의 인자를 가지도록 함수를 설계하라. 4개가 넘어가면 스택을 통해서 인자를 넘기게 된다.
* 만약 함수가 4개 이상의 인자가 사용되면, 스택을 통해서 인자를 넘기게 되고 스택의 크기만큼 메모리 접근이 발생하게 된다.
* 이럴 경우 구조체를 선언하고, 구조체에 대한 포인터를 넘기는 방식을 사용하도록 한다.
* 구조체를 사용하면 인자의 양을 줄일 수 있으며, 코드 활용성도 높아지게 된다.
* 인자에 사용되는 자료형은 long크기 이상으로 하도록 하자.
* Avoid functions with a variable number of parameters. Those functions effectively pass all their arguments on the stack.

19.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));
    }

comment:
Thanks for very good and intersting postings! (Sorry, can't type Korean). I have some comments:

9) I'm not sure how many cycles are needed to compute % operator; however, IMHO, this would be better than branchness. Including ARM processors, most modern processros are exploiting branch predctions for aggressive dynamic scheduling. So, branch mis-prediction should be considered to minimize negative effects. If branch mis-predctions occur in the second code, I'm pretty sure that this code is slower than the first one. In other words, removing branchness is better.

I think you can replace % operator with & operator for calculating remainder. & should be quite faster than % and branch.

15) Unless we have a really stupid compier, I don't think that this kind of replacement would bring a better performance. A comper will automatically uses *cached* (more precisely, cached into a register) value in computing others unless a variable is declared as volatile.

17) Really good!! I've never seen that.

18) Absolutely agree with you. I'm always using * for (i = n - 1; i >= 0; --i) * insted of * for (i = 0; i < n ; ++i) *

ohhara의 이미지

http://www.azillionmonkeys.co

http://www.azillionmonkeys.com/qed/optimize.html 이 글을 읽어보시면 아마 느끼시는게 많이 있으실 듯... :)

여러가지 명언이 나옵니다.
Memory in modern computers is most definitely NOT "random access".
라던가...

나름대로 감동적으로 읽은 글. :)

Taeho Oh ( ohhara@postech.edu , ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Digital Media Professionals Inc. http://www.dmprof.com

권순선의 이미지

<code>...</code> 태그를

<code>...</code> 태그를 제가 추가하였습니다. :-)

yundreamm의 이미지

// 오하라님 좋은 글

// 오하라님
좋은 글 감사합니다. 함 읽어봐야 겠네요.

// 권순선님
고맙습니다. 훨씬 보기 좋군요.
약간의 귀차니즘만 극복하면 될건데 -.-;

blkstorm의 이미지

오호...

예전에 회사 다닐 때 ARM프로세서 강사로 오셨던 분의 강의 내용과 아주 흡사하군요. @.@

17번은 인상적이군요. @.@

오랜만에 보니깐 재미있네요... ^^;;

조성현의 이미지

감사~

재미있군요. :) 앞으로 프로그래밍 할 때, 많은 도움이 될 것 같습니다.
----
MyWiki http://linu.sarang.net
MyBlog http://ntames8.linuxstudy.pe.kr
----
;p $ rm -rf ~ && mkdir ~ && wget $열정 and $연애

anyshin의 이미지

좋은정보 감사합니다.

와.. 참고하겠습니다. 감사합니다.

kalstein의 이미지

미세 성능 높이기

미세 성능 높이기 관련 아티클이네요...

요즘은 제가 C++ SW design쪽에 관심을 가지면서...약간 덜해지긴했지만...그래도 몇가지 추가 및 수정을 하자면...

일단 객체뒤에 ++ 를 쓰는것 보다는 (postfix) 앞에 쓰는것이 좋습니다 (prefix)
i++ 보다는 ++i 라는거지요. 이유는...i++의 경우는 temp로 리턴될 값을 저장해야 하기 때문인데요...아마 int 형태일 경우에는 컴파일러가 order를 뒤바꿈으로써 최적화를 할테지만...C++에서 객체사용중이라면 좀 비효율적이 됩니다. (MEC++ 책에 나옵니다 ^^;)

그리고...18.5를 좀 더 개선해 보도록 하지요. 그냥 내용 자체는...loop unrolling인가? 하는 기법같은데요...그렇게 하지말고 이런 방식은 어떨까요?

int countbit3(uint n)
{
  int bits = 0;
  while (n != 0)
  {
    bits += n & 1;
    n >>= 1;
  }
  return bits;
}

이 방식은 루핑 횟수는 많겠지만, 분기문이 없으므로 캐쉬미스가 없습니다. 상위 2개의 샘플보단 빠르겠죠?

^^

ps : 근데...countbit2가 countbit1에 비해...월등히 분기문이 많은데...;; 과연 성능 향상이 어느정도인지 궁금하네요...

owlet의 이미지

while문에서도 분기가

while문에서도 분기가 일어난다는것을 잊으신것같습니다. 루프 언롤링을 사용하는 이유가 분기 횟수를 줄이기 위함입니다. 32bit가 체킹된다고 가정했을때 분기 횟수를 비교해보면
countbit1 : while = 33, if = 32, total = 65
countbit2 : while = 5, if = 32, total = 37
countbit3 : while = 33, if = 0, total = 33
3이 2보다 total은 적지만 무조건 분기하는 while보다는 조건에 따라서 분기하지 않을수도 있는 if문이 더 유리할것같아 보입니다. 또한 3에서는 n & 1의 결과를 레지스터에 저장한 후 bits에 더해야 하기때문에 환경에 따라서 손실이 더 있을수도 있을것같습니다.

kalstein의 이미지

아차차...while문

아차차...while문 분기는 생각을 안했군요 ㅎㅎ

레지스터에 저장한 후 bits에 더하는 문제는...if문안에서 도 어차피 AND연산의 결과를 어딘가에 저장한 후에 0인지 아닌지 비교하니까 큰 문제는 없을꺼 같아요.

countbit3을 제시한 이유가...분기문 자체는 적으면 적을수록 컴파일러가 최적화 시키는게 편하다고 하더군요. 그쪽 분야는...워낙 대단한 분들이 많아놔서리 ㅎㅎㅎ

ohhara의 이미지

약간 다른 얘기긴

약간 다른 얘기긴 한데...

max, abs, min을 분기 안 쓰고 구현하는 것도 다음과 같이 가능하다더군요. 분기 안 쓰고 구현하려는 처절함이 느껴지는 -_-;;;

static int int_max(int a, int b) {
b = a-b;
a -= b & (b>>31);
return a;
}

static int int_abs(int a) {
return a - ((a+a) & (a>>31));
}

static int int_min(int a, int b) {
b = b-a;
a += b & (b>>31);
return a;
}

Taeho Oh ( ohhara@postech.edu , ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Digital Media Professionals Inc. http://www.dmprof.com

magingax의 이미지

급 질문...~!!

그럼 global 변수를 애용하는게 비효율적이란 건가요?

덩어리가 크고 자주 쓰이는 클래스를 global로 좋고 각 함수에서 여러번

사용하는 구조를 애용했는데..이건 아닌건가요?

큰괭이의 이미지

어떤 각도로 코드를 보느냐가 차이가 아닐가요?

"unsigned가 signed보다 효율적이고 parameter pointer를 직접 Access하지 않고. Local에 pointer변수를 만들어서 casting해서 쓰고"

사실 이런 원리의 중요성은 그 사람이 어떤일을 하느냐에 따라서 이런 fact의 중요도가 결정되는 것 같습니다.
저도 Embedded하는 사람입니다만.
driver를 작성할때는 되도록 이런 부분에 대해서 신경을 씁니다만.
Application을 할때 이런 부분까지 고민하면서 코딩을 하면... 코드 짜기 정말 힘들죠.
(혹은 이런데 신경쓰는 것보다 다른 관점의 것을 신경쓰는것이 App코딩에서 더 성능향상을 가져올 수 있습니다.
ex) File Access를 최소화 한다던가, MemAlloc을 최소화 할 수 있도록 코딩한다던가...)

실제 Driver작성시/DSP Code작성시/Kernel 코드 작성시/Assembler코드 작성시 일때는 이러한 부분이 많이 생각해서
읽기 쉽고 최적화된 코드를 작성해야겠지요.

Global변수가 비효율적이다 라는 관점은 Application Level로 올라가면 무의미 할것 같습니다 ^^.

tj의 이미지

컴파일러가 값이 변할지 변하지 않을지 몰라서 그래요

int global;

void myfn()
{
        global++;
        some_function();
        global++;
}

누구나 global을 바꿀 수 있고, some_function()이 global을 바꿀지 안바꿀지를 컴파일러가 알 수 없어서 레지스터에 값을 캐싱할 수가 없는건데, 원글에서처럼 스택 변수에 캐싱을 해도 되고 아니면 컴파일러에게 some_function()이 pure function이라는 걸 __attribute__로 알려줘도 됩니다.

음, 마이크로 최적화가 필요한 부분이 있긴 하지만 아키텍쳐 의존적인 부분도 많고, 컴파일러들도 똑똑한데다가, 무작정 적용해선 대부분의 경우 의미있는 성능 차이로 나타나지 않거나 오히려 성능이 나빠지는 경우도 있습니다 (최적화한다고 쓰는 코드들이 원래 코드보다 빨라보이지만 크기는 커지는 경우가 많아요. 결과적으론 더 느리죠). 가독성을 목표로 코드를 쓰는게 훨씬 나으리라 생각합니다. 마이크로 최적화는 코드 돌리면서 oprofile로 보고 정말 hot path들만 해줘도 충분해요.

ㅡ,.ㅡ;;의 이미지

if문에 대해서...

16번..if문에 대해서...
>>16 Binary Breakdown
>>여러개의 조건을 검사하다 보면, if와 else if를 여러개 사용하는 경우가 생긴다.

이부분... 그와같은경우는 switch 문이 더빠름.

그리고..8번.
>>8 배열을 이용한 index 생성
default 처리가 어렵다.. 즉, 범위이상의 값이 들어오면 메모리에러로 바로죽어버리는 매우위험한방법입니다.
또한 그리큰이득은 얻기힘들다.사실어느것이 우월한지 잘모르겠네요..
인덱스는 포인터계산이 한번들어가니..

>>11 Using Aliases
>>아래의 코드를 보기 바란다.
이부분도...좀...이상한거같네요..
함수호출은 이미 문법의 의미상 값을 복사하도록합니다. 포인터를쓰던 로컬변수를 쓰던말이죠..그러나.컴파일러에의해
최적화를 기대하셨다면.. 글쎄요.. 요즘컴파일러는또어떨지.. 아니면 조만간 가까운미래의 컴파일러는또어떨지..
그럴경우..로컬변수카피는 오히려 부하를 가중시키게될텐데요..

다른건 안봤는데 그냥 눈에띄는거같아...적어봤습니다.
----------------------------------------------------------------------------
C Library Development Project

소타의 이미지

어떻게 쓰느냐에

어떻게 쓰느냐에 따라 많은 if 문 보다는 switch가 편하고 더 빠른건 맞는것 같습니다.
문자열 비교로 HTTP 파서를 만들때
if (strncasecmp) 이런걸 자주 써야 하는데 좀 느린감이 있었는데

switch (요청헤더[0]) {
case 'c':
case 'C':
  switch (요청헤더[1]) {
   case 'o':
   case 'O':
    //Connection: 헤더 처리
    if (strncasecmp(&(요청헤더[2])))
   break;
  }
break;
}

이런식으로 풀어서 많은 성능 향상을 맛본적이 있습니다

그리고 8번 같은 경우는 저도 자주 쓰는데 그전에 범위에 대한 처리는 미리 해야겠죠. 그부분은 사전에 해야 하는 작업이거나 또는 임베디드나 모바일 쪽이라 한정적인 환경이라 생략하신것 같네요 ㅎㅎ

cronex의 이미지

16번. 비교하는 값이

16번.
비교하는 값이 한가지라면 당연히 switch 가 좋겠지만...
여러가지 값을 비교해가면서 상황이나 대상을 분류해나가는 if 문이라면
당연히 if ... else if를 여러개 써야만 하는경우가 생깁니다.
(윤드림 님의 예가 좀 너무 단순한 면이 없지는 않습니다만...)
(만약 0이하일 때, 0일때, 1~30일때, 31~100일 때 , 101~300일때, 301~1000 일때, 1001~3000일 때 3001~10000 일때, 10001 이상일 때 등으로 분류해야 할때 과연 switch 로 해결할 수 있을까요?)

8번 값의 범위를 벗어나는 처리는 배열 사용 앞에서 체크를 해줘야겠지요.

11번
윤드림 님이 말씀하신건 함수가 아니라 for문 앞에 local variable을 선언하는 것을
말씀하신 겁니다. 포인터 값의 원래값을 가져다 쓰려면 메모리 참조를 2번하게 됩니다.
따라서 그 값을 local에다가 저장해두고 그 값을 가져다 쓰는 것이
메모리 참조 횟수를 줄일 수는 있지요. 저게 루프를 10번 도는게 아니라
한 1000번쯤 도는 거라고 생각하면 엄청난 차이가 생깁니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

ㅡ,.ㅡ;;의 이미지

설사

설사 스트링이라하더라도 저렇게 if 를 문기할것이라면 switch가 유리하죠.
그러나 매우 잦은호출이 일어 나지 않는다면 스트링은 그냥 if else로 줄줄이 씁니다..
11번은 포인터참조를줄인다는말이었던가요.. 잘못이해했네요..

>17 Switch 대신 lookup table 를 사용하라
>switch는 다음과 같은 경우 사용한다.
>예를 들어서 조건값을 입력받아서 거기에 맞는 문자열을 리턴하는 아래와 같은 코드가 있다고 가정해보자.
>위의 코드는 아래와 같이 좀 더 효율적인 코드로 만들 수 있다. 덤으로 보기에도 편하다.
>첫번째 루틴은 240byte가 필요하지만 두번째 루틴은 72바이트만 소모되고 있다.

이것도 코딩량은 줄어들었으나 성능은떨어짐..

----------------------------------------------------------------------------
C Library Development Project

cronex의 이미지

댓글을 수정해서

댓글을 수정해서 설명했지만 단순 값의 비교가 아닌
값의 범위가 설정되는 경우는 if ~ else if를 쓸 수밖에 없다고 생각합니다.

17번의 경우는 코딩량이 아니라 코드가 컴파일 됐을 때
메모리에서 차지하는 양을 의미합니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

ㅡ,.ㅡ;;의 이미지

물론 아예 if else 를

물론 아예 if else 를 쓰지말라는게 아니죠.. 써야할곳이 있죠 그러나 예제에서 보면 그렇다는겁니다.
상황에 맞게 써야죠. 저도 if else 더많이 씁니다.
제가 게을러서 궂이 작은 성능개선을 위해서 switch 쓰고 싶지 않거든요..
적절히 쓰죠.. 그러나 글쓰신분은 성능을위해 더 힘든? 코드를 마다하지 않으셨기에 그렇것같으면 차라리 switch가
성능도 더좋고 보기도 더좋지 않냐는것입니다.

17번
윗글에도 코드량이 적다는건 이미 말씀드린거구요..
컴파일됬을때 덩치가 더커지는지는 해보지 않아 어느것이 많은지는 모르겠습니다만.
위의 다른내용들을보면 성능을위해서 더많은 코드를 마다하지 않고 있죠...
그런의미에서 말한겁니다.

----------------------------------------------------------------------------
C Library Development Project

cronex의 이미지

최적화라는 건 성능

최적화라는 건 성능 최적화와 메모리 최적화가 있을 수 있습니다.
하지만 어느 경우에도 trade-off라는게 있어서 항상 성능 최적화만을 추구할 수도 없고
항상 메모리 최적화만을 추구할 수도 없습니다.

어느 부분은 메모리 사용량을 절반 이하로 줄여도 성능이 아주 약간 떨어질 뿐인 반면
어느 부분은 메모리 사용량을 10%만 줄여도 성능이 두배 이상으로 느려지는 경우도 종종 있죠.
반대로 성능을 10% 향상 시키기 위해 메모리 사용량을 두배를 투자해도 힘든 경우도 있고
약간의 캐시를 두는 것만으로도 성능이 두배로 향상되는 경우도 있습니다.

따라서 상황에 맞는 최적화를 수행해야 해야 합니다.
윤드림님의 글은 최적화를 수행하고자 할 때 기존의 코드를 고칠 수 있는 방법들을 소개하고 있을 뿐..
모든 경우에 대해서 꼭 이렇게 해야만 모든 경우와 상황에서 항상 최적화된 코드가 나온다는
메뉴얼이나 무슨 법칙같은 걸 써놓으신게 아닙니다. 이건 본문 처음에도 나와있지요.

yundream7 씀:
보통 프로그램의 속도를 높이게 되면 코드의 크기가 늘어나게 된다. 코드의 크기가 늘어나면 프로그램이 복잡해지고, 읽고 이해하기 어려워진다. 메모리 자원이 넉넉한 개인PC혹은 서버 컴퓨터라면 문제가 되지 않겠지만 PDA와 같은 제한된 메모리 자원을 가진 기기일 경우 심각한 문제가 될 수 있다. 1%의 속도향상을 위해서 코드의 크기가 10%만큼 늘어난다면 분명 문제가 될 것이다. 이런 이유로 속도와 코드크기 모두에 대한 최적화를 수행하기로 결정을 했다.

윗글의 내용은 단순한 경험적인 예들일 뿐입니다. 명제들이 아니구요.
어느 경우에는 단순히 성능이나 메모리만 잡아먹는 코드 일 수도 있고
어느 경우에는 둘다 잡을 수 있는 좋은 해법일 수도 있는 거죠.
다 쓰기 나름일 것입니다. 다만 글의 일부가 맘에 안드신다면 그건 안쓰시면 되겠습니다.
부디 본문의 목적과 지향하는 바를 이해해주셨으면 하네요.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

tj의 이미지

약간 부연하면,

cronex님 말씀에 대부분 동의하구요. 약간 부연하면, 요즘 포로세서들의 경우엔 메모리 최적화와 성능 최적화가 같이 가는 경우가 많습니다. 캐시 미스 레이턴시가 워낙 크기 때문에, 작은 코드가 빠른 코드인 경우가 많고, 소프트웨어가 어느정도만 복잡해지면 마이크로 밴치시 성능이 좋아졌던 코드를 실제에 적용하면 캐시 사용량 문제로 전체 속도에 떨어지는 경우도 생깁니다. 좀 더 장기적으로 생각하면, 아키텍쳐나 프로세서 세대 변화에 따라 득이 되던게 해가 될 수도 있고, 컴파일러에게 의도가 아니라 구현을 설명하게 되서 컴파일러가 최적화할 수 있는 가능성을 떨어뜨립니다. 특수 환경이나, 프로세싱 시간의 대부분을 사용하는 tight loop이 아니면 가독성을 떨어뜨리는 최적화는 최선을 다해 피하는게 좋습니다.

익명사용자의 이미지

8번/11번

음, 제 생각에는 범위를 벗어나는 값이 들어왔을 때 바로 죽어버리는 게 아주 안전하고 좋은 방법입니다. (물론, "범위를 벗어나는 값이 들어오지 않는다"라고 약속이 되어 있을 때.) 그러면 왜 죽었는지 core 떠보고 바로 디버깅을 할 수 있죠.

범위를 벗어난 값이 원래 들어오면 안되는데도 불구하고 죽지 않으면 오히려 그만큼 버그 발견이 늦어집니다. 최악의 경우, 들어오면 안되는데 일부러 default를 써서 안 죽게 만들어 놓으면, 이건 버그를 숨기는 코드가 되는 거죠. QA에서 테스트할 때까지 잘 돌다가 고객 앞에서 장렬히 산화하게 됩니다. -.-

그리고 11번은 확실히 차이가 있습니다. 포인터를 쓰면 aliasing의 가능성 때문에 컴파일러가 매번 메모리에서 읽어와야 합니다. 예제로 나온 코드의 경우 메모리를 열 번 참조해야 하죠. 하지만 로컬 변수로 복사하면 (레지스터가 넉넉하단 전제 하에) 메모리에서 단 한번만 읽어오면 되고, aliasing이 없다는 게 보장이 되기 때문에 최적화의 가능성이 훨씬 넓어집니다.

컴파일러는 무슨 최적화를 해도 절대 원래 코드와 동작이 달라지면 안되기 때문에 아주 보수적으로 (최악의 가능성을 항상 생각하면서) 작업할 수밖에 없죠. 로컬 변수는 aliasing 가능성을 없애줘서 optimizer의 운신의 폭을 넓혀줍니다.

예를 들어 다음과 같은 코드를 보죠.

void foo(int *a, int *array)
{
  while (*a) { array[*a] = (어쩌구); (*a)--; }
}

이 경우 a와 array가 겹치지 않는다는 보장이 없으므로 컴파일러는 루프를 돌 때마다 a를 다시 읽어와야 하는 것은 물론, array에 값을 넣는 순서도 순차적이 아닐 수 있다고 가정해야 하죠.

하지만 *a를 먼저 로컬 변수에 복사해 놓고 시작하면, 컴파일러는 a가 하나씩 감소하여 0이 될 때까지 루프를 돈다는 것을 알 수 있으므로 loop를 위해 최적화된 기계어 명령을 쓸 수도 있고, array에 값이 순서대로 들어가는 게 보장되므로 이 경우에 특화된 각종 최적화를 수행할 수 있습니다. (예를 들어 x86이라면 string instruction 등을 쓸 수도 있겠죠.)

- jick

ㅡ,.ㅡ;;의 이미지

개인적인

개인적인 의견이라하셨지만...약간반론을 달자면....

메모리 범위가 넘어서면 바로 안죽습니다.. 죽는거는 잡아도 안죽으면 나중에실상황에서 죽겠죠..

>>default를 써서 안 죽게 만들어 놓으면, 이건 버그를 숨기는 코드가 되는 거죠. QA에서 테스트할 때까지 잘 돌다가 >>고객 앞에서 장렬히 산화하게 됩니다
이건좀...ㅡ,.ㅡ;;
default로들어오면 당연히 에러로 빠지든지하지 그걸 머어떻게 해놓길래 테스트때 안죽고 고객앞에서 죽는다는건지 이해할수 없군요.. 전 일부러 만들려해도 힘들것같군요..

----------------------------------------------------------------------------
C Library Development Project

시지프스의 이미지

11번에 대해 질문이 있습니다.

11 Using Aliases
int *data가 결코 변하지 않으면 const int *data라고 하면 되지 않나요? 이렇게 되면 *data가 상수니까 알아서 한 번만 읽어 올 것 같은데요.
아니면 int * restrict data라고 하면 되지 않나요?

cronex의 이미지

포인터 값이

포인터 값이 변경되서는 안되기 때문에 const int *data 를 쓰는 것과는 별개의 문제 입니다.
즉, int형 포인터인 data가 가리키는 값(즉 *data)을 읽어오기 위해서
값 참조를 2번 해야 한다는 것이 문제지요.

즉, 변수 data 에 저장된 포인터 값을 읽어와서(1)
그 포인터가 가리키는 메모리 위치에 있는 실제 int 값을(2)
읽어오기 때문이죠.

하지만 만약 저 data 값을 로컬 변수에 저장하면
포인터의 위치를 참조할 필요 없이 바로 로컬 변수에 접근해서 값을 읽을 수 있습니다.

물론 단 한번만 쓰고 끝날 값이라면 이렇게 할 필요가 없겠지만
루프내에서 사용되거나 비교문에서 중첩되서 사용되는 경우라면
로컬 변수로 저장하는 편이 좋습니다.

그리고 restrict와는 그다지 관련이 없는 것 같습니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

전웅의 이미지

> > 11 Using Aliases > > int

> > 11 Using Aliases
> > int *data가 결코 변하지 않으면 const int *data라고 하면 되지 않나요?

const 라는 형한정어는 값이 변하지 않음을 보장해 주는 역할을 하는 것이
아닙니다. 단지 "해당 포인터를 통해서 값을 변경할 수 없음"을 의미하는
것뿐입니다 - 아래 코드를 생각해 보시기 바랍니다.

void f(const int *a, int *b)
{
    int temp = *a;
    *b = 0;
    printf("%d, %d\n", temp, *a);
}

int i = 100;
f(&i, &i);

> > 이렇게 되면 *data가 상수니까 알아서 한 번만 읽어 올 것 같은데요.
> > 아니면 int * restrict data라고 하면 되지 않나요?
> >

넵, 맞습니다. 바로 restrict 가 해당 포인터가 가리키는 변수는 그
포인터를 통해서만 접근이 된다는 사실을 분명히 해주기 위한 것입니다!
만약, 위의 제 예에서 a, b 포인터를 restrict 로 한정할 경우, f(&i, &i)
라는 호출 자체가 잘못된 행동(undefined behavior)이 됩니다. 따라서
컴파일러는 함수 f 안에서 안전하게 *a 와 temp 가 결국 같은 값이라는
가정을 할 수 있게 되는 것입니다.

>
> 포인터 값이 변경되서는 안되기 때문에 const int *data 를 쓰는 것과는 별개의 문제 입니다.
> 즉, int형 포인터인 data가 가리키는 값(즉 *data)을 읽어오기 위해서
> 값 참조를 2번 해야 한다는 것이 문제지요.
>
> 즉, 변수 data 에 저장된 포인터 값을 읽어와서(1)
> 그 포인터가 가리키는 메모리 위치에 있는 실제 int 값을(2)
> 읽어오기 때문이죠.
>
> 하지만 만약 저 data 값을 로컬 변수에 저장하면
> 포인터의 위치를 참조할 필요 없이 바로 로컬 변수에 접근해서 값을 읽을 수 있습니다.
>
> 물론 단 한번만 쓰고 끝날 값이라면 이렇게 할 필요가 없겠지만
> 루프내에서 사용되거나 비교문에서 중첩되서 사용되는 경우라면
> 로컬 변수로 저장하는 편이 좋습니다.
>

11번 항목의 핵심은 포인터의 위치를 추가로 참조하냐 안 하느냐의 문제가
아닙니다.

void a(int *a, int *b)
{
    ...
    for (i = 0; i < N; i++)
        process(*a);
    ...
}

이 코드에서 컴파일러가 "포인터 a 가 가리키는 변수가 다른 포인터나
변수에 의해 aliasing 되었는지 알기 어렵기 때문에" *a 를 고정된 값으로
치환해 최적화할 수 없다는 뜻입니다. 즉, a 가 가리키는 변수가 맨 처음
보인 예에서 처럼 다른 포인터나 변수 등을 통해 수정될 수 있기 때문에
매번 a 가 가리키는 변수의 현재 저장된 값을 확인해야 하는 문제가 발생
한다는 것입니다 - 물론, data flow analysis 등을 통하면 aliasing 에
대한 사실을 확인하는 것도 가능합니다만, 항상 가능한 문제도 아닐 뿐더러
쉽지 않은 과정입니다.

이 문제는 위에서 언급했듯이 restrict 를 적절히 도입해 해결할 수 있는
문제입니다. 즉, 포인터 a 가 가리키는 변수가 aliasing 되지 않았음을
컴파일러에 알려주면 컴파일러가 문제의 코드를 "맘놓고" 최적화할 수 있게
됩니다. 즉, 위의 코드를

void a(int * restrict a, int * restrict b)
...

로 써줄 경우 컴파일러는 aliasing 에 대한 걱정 없이 루프 수행 전 *a 의
값을 한번만 확인해 process() 호출에 사용할 수 있게 됩니다.

임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
유용한 팁으로 생각할 수 있습니다.

> 그리고 restrict와는 그다지 관련이 없는 것 같습니다.
>

고로 restrict 와 관련된 문제입니다.

그럼...

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

시지프스의 이미지

답변 감사합니다.

인용:
const 라는 형한정어는 값이 변하지 않음을 보장해 주는 역할을 하는 것이
아닙니다. 단지 "해당 포인터를 통해서 값을 변경할 수 없음"을 의미하는
것뿐입니다 - 아래 코드를 생각해 보시기 바랍니다.

전혀 생각하지 못한 부분이었네요.
인용:
임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
유용한 팁으로 생각할 수 있습니다.

그런데 gcc는 restricted pointer를 지원하지 않나요? http://gcc.gnu.org/c99status.html에는 지원한다고 나오는데, syntax 에러가 나네요.
      1 #include 
      2
      3 void noname(int * restrict a);
      4
      5 int main(void) {
      6         int *a = malloc(sizeof (int));
      7         *a=8;
      8         noname(a);
      9         return 0;
     10 }
     11
     12 void noname(int * restrict a) {
     13         printf("%d",*a);
     14 }

rest.c:3: error: syntax error before "a"
rest.c:12: error: syntax error before "a"
rest.c: In function `noname':
rest.c:13: error: `a' undeclared (first use in this function)
rest.c:13: error: (Each undeclared identifier is reported only once
rest.c:13: error: for each function it appears in.)

전웅의 이미지

> > 임시 변수를 직접

>
> 임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
> 손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
> 것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
> 통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
> 유용한 팁으로 생각할 수 있습니다.
>
> 그런데 gcc는 restricted pointer를 지원하지 않나요? http://gcc.gnu.org/c99status.html에는 지원한다고 나오는데, syntax 에러가 나네요.
>
> 1 #include
> 2
> 3 void noname(int * restrict a);
> 4
> 5 int main(void) {
> 6 int *a = malloc(sizeof (int));
> 7 *a=8;
> 8 noname(a);
> 9 return 0;
> 10 }
> 11
> 12 void noname(int * restrict a) {
> 13 printf("%d",*a);
> 14 }
>
> rest.c:3: error: syntax error before "a"
> rest.c:12: error: syntax error before "a"
> rest.c: In function `noname':
> rest.c:13: error: `a' undeclared (first use in this function)
> rest.c:13: error: (Each undeclared identifier is reported only once
> rest.c:13: error: for each function it appears in.)
>

-std=c99 옵션이 필요합니다.

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

yarmini의 이미지

좋은 글 감사합니다.

도움이 많이 되는 글이었습니다^^

항상 최적화와 가독성 두 갈래에 있는 데 둘 다 잡는 팁도 많네요^^

언 제 나 멋 진

익명 사용자의 이미지

장문의 좋은 글이네요.

장문의 좋은 글이네요.

부족하게나마 저도 나름대로 아래 블로그에 정리를 해보았습니다.

http://a.tk.co.kr/126

Kyuseo의 최적화 원칙:

- CPU 부하가 큰 반복작업은 최대한 최적화 코드로 작성하라.

- 코딩 수정으로 최적화를 하지 말고 알고리즘으로 최적화를 하라.

- 더 빠른 속도가 필요하다면 어셈블리(Assembler) 언어를 사용하라

- 코드의 최적화보다는 코드의 유지보수를 우선하라.

- 사소한 최적화에 시간과 노력을 투자하지 말아라.

- 중복코드를 최소화 하라.

- inline을 활용하라.

  1. 2010.03.29 00:07

    비밀댓글입니다

  2. 2010.04.16 20:31

    비밀댓글입니다

  3. michael kors outlet 2013.07.26 20:51

    희미한 달빛이 샘물 위에 떠있으면,나는 너를 생각한다.

  4. ghd 2013.07.29 08:35

    좋으면 좋고 싫으면 싫은 거지, 뭐가 이렇게 어렵고 복잡하냐구

  5. ugg boots 2013.08.05 06:08

    사람들은 죽을걸 알면서도 살잖아 .사랑은 원래 유치한거에요

  6. cheesam31@gmail.com 2021.02.08 17:23

    9번 나머지 연산자의 대체 부분 궁금한점이 있어서 글 남깁니다.
    "우리는 나눗셈의 나머지를 알기 위해서 나머지 연산자 %를 사용한다." 라고 말씀하시고... 2번째 modulo_func2 같은 경우 0 ~ 60 같은 경우에만 정확한 값이 반영된다고 적혀있는데 보통 나머지 연산자 %를 쓸때 나누어지는 값(60) 보다 큰 값의 나머지를 구하기위해 사용되는 연산자 아닌가요...? 저렇게 사용하면 빨라지는건 알겠는데.. 보통 저렇게 사용을 하나요..? 지식이 짧아서 글 남깁니다.

아래 메인 함수를 보고 소스를 완성시켜라.
* 조건 : 아래 메인 함수를 수정해서는 안된다.
int main() {
  int studentNo = 0 ;
  cin >> studentNo ;
  StudentInfo* studentInfos = new StudentInfo[studentNo] ;
  for ( int i = 0 ; i < studentNo ; i ++ ) 
  {
    cout<< "이      름 : ";
    cin>>studentInfos[i].name;
    
    cout<< "영어  점수 : ";
    cin>>studentInfos[i].engScore;
    
    cout<< "한국어점수 : ";
    cin >> studentInfos[i].korScore;

    cout << "수학  점수 : ";
    cin >> studentInfos[i].mathScore;
    
    computeSum(studentInfos[i]) ;
    computeAverage(studentInfos[i]) ;
  }
  sortStudentInfos(studentInfos, studentNo) ;
  cout << endl << "After sorting the student scores..." << endl ;
  printStudentInfos(studentInfos, studentNo) ;
  delete [] studentInfos ;

  return 0;
}

여기서 StudentInfo구조체의 정보와 전역변수는 아래와 같다.
const int SubjectNo = 3 ;

typedef struct StudentInfo {
  string name ;
  int engScore ;
  int korScore ;
  int mathScore ;
  int sum ;
  float average ;
}StudentInfo;

이 소스코드를 완성시켜라.

  1. michael kors outlet 2013.07.27 07:58

    눈을 감아봐 입가에 미소가 떠오르면 네가 사랑하는 그 사람이 널 사랑하고 있는거야.

Kyuseo's C++ 독시젠을 활용한 주석 작성 스타일 가이드라인(규칙)

Kyuseo's C++ Comment Style Guideline with Doxygen

 

버전 : 2.2

최종수정: 2008-01-24

작성자 : Kyuseo의 게임 프로그래밍 이야기 :: http://a.TK.co.kr

 

개요..

 

프로그램 소스 코드의 주석은 크게는 다른 사람이, 작게는 코드 작성자 자신이 소스 이해하는데 도움을 주고 오작동을 방지하며 수정을 쉽게 해주는 매우 중요한 역할을 합니다. 따라서 코딩 스타일과 마찬가지로 주석 역시 공통의 규격이 없다면 다른 사람의 주석을 읽는데 많은 수고가 필요하므로 규격화된 주석 작성방법이 필요합니다.

 

위와 같은 이유로 C++ 주석 스타일 가이드라인이 필요하며 적어도 팀 내에서는 동일한 주석 스타일을 사용할 필요성이 있습니다.

 

주석을 코드 라인단위로 너무 많이 작성하는 것은 오히려 코드를 보기 어렵게 하는 안 좋은 방법이지만 자신의 코딩스타일과 실력만을 믿고 주석을 전혀 쓰지 않는 행위는 더더욱 안 좋은 방법입니다. 많은 경험을 통하여 적절한 위치와 꼭 필요한 곳에 올바른 주석을 작성하는 것은 다른 사람을 위한 배려이자 자신의 실수를 줄이기 위한 매우 중요한 행동입니다.

 

과거에는 소스코드의 주석과 문서화를 별도로 행하였지만 개발자들이 문서작성을 꺼려하고 문서와 코드의 주석 모두 동기화시켜서 업데이트하기가 무척 번거롭기 때문에 대부분의 개발자들의 두 가지 방법 중 하나의 방법만 택하여 작업을 하는 것이 대부분입니다. 이러한 반복 작업을 간소화 하기 위해서 Doxygen 및 기타 소스코드 자동 문서화 도구를 사용하는 방법을 최근에는 많이 채택하고 있습니다.

(주의: 소스코드 자동 문서화 도구를 사용하여 주석을 작성한다고 하여 문서화를 하지 말라는 것은 절대 아닙니다. 설계나 구조 등의 요구사항 등에 대해서는 별도의 문서화가 작업이 되어야 합니다.)

 

Kyuseo 의 C++ 프로그래밍 주석 가이드라인은 Doxygen 방식을 기본 주석 작성 스타일로 가지고 있습니다.

 

 

RapidEngine에서 Doxygen 과 주석 스타일 가이드를 사용한 소스코드

 

 

 

RapidEngine에서 Doxygen과 주석 스타일 가이드를 사용하여 자동으로 문서화가 된 예

 

 

 

★ 기본 원칙 ..

 

- 주석 작성의 목적은 자기 자신뿐만이 아니라 다른 사람들을 위한 작업이다.

 

(따라서 자기만 알아볼 수 잇는 약어를 사용하거나 농담으로 주석을 작성하면 안 된다.)

 

 

- 주석은 모국어로 기입한다.

 

(타국어(영어)를 잘하는 사람이라도 모국어보다 타국어를 잘할 수 없다. 또한 다른 사람들을 위한 배려이다.)

 

 

- 주석은 항상 최신으로 유지 되어야 한다.

 

(코드를 변경한 이후에는 반드시 관련 주석을 함께 변경해야 한다. 최신이 아닌 주석은 오히려 코드분석에 큰 방해가 된다.)

 

 

- 헤더파일(Header File)의 손상을 최소화한다.

 

(헤더파일은 개발자가 가장 자주 접근하는 파일이다. 무분별한 주석추가로 인하여 헤더파일 자체의 분석능력을 감소 시킨다면 개발자는 결국 불편한 문서파일만 접근하게 될 것이다.)

 

 

- 중복된 주석 입력 작업은 최소화 한다.

 

(인자를 설명하기 위해서 인자를 주석에 입력할 필요는 없다. 동일한 클래스 멤버 함수의 설명주석을 헤더와 구현 파일에 중복하여 기입 할 필요가 없다. 불필요한, 중복된 주석입력은 주석입력을 꺼리게 만드는 계기가 된다. 단 클래스와 같이 중요도가 매우 높은 항목은 예외로 한다.)

 

 

- Doxygen은 파일, 클래스, 구조체, 멤버변수, 멤버함수, 일반함수에만 사용한다.

 

(Doxygen의 모든 문법과 방법을 사용하여 너무 많은 활용을 하는 것은 코드가 좋지 않고 가독성이 떨어지게 된다.)

 

 

- 함수의 주석은 동사로 끝을 맺고 변수나 인자, 객체의 주석은 명사로 끝을 맺는다.

 

(함수는 행위를 나타내고 변수는 객체를 나타내기 때문이다.)

 

 

- 무분별한, 불필요한 주석추가는 자제한다.

 

(어려운 알고리즘이 아니라면 라인단위로 주석을 다는 것은 코드의 가독성을 해치게 된다.)

 

 

- 주석 입력작업의 목적은 코드를 알아보기 좋게 하는 작업이므로 모든 사항을 지나치게 철저하게 지킬 필요는 없다.

 

 

★ Doxygen을 활용한 주석 작성 규칙

 

- 소스 파일의 최상단에 파일명, 날짜, 제작자, 설명을 명기한다.

 

(주석의 방식이/**, */인 것에 주의한다. h, inl, cpp등 모든 소스파일에 표기한다.)

 

예)


/**    
    @file    RHttp.h
    @date    2004/11/2
    @author    채경석(kyuseo99@chol.com) RapidEngine™
    @brief
*/

 

 

- 클래스 및 구조체 인터페이스 윗부분에 클래스 명, 날짜, 제작자, 설명 등을 첨부한다.

 

(주석의 방식이/**, */인 것에 주의한다.)

 

예)


/**
    @class    CRHttp
    @date    2004/11/2
    @author    채경석(kyuseo99@chol.com)
    @brief    Http 클라이언트
    @warning 몇몇 서버상의 오류로 가능한 업로드는 소문자로 한다. (특히 하나포스 마이홈)
*/
class CRHttp
{
        …
};

 

 

- 클래스 / 구조체 멤버 변수 주석은 공백 1칸을 띄우고 "///<" 를 사용한다.

 

예)
long m_lFrameTic; ///< 1000/fps로 이 시간후 (ms) 프레임을 이동한다.

long m_lNewTime; ///< 최종 프레임 진행 시간

 

 

- 클레스 멤버 함수는 헤더에 "///<"을 사용하고 추가 설명, 리턴값 및 인자에 대한 필요하다면 구현부분에 추가 설명을 입력한다.

 

선언부 예)

 

DWORD* Decode( DWORD* pBuffer, DWORD* pSize ); ///< 암호화 버퍼와 변경된 size리턴 (4바이트 증가)

 

 

구현부 예)

 

/**
    @return    찾은 그룹 (없으면 NULL)
    @warning 외부에서 관련된 동기화 객체를 Lock을 걸어 사용한다.
*/
CRGroup* CRServer::FindGroup( RGID gid ///< 그룹 아이디
                )
{
}

 

 

- 일반 함수는 헤더에 "///"을 사용하고 추가 설명, 리턴 값 및 인자에 대한 필요하다면 구현부분에 추가 설명을 입력한다.

 

선언부 예)


/// 하위폴더 포함하여 디렉토리를 생성한다.
extern BOOL CreateXDirectory( LPCTSTR szPath );

 

구현부 예)


/**
    @return    디렉토리 생성 성공유무
    @warning King\kong\file.dat와 같은 파일명은 포함 안된다.
*/
BOOL CreateXDirectory( LPCTSTR szPath ///< 생성할 디렉토리명 (예:C:\\King\\kong, King\\kong\\)
            )
{

}

 

 

★ 일반 주석 작성 규칙..

 

- 주석은 설명하는 구문의 앞 라인에 한줄로 작성한다.

 

올바른 예)


// 파일의 크기가 설정되지 않았다면…
if( nSize == 0 )
{
    return FALSE;
}



잘못된 예)


if( nSize == 0 ) // 파일의 크기가 설정되지 않았다면…
{
    return FALSE;
}

if( nSize == 0 )
{    // 파일의 크기가 설정되지 않았다면…
    return FALSE;
}

 

내용출처 : http://a.tk.co.kr/130 
출처 : http://junyoung.tistory.com/trackback/21

C++/자바/C#과 달리 C는 언어 차원에서 OOP 기능을 제공하지 않기 때문에 OOP를 구현하려면 상당한 애로 사항이 있다. 하지만 실제로는 여러 가지 테크닉과 꼼수를 동원해서 C로도 OOP를 많이 하고 있다. 문제는 "할 수 있는" 것과 "하기 편한" 것 사이의 넘을 수 없는 4차원의 벽인데...오늘은 같은 프로그램을 C로 짠 것과 C#으로 짠 것을 비교해 보면서 그 벽을 느껴보도록 하자. :-)

살펴볼 것은 오픈 소스계에서 가장 유명한 GUI 툴킷 중 하나인 GTK+이다. GTK+은 기본 코드가 C로 작성되어 있고, 바인딩이라고 해서 GTK+를 다른 언어에서 쓸 수 있도록 이음새 역할을 하는 라이브러리가 C++/자바/C#/파이썬 등의 주요 언어별로 하나씩 제공된다. C로 만들었음에도 불구하고 OOP를 구현하기 위해 혼신의(?) 노력을 기울인 흔적이 역력하기에 연구 대상으로는 안성맞춤이다.

Hello GTK+

설명을 위해 GTK+로 간단한 프로그램을 작성해 보았다(제일 간단한 편인데도 뭔가 뻑뻑한 느낌이 든다):

#include <gtk/gtk.h>

 

static void destroy(GtkWidget *window, gpointer data);

 

int main()

{

    GtkWidget *window;

    GtkWidget *button;

 

    gtk_init(NULL, NULL);

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(GTK_WINDOW(window), "Hello GTK+");

    gtk_container_set_border_width(GTK_CONTAINER(window), 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);

 

    button = gtk_button_new_with_mnemonic("_Close");

    g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(destroy), NULL);

 

    gtk_container_add(GTK_CONTAINER(window), button);

    gtk_widget_show_all(window);

 

    gtk_main();

    return 0;

}

 

static void destroy(GtkWidget *window, gpointer data)

{

    gtk_main_quit();

}

이 프로그램을 리눅스 등에서

$ cc `pkg-config --cflags --libs gtk+-2.0` hellogtkplus.c -o hellogtkplus

처럼 컴파일하면 현재 디렉터리에 hellogtkplus라는 실행 파일이 생긴다(에러가 난다면 GTK+ 개발 관련 패키지가 설치되어 있지 않은 것임). 이것을 실행해 보면

처럼 화면에 표시될 것이다. 우상단의 X 버튼을 누르거나 중앙의 "Close" 버튼을 누르면 프로그램이 종료된다. 버튼에 단축키가 지정되어 있어서 Alt+C를 눌러도 버튼을 누르는 것과 같은 효과가 난다.

GTK+ 프로그램의 뼈대

GTK+ 프로그램은 기본적으로 다음과 같은 뼈대로 이루어진다:

#include <gtk/gtk.h>

 

int main(int argc, char *argv[])

{

    gtk_init(&argc, &argv);

        .

        .

        .

    gtk_main();

    return some error code;

}

원래는 여기서처럼 main() 함수의 argc, argv를 gtk_init()에 넘겨주어야 하지만, 위의 "Hello GTK+" 프로그램은 커맨드 라인 처리를 하지 않기 때문에 편의상 NULL, NULL을 넘겨주었다. 창이나 버튼, 레이블 등의 각종 위짓(윈도에서의 컨트롤에 해당)을 생성하고 처리하는 부분은 gtk_init() ... gtk_main() 사이에 들어가게 된다.

창 만들기

    GtkWidget *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(GTK_WINDOW(window), "Hello GTK+");

    gtk_container_set_border_width(GTK_CONTAINER(window), 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);

GTK+에서 창은 gtk_window_new()로 생성한 다음 gtk_window_set_title()로 제목을 설정하고, gtk_widget_set_size_request()로 크기 설정, g_signal_connect()를 써서 destroy 시그널과 해당 콜백 함수를 연결해 주면 기본 동작이 가능해진다. 이 시그널 연결 과정이 아주 중요한데, 이걸 생략하면 메인 창을 닫아도(즉 destroy 시그널이 발생해도) 프로그램이 종료되지 않는다.

위 코드에서 눈여겨 봐야 할 점은 window란 변수가 gtk_window_new()에 의해 GtkWindow 타입의 인스턴스가 되지만 타입 자체는 GtkWidget으로 선언되어 있다는 점이다. C는 OOP의 상속 개념이 없기 때문에 GTK+에서는 모든 위짓을 상위 타입인 GtkWidget으로 선언한 다음 필요할 때마다 실제 타입으로 캐스트하는 꼼수를 쓰고 있다. 그래서 다른 OOP 언어에서라면 애시당초 필요하지 않을 캐스트 연산이 거의 도배에 가까울 정도로 많이 필요하다.

그런데 여기서 생길 수 있는 의문은

    void *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(window, "Hello GTK+");

    gtk_container_set_border_width(window, 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(window, "destroy", destroy, NULL);

처럼 void * 타입을 쓰면 캐스트 없이 훨씬 깔끔하게 되지 않을까? 이 경우 코드 자체는 깔끔해지지만 타입 체크가 무력화되는 심각한 결점이 있다. 또는

    GtkWindow *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(window, "Hello GTK+");

    gtk_window_set_border_width(window, 25);

    gtk_window_set_size_request(window, 200, 100);

    gtk_window_signal_connect(window, "destroy", destroy, NULL);

처럼 각 타입별로 set_title(), set_border_width(), set_size_request() 등의 온갖 함수를 일일히 구현해 줌으로써 타입 안정성을 유지하는 방법도 있을 수 있다. 그렇지만 이 방법은 OOP의 핵심 목적인 코드 재사용을 거의 포기한다는 면에서 void *의 사용 못지 않게 나쁜 해결책이다.

이런저런 문제를 고려해 볼 때 그나마 GTK+ 방식이 제일 나아 보인다.

버튼 만들기와 화면 표시

버튼도 창처럼 GtkWidget 타입으로 선언한다:

    GtkWidget *button;

 

    button = gtk_button_new_with_mnemonic("_Close");

    g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(destroy), NULL);

 

    gtk_container_add(GTK_CONTAINER(window), button);

    gtk_widget_show_all(window);

gtk_button_new_with_mnemonic()라는 길다란 이름의 함수를 써서 버튼을 생성하고 있는데, 함수 오버로딩(같은 이름의 함수를 여러 개 정의하는 것)이 안되다 보니 GTK+에는 이런 식으로 길고 복잡한 이름을 가진 함수들이 많다. 창과 마찬가지로 버튼도 g_signal_connect()를 써서 마우스를 클릭했을 때 destroy() 콜백 함수를 호출하도록 설정하고 있다.

gtk_container_add()를 써서 버튼을 창 안에 집어 넣고, gtk_widget_show_all()을 써서 최종적으로 버튼 달린 창을 화면에 표시하게 된다. 직관적으로 생각할 때는 창에 버튼을 바로 넣으면 편할 텐데, GTK+에서는 일단 창을 컨테이너로 바꾼 다음 그 안에 버튼을 넣어야 하는 번거로움이 있다.

Hello Gtk#

같은 일을 하는 프로그램을 C#으로 작성해 보았다:

using Gtk;

using System;

 

class HelloGtkSharp

{

    public static void Main()

    {

        Application.Init();

 

        Window window = new Window("Hello Gtk#");

        window.BorderWidth = 25;

        window.DefaultWidth = 200;

        window.DefaultHeight = 100;

        window.DeleteEvent += Destroy;

 

        Button button = new Button("_Close");

        button.Clicked += Destroy;

 

        window.Add(button);

        window.ShowAll();

 

        Application.Run();

    }

 

    private static void Destroy(object obj, EventArgs e)

    {

        Application.Quit();

    }

}

컴파일하는 방법은:

$ gmcs hellogtksharp.cs -pkg:gtk-sharp-2.0

여기서 쓴 API는 GTK+의 C# 바인딩인 Gtk#이라는 툴킷이다. 보이는 바와 같이 GTK+의 온갖 지저분한 캐스트 연산자와 장황한 함수명이 싹 사라지고 코드가 아주 깔끔해졌다. 그와 함께 타입 안정성이 보장되기 때문에 서로 다른 타입의 변수간 대입을 한다든지, 잘못된 타입을 메쏘드에 넘겨준다든지 하면 컴파일러가 즉각 실수를 잡아내준다.

크기는 hellogtkplus.c가 805바이트, hellogtksharp.cs가 576바이트다. 공백을 제외하고 문자끼리 비교해 보면 hellogtkplus.c는 668바이트, hellogtksharp.cs는 397바이트로 차이가 더 크게 벌어진다.

C로 OOP를 할 수 밖에 없는 경우

위에서 본 것처럼 다른 언어로 할 수 있는데도 굳이 C로 OOP를 하는 것은 엄청난 닭질이다. ㅡ.ㅡ

그럼에도 불구하고 C로 OOP를 해야만 하는 이유는 쓸만한 언어가 C 밖에 없는 환경, 속도가 절대적으로 중요한 환경 등등이 있겠다. 윈도, 리눅스, BSD 등 각종 OS들도 커널의 일부에 OOP 개념을 도입해서 만들고 있는데, 이런 환경에서는 C 말고는 사실상 선택의 여지가 없다고 하겠다.
  1. GHd 2013.07.26 20:39

    당신은 내가사랑할 만한 사람이 아니예요,사랑하지 않으면 안될 사람이예요.

#include <iostream.h>
#include <malloc.h>

class EMB
{
  public:
    char Z;
    EMB(int J)
    {
      cout<<J<<endl;    
    }
    void test()
    {
      cout<<"test\n";
    }
    EMB()
    {
      cout<<"생성자\n";
    }
    ~EMB()
    {
      cout<<"소멸자\n";
    }
};

int main()
{
  EMB *P;
  EMB *P2;
  // C++에서의 객체 생성법은 C에서의 
  // 동적할당을 사용하는게 아니라
  // new라는 생성자를 사용하여 객체를 
  // 메모리에 할당 시키는 방법을 사용한다.
  int *iP = new int;
  int *iP2 = new int[3];
  
  
  cout<<"P의 출력"<<endl;
  P = (EMB*)malloc(sizeof(EMB));// 동적할당으론 생성자와
  P->test();                    // 소멸자가 실행되지 않는다.
                                // C에서의 동적 할당법을 사용하지 
                                // 말자는 의미인다.
  cout<<"P2의 출력"<<endl;
  P2=new EMB;
  P2->test(); 
  
  // delete : new에 반대되는 키워드로 메모리에 
  // 레퍼런스를 삭제시켜주면서 소멸자를 생성시킨다.
  delete P2;
  delete iP;
  delete []iP2;  // 지울때 배열은 []로 배열이란 표시를 해줘야 한다.
                // 그렇지  않을 경우 배열의 1개만 삭제를 해주고
                // 나머지는 메모리를 차지하고 있게 된다.
  
  free(P);    
  return 0;
}
[별 그리기는 반복문을 이해하는데 상당히 많은 도움을 줍니다.]

이제 막 C에 입문한 후에 반복문을 다루게 되는데 어려움을 느끼는 경우가 많은데 별 그리기를 통해서 이를 극복해 나갈 수 있다고 생각합니다. 

1.  직각 삼각형 그리기
- 이 별 그리기는 이중 반복문을 통해서 별을 찍어줍니다.
 바깥 반복문에서는 각 행을 카운트 해 주면 되고 안쪽 반복문에서는 별을 찍어주는 방법을 사용하는 것이 보편적입니다. 
 여기서 역 직각 삼각형은 조금 생각을 필요로 합니다. 별을 찍기전에 공백을 먼저 찍어줘야 하기 때문입니다.
 이러한 경우 다음과 같은 구조를 가지는것이 보편적입니다.
//////////////////////////////////////////////////////
 바깥 for문
{
안쪽 for문
{
// 공백을 찍어냄
}
안쪽 for문
{
// 별을 찍어냄
}
}
//////////////////////////////////////////////////////


                 <직각 삼각형 그리기의 예>


2. 역 삼각형 그리기
 - 이것은 역 직각 삼각형 그리기의 응용이라고 생각하시면 됩니다. 앞의 공백과 별을 찍어주는데 다만 홀수로 입력을 받는 것이 프로그램을 제어 하는데 유용할 것입니다.


                 <역 삼각형 그리기의 예>

3. 다이아몬드(마름모) 형태로 별 그리기
 - 이것은 난이도가 상당히 있습니다.
 별의 개수가 처음에는 증가하다가 중반부를 넘어서면 다시 줄어들어야 하기 때문입니다.
 물론 다양한 방법의 테크닉이 존재 하므로 찬찬히 생각을 해 보시면서 프로그램을 짜 보시기 바랍니다.
 

                 <마름모 그리기의 예>
4. 나비리본 형태로 별 그리기
 - 나비리본 형태로 별 그리기는 마름모 그리기의 응용이라고 볼 수 있겠습니다. 하지만 난이도는 이중 제일 높다고 할 수 있겠습니다. 하지만 잘 생각 해 보신다면 그렇게 어렵다고 할 수도 없겠습니다.


                 <나비리본 그리기의 예>
##################################################################################################

맨 아래에 각 결과에 대한 소스코드를 첨부하였습니다. 
별 그리기에 도전하고 계신 분들이라면 결과 화면만을 보시고 그에 해당하는 결과를 스스로 도출 할 수 있도록 노력해 보시고 아래의 소스코드는 단지 참고용으로만 사용하셨으면 합니다.

소스 코드 보기는 view plain , 소스 복사는 copy to clipboar라는 기능을 사용하시면 클립보드에 소스가 입력되므로 텍스트 에디터에 붙여넣기 하시면 됩니다.



함수 포인터란 함수를 포인터로 받는 것을 의미한다.
그냥 포인터도 헷갈리는데 함수포인터는  모양까지 어려워 보여 많이 헷갈리게 한다는 것은 엄연한 사실이고
많은 분들이 여기서 머리에 두통을 느끼실 것이다.

하지만 결론적으로는 함수포인터는 함수의 주소를 받는 4byte의 크기를 가지는 포인터 변수것이고 이 주소를 통해서 값을 넘기고
받는 것이다.

이 소스는 EHW라는 함수를 fP라는 함수 포인터(fP는 function pointer의 약자쯤 되겠다)로 받아서
함수의 주소와 함수포인터의 값을 출력해보고 이 출력값을 이용해서
주석 처리한 부분에 그 출력값을 이용해서 함수를 호출해보는 정상적인 함수호출이 아니지만
함수도 결국 주소에 불과다하는 것을 보여주기 위한 소스이다.
그럼 함수 포인터를 보는 방법을 말해보기위해 위의 소스를 예를 들어 설명을 해 보겠다.

어떤 변수든 그 변수를 선언할때 데이터 타입을 같이 선언해 주어야 한다는 것은 기본이다.
우리의 눈을 어지럽게 하는 것은 바로 그 함수 포인터의 데이터 타입(Data type)부분 인 것이다.
이 부분만 이해를 한다면 함수 포인터는 그냥 포인터일 뿐이다.

그럼 함수 포인터의 데이터 타입은 어떻게 결정 되는가?
우리는 함수를 포인터로 받으려고 하는 것이다. 그럼 함수의 데이터 타입에 함수포인터 변수의 데이터 타입을 맞춰야 한다.
자 결론이 나왔다. R-value의 데이터 타입을 L-value에 맞춰야 한다는 의미이다.
(R-value : 대입연산자에서의 오른쪽 변에 쓰인 값, L-value : 대입연산자의 왼쪽 변에 쓰인 값)

그럼 위의 경우에 함수의 데이터 타입을 알아보자.
일단 선언한 EHW의 함수의 반환형은 void형이다. 그리고 우린 이 함수를 포인터로 받으려고 한다. 또한 이 함수의 인자값은 현재 int형이다.
이 말을 code로 표현 한다면 void(*)(int)가 될것이다.

그럼 다 끝난 것이다. 이 포인터를 함수 포인터형 변수로 쓸 변수 fP로 받으면 되는것이다.
void (*fP)(int) = EHW; // 이 부분이 소스 중간에 사용된다면 명시적 형변환의 일종이 될 것이다.
이렇게 말이다.

설명이 어렵다고 느껴지거나 잘못 됬다고 생각 되는 점이 있으면 덧글을 달아 주시기 바랍니다.

+ Recent posts