본문 바로가기
develop

for문 날씬하게 사용하자

by 찬이 2004. 4. 19.

written by 김시찬 (chanywa), 2004-01-19 (updated 2004-05-05 )
Homepage : http://chanywa.com
Email : chany@chanywa.com

가장 많이 사용되는 순환문, for

어떤 프로그램에서나 빠지지 않는 것들이 있다. 선언문, 입력문, 조건문, 출력문, 제어문이 그것인데, 그 중에서도 프로그램 처리시에 결과출력까지의 시간을 좌우하는 것이 바로 순환문이다.

물론 많은 수의 복잡한 연산 때문에 프로그램 속도가 느려지는 건 사실이다. 특히 고성능의 그래픽 출력이나 대량 데이터들의 통계 등을 할때에는 보조연산을 해주는 보조프로세서가 없다면 컴퓨터는 엄청나게 느려진다. 그런데 그것도 사실은 순환문에 의해 조장(?)되는 것이다. 컴퓨터 대부분의 작업은 몇몇 연산을 수없이 반복함으로써 얻어지는 CPU 노가다의 결정체다. 따라서 대부분의 그래픽연산, 통계연산 등도 순환문에 의해 반복되어져서 결과를 얻게 된다고 볼 수 있다.

for문만 순환문이냐


이렇게 시비를 걸진 않았으면 한다. 하지만 세상엔 참 별별 사람이 다 있는 법이다. 그래서 간단히 짚고 넘어간다. 혹시나 초보나 C 이외의 다른 언어를 주력으로 사용하는 사람의 경우에는 잘 모르는 사람도 있을 수 있으니 말이다.

for문은 C 언어에서의 예약어(Reserved word)로써 일부 루틴을 원하는 수만큼 혹은 원하는 조건이 될 때까지 반복수행하는 기능을 한다. 비슷한 종류로 while문과 do-while문이 있으나, while문의 경우 for문의 축소판 정도라 잘 사용하지 않는다. 그리고 do-while문의 경우에는 순환여부의 조건이 루틴 마지막에 존재하는 스타일로서 사용빈도가 전자에 비해 낮은 편이다.
따라서 C에서의 순환문이라 함은 대부분 for문을 뜻하게 된다.


for문? 그냥 쓰면 되는 거 아닌가?


순환문이 차지하는 비중이 큰 만큼 for문을 효과적으로 쓸 필요가 있다. 공부하는 학생의 입장이라면 for문을 이용하여 반복회수를 줄여보고자 노력을 할 것이다. 그리고 '소스 라인수 감소 == 최적화' 라고 생각하게 된다. 물론 잘못된 착각이지만, 현실은 많은 사람들이 그렇게 착각을 하고 산다.

그럼 for문을 좀 더 효과적으로 사용해서 프로그램을 최적화하는 방법을 생각해보자.


반복회수를 줄이자

1부터 100까지 출력하는 프로그램을 작성하라.

이런 문제가 나왔다면 으례히 나오는 답은 아래 소스 정도이다. 아마 대부분의 C 예제 서적이나 수업시간에 선생님이 가르쳐 주신 것과도 별 차이는 없을 것이다.

void main(void)
{
    int i;
    for( i = 1; i <= 100; i++ )
            printf("%d ", i );
}

간단히 공부하는 의미에서는 이 정도로 작성하면 충분하다. 만약 이런 루틴이 엄청 많은 경우나 혹은 빠른 처리속도를 필요로 하는 프로그램의 경우엔 좀 더 고려해볼 필요가 있다.
간단히 개선된 코드는 아래와 같다.

void main(void)
{
    int i;
    for( i = 1; i <= 100; i+=10 )
    {
        printf("%d ", i );
        printf("%d ", i+1 );
        printf("%d ", i+2 );
        printf("%d ", i+3 );
        printf("%d ", i+4 );
        printf("%d ", i+5 );
        printf("%d ", i+6 );
        printf("%d ", i+7 );
        printf("%d ", i+8 );
        printf("%d ", i+9 );
    }
}

어떻게 보면 불필요할 정도로 길게 작성했다고 보일 수 있다. 하지만 효과는 의외로 좋다. 바로 for문 자체에 걸리는 실행회수를 줄이는 방법이다. 순환문을 반복 수행하면 내부의 코드만 반복실행되는 것이 아니라, 순환문 자체도 반복적으로 수행되기 때문에 전자의 경우에는 그만큼 순환문 for의 수행회수가 많아지기 때문이다.

물론 printf()한개에 10개 변수를 다 출력하는 방법도 있겠지만, 여기서 printf()는 단지 어떤 실행 문장의 예를 든 것이므로 그것은 제외하도록 한다.
수행회수를 테스트 해보기 위해서 다음처럼 소스를 작성해 보았다.

 #define MAX_RUN_TIMES  1000
void main(void)
{
    int step;    // 실제 실행 회수
    int run;     // 원하는 작업의 실행회수
    for( step = 1, run = 0; run < MAX_RUN_TIMES; step++ )
    {
        run++;    step++;   
    }
    printf( "%d steps per %d times\n", step, MAX_RUN_TIMES );
}


#define MAX_RUN_TIMES  1000

void main(void)
{
    int step;    // 실제 실행 회수
    int run;     // 원하는 작업의 실행회수
    for( step = 1, run = 0; run < MAX_RUN_TIMES; step++)
    {
        run++;    step++;   
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
        run++;    step++;
    }
    printf( "%d steps per %d times\n", step, MAX_RUN_TIMES );
}

여기서 run 변수는 반복실행을 원하는 명령의 실행회수를 말하고, step은 원하든 원치 않든 프로그램내에서 명령들의 총 실행회수를 말한다.

즉, 동일한 수의 run을 얻고자 할때 step만큼의 명령이 실행된다는 것을 알아보기 위함이다.
1000회를 돌렸을 때, 전자의 경우는 2002회가 수행된다. 원래 소스에서의 printf("%d ", i);를 1000번 수행하려고 할때에 총 2002번의 명령이 수행된다는 뜻이다.

후자의 경우에는 똑같이 1000번을 수행하지만 실제 동작하는 명령회수는 1402번으로 상당수 감소하게 된다. 이렇게만 따져도 명령 실행회수가 줄어드는 엄청난 효과를 볼 수 있다는 계산이 나온다.

전자는 원하는 명령 1번 수행에 순환문 2번이상 수행이다. 하지만 후자는 원하는 명령 10번 수행에 순환문 2번 가량 수행이다. 이렇기 때문에 프로그램의 실행속도가 빠라질 수밖에 없다. 똑같은 기능을 하는데도 이렇게 차이가 많이 나는 것이다.


함수대신 변수를 애용하자


for문을 사용할 때에 순환하는 조건이 들어가는 부분에 함수를 사용하는 경우가 종종 있다. 하지만 이런 습관은 정말 잘못된 것이다. 가급적이면 변수를 이용해서 미리 값을 받아놓은 후에 그 변수를 이용해서 for문을 사용해야 한다. for문을 한번 돌 때마다 함수를 호출해야 하기 때문이다.

void main(void)
{
    char szCount[2] = "9";
    int i;
    for( i = 0; i < atoi( szCount ); i++ )
        printf("%d ", i );
}

보통 이렇게 사용하는 사람이 많다. 참고로 atoi()는 문자로된 숫자를 int형 숫자로 바꾸어 주는 역할을 한다. 이걸 올바르게 바꾸면 아래와 같다.

void main(void)
{
    char szCount[2] = "9";
    int i, cnt;
    cnt = atoi( szCount );
    for( i = 0; i < cnt; i++ )
        printf("%d ", i );
}

for문의 조건을 체크할 때마다 함수를 호출한다는 것은 정말 낭비이다. 실제로 이렇게 컴파일된 것을 어셈블리어로 확인결과 매번 함수를 호출하는 것으로 확인되었다. 

      for( i = 0; i < cnt ; i++ )
0040D734 mov dword ptr [ebp-8], 0
0040D73B jmp main+86h (0040d746)
0040D73D mov edx, dword ptr [ebp-8]
00$0D740 add edx, 1
0040D743 mov dword ptr [ebp-8], edx
0040D746 mov eax, dword ptr [ebp-8]
0040D749 cmp eax, dword ptr [ebp-0ch]
0040D74C jge main+0A1h (0040d761)

      for( i = 0; i < atoi( count ); i++ )
0040D6F1 mov dword ptr [ebp-8], 0
0040D6F8 jmp main+43h (0040D7d03)
0040D6FA mov edx, dword ptr [ebp-8]
00$0D6FD add edx, 1
0040D700 mov dword ptr [ebp-8], edx
0040D703 lea eax, [ebp-4]
0040D706 push eax
0040D707 call atoi (0040d8c0)
0040D70C add esp, 4
0040D70F cmp dword ptr [ebp-8], eax
0040D712 jge main+67h (0040d727)


컴퓨터 좋은데 그런것 쯤이야

이런 말 하는 사람은 프로그래머로서의 자질이 아직은 갖추어져 있지 않다고 볼 수 있다. 아니면 학원 강사나 학교 교사 정도로서만 지낼 것을 권장한다.
한개의 컴퓨터에 한개의 프로그램만 가동될 때는 별 문제가 안된다. 요즈음 컴퓨터가 옛날의 슈퍼컴퓨터와 맞먹기 때문에 for문 속도가 두배 빨라진다고 해서 눈에 띄진 않는다.

그럼 어디에 써먹느냐... 바로 C 공부시간 이외에 대부분 다 써먹는다. 요즘은 대부분의 프로그램들이 서버-클라이언트 형태이다. 그리고 또 다른 큰 분야가 임베디드 시장이다. 이런 분야가 아니라 일반 PC용 프로그램이라고 할 지라도 멀티테스킹인 윈도우 환경에서는 전체적인 시스템 속도를 좌우한다.

서버 클라이언트에서 서버역할을 하는 프로그램에는 정말 필수 요소이다. 클라이언트야 혼자 한 컴퓨터를 차지하고 동작하는게 대부분이라 큰 상관은 없지만 서버의 경우에는 사용자가 몰릴 경우 서버가 견디느냐 못견디느냐를 좌우할 만큼 중요한 요소이다. 특히 검색 기능을 포함하는 그 어떤 시스템에서도 말이다. 물론 클라이언트도 중요하다. 메신저나 바이러스 감시 프로그램 등이 몇개씩이나 떠 있는 요즘, for문 때문에 다른 프로그램들보다 속도가 2배나 느리다면 과연 인기를 얻을 수 있을까? 필자같으면 10초내에 바로 삭제해버리고 말 것이다.

임베디드는 말할 것도 없다. RTOS라고 하는 것 자체가 Real Time Operating System의 약자이다. 바로 실시간 동작을 중요시 한다. 특히 이 경우에는 화면 출력하는 부분까지 직접 제어하는 경우가 많기 때문에 필수라고 볼 수 있다. 이것은 필자가 직접 실전에서 경험한 바, 너무나 큰 효과를 봐서 잊을 수가 없다.

사실 이렇게 적고보니 내가 바보짓을 하는 것 같다. 앞에서 얘기했듯, 지금 얘기하는 부분들은 공부하는 과정이 아닌 이상 모든 실전에서 적용된다고 했으면서도 또 이렇게 나열해서 강조를 하고 있으니 말이다. 그러고 보면 나도 참 한심한 녀석이다... -_-;;


필자가 한 거짓말


사실 수치상으로 두배의 속도를 낼 수 있는 것처럼 보이긴 했으나, 사실은 거짓말이다. ^^;; 속도를 좌우하는 것에는 실행단계수 외에 다른 요인이 있는데 그것을 고려하지 않고, 단지 프로그램 명령 실행회수만을 놓고 검토했기 때문이다.
프로그램의 성능을 좌우하는 것은 크게 시간과 단계가 있다. for문은 별도의 함수가 아니라 예약어이므로 printf()를 사용하는 것보다는 훨씬 빠른 속도로 동작한다. 혹은 조건문이나 증감문에 함수를 넣게 되면 느려질 수도 있다. 따라서 시간상으로 따졌을 때에 printf()와 같은 함수와 동등하게 취급하는 것은 무리이다. 다만 여기서는 for문에 의해 필요이상으로 수행되는 단계 수를 줄이고자 하는데 목적이 있다.
어디가서 아는체 하더라도 이 정도는 알고 있는게 좋을 것 같아서 말이다. ^^;;


효과가 정말 좋은 시스템은 따로 있다


혹시 RISC, CISC라는 말을 들어보았는가? CISC는 일반 PC등에서 사용하는 프로세서 방식으로 CPU가 사용하는 명령어 수가 아주 많아서 쉽게 기능구현이 가능하지만 그만큼 속도가 느리기 때문에 고집적형태로 간다.
반대로 RISC는 주로 서버용 프로세서로서 명령어는 개수는 적지만 그만큼 코드가 단순하고 다수개의 명령이 한번에 병렬적으로 수행될 수 있기에 속도가 빠르다는 장점을 가지고 있다. 그래서 정말 돈되는 쪽으로는 CISC보다는 RISC가 많이 사용되는데, 이때에 특히 앞에서 언급한 for()의 사용법이 효과를 발휘한다.

for()의 필요이상으로 많은 명령어의 실행수를 줄여서 빠르게 하는 것도 있지만, 병렬 형태로 수행할 수 있는 명령의 수를 채워서 수행할 수 있기 때문에 한번에 여러 개의 명령을 수행할 수 있다. 따라서 효과가 극대화될 수 있는데, 이때에는 2배가 아니라 그 이상의 효과를 발휘할 수도 있다. 믿거나 말거나~~~
 

범용 마이크로프로세서를 구성하는 요소에는 명령세트, 레지스터, 메모리 공간 등이 있다. 이중 명령세트는 RISC와 CISC의 2가지로 크게 분류할 수 있다. CISC란 소프트웨어 특히, 컴파일러 작성을 쉽게 하기 위해 하드웨어화할 수 있는 것은 가능한 모두 하드웨어에게 맡긴다는 원칙 아래 설계된 컴퓨터이다. 반면 RISC는 실행속도를 높히기 위해 가능한 한 복잡한 처리는 소프트웨어에게 맡기는 방법을 택한 컴퓨터이다.

RISC의 특징을 CISC와 비교하여 알아보면 다음과 같다. 첫째, 명령의 대부분은 1머신 사이클에 실행되고, 명령길이는 고정이며, 명령세트는 단순한것으로 구성되어 있는데, 가령 메모리에 대한 액세스는 Load/Store 명령으로 한정되어 있다. 둘째, 어드레싱 모드가 적으며, 마이크로 프로그램에 의한 제어를 줄이고, 와이어드 로직을 많이 이용하고 있다. 반면에 레지스터 수가 많으며 마이크로 프로그램을 저장하는 칩의 공간에 레지스터를 배치한다. 셋째, 어셈블러 코드를 읽기 어려울 뿐 아니라 파이프라인을 효과적으로 사용하기 위해서 일부 어셈블러 코드를 시계열로 나열하지 않은 부분이 존재하여 컴파일러의 최적화가 필요하다. 최적화를 하지 않으면 파이프라인을 유효하게 이용할 수 없고, RISC를 사용하는 의미가 없어진다.

출처 : 네이버 백과사전 검색


마무리할 시간...^^

글솜씨도 없는 공돌이가 이런 글을 쓰다보니 주절주절 거리는게 참 많다. 정확히 요점을 전달했는지, 알기 쉽게 썼는지 몇번이고 다시 읽어보긴 하지만, 읽을 때 마다 고칠 부분이 나오는건 왜 그런지 모르겠다. 공대생의 비애인가... ㅠㅠ

필자가 공부할 때는 정말 스스로 재미가 있어서 공부를 했는데, 요즘에는 그런 사람들이 드물다 보니, 프로그래밍 조차도 국영수 보듯이 하는 사람들이 늘어가고 있는게 참 마음이 아프다.
단지 늘어만 가는 공부를 하는 것 이상으로 생각의 폭도 조금씩 넓혀갔으면 좋겠다. 문법이다 예제다 해서 한번에 모아서 달달 외우는 것이 아니라, 이렇게 필자의 글을 읽어보면서 프로그래밍에 대한 생각의 여유를 1 byte 씩만이라도 늘릴 수 있었으면 한다.  <끝>


 

'develop' 카테고리의 다른 글

논리적 오류 해결하기  (0) 2004.05.05
독학인가, 강습인가  (0) 2004.04.27
어떤 언어를 선택해야 하나  (2) 2004.01.08
TC, BC, VC의 차이점  (1) 2004.01.08
구구단을 무시하지 마라  (0) 2004.01.06

댓글