. GetLine 최적화

여러 함수들이 GetLine 호출 대신 pLine 배열에서 줄 정보를 구함으로써 프로그램의 속도를 획기적으로 개선시켰다. 소스 전체를 뒤져 GetLine 호출문을 제거하고 대신 미리 계산된 정보인 pLine 배열을 사용하도록 했지만 정작 가장 중요한 함수는 여전히 비효율적으로 동작하고 있다. GetLine 함수는 모든 함수가 참조할 수 있는 pLine 배열을 작성하는 함수이지만 정작 자기 자신도 pLine을 사용할 수 있다는 사실을 모르고 있는 것이다.

첫 번째 줄의 시작위치는 계산할 필요도 없이 항상 버퍼의 처음인 0이다. 두 번째 줄의 시작은 첫 번째 줄의 끝으로부터 쉽게 구할 수 있다. 만약 첫 번째 줄이 자동개행되었다면 두 번째 줄의 시작은 첫 번째 줄의 끝과 같고 개행코드로 강제 개행되었다면 첫 번째 줄의 끝에서 개행코드만 건너뛰면 두 번째 줄의 시작이 된다. 이 점에 착안한다면 GetLine 함수가 조사할 줄의 시작을 찾기 위해 일일이 GetLineSub 함수를 호출할 이유가 없어지는 것이다.

줄의 시작은 항상 그 이전 줄의 끝과 같거나 아니면 두 칸 다음이다. , 첫 번째 줄은 이전 줄이 없으므로 항상 0이 시작이다. GetLineSub 서브함수가 필요한 이유는 GetLine이 목표줄의 시작과 끝을 찾기 위해 동일한 코드를 두 번 실행해야 하기 때문이었다. 그러나 이제 다른 방법으로 시작을 찾을 수 있게 되었으므로 GetLineSub GetLine으로 통합해도 코드가 중복되지 않는다. GetLine 함수와 GetLineSub를 통합하여 다시 GetLine 함수를 작성해보도록 하자.

 

void GetLine(int Line, int &s, int &e)

{

     TCHAR *p;

     int len, acwidth;

     RECT crt;

     TCHAR *EndPos=NULL;

     TCHAR *EndPosNoSpace=NULL;

     BOOL IsPrevDBCS=FALSE;

 

     if (Line == 0) {

          p=buf;

          s=0;

     } else {

          p=buf+pLine[Line-1].End;

          if (*p == 0) {

               s=-1;

               e=-1;

               return;

          }

 

          if (*p == ‘\r’) {

               p+=2;

          }

 

          s=p-buf;

     }

 

     GetClientRect(hWndMain,&crt);

     if (nWrap == 0) {

          while (*p != ‘\r’ && *p != 0)

               p++;

     } else {

          for (acwidth=0;;) {

               if (*p == ‘\t’) {

                   len=1;

                   acwidth =(acwidth/TabSize+1)*TabSize;

                   EndPos=p;

               } else {

                   if (IsDBCS(p-buf)) {

                        len=2;

 

                        if (nWrap==2 || (nWrap==3 && IsPrevDBCS==FALSE)) {

                             EndPos=p;

                             if (*EndPos != ‘ ‘) {

                                 EndPosNoSpace=EndPos;

                             }

                        }

                        IsPrevDBCS=TRUE;

                   } else {

                        len=1;

 

                        if (IsPrevDBCS==TRUE) {

                             EndPos=p;

                             if (*EndPos != ‘ ‘) {

                                  EndPosNoSpace=EndPos;

                             }

                        }

                        IsPrevDBCS=FALSE;

                   }

                   acwidth+=GetCharWidth(p,len);

               }

 

               if (*p == ‘\r’ || *p == 0) {

                   EndPos=p;

                   break;

               }

 

               if (acwidth > crt.right-2) {

                   break;

               }

 

               if (*p == ‘ ‘ || *p==‘\t’) {

                    EndPos=p+1;

                   if (*EndPos != ‘ ‘) {

                        EndPosNoSpace=EndPos;

                   }

               }

 

               p+=len;

          }

     }

 

     if (nWrap == 1 || EndPos == NULL) {

          p=p;

     } else {

          p=EndPos;

     }

 

     if (bNoFirstSpace && *p==‘ ‘ && EndPosNoSpace!=NULL) {

          p=EndPosNoSpace;

     }

     e=p-buf;

}

 

Line 0이면 그 줄의 시작은 더 볼 것도 없이 0이다. 그렇지 않다면 이전 줄의 끝인 pLine[Line-1].End를 그 줄의 시작으로 일단 계산한다. 이때 문서의 끝인지를 점검하여 이 줄이 과연 존재하는지를 확인해야 한다. 만약 이전 줄의 끝이 NULL이면 이 줄은 존재하지 않는 잘못된 줄이므로 s e에 모두 -1을 대입하고 리턴한다.

이전 줄의 끝이 NULL이 아니면 개행코드인지 본다. 개행코드라면 이전 줄의 끝에서 두 칸 건너뛰어 현재 찾고자 하는 줄의 시작점을 찾는다. 이때는 이 위치의 코드가 NULL인지 점검할 필요가 없다. 왜냐하면 설사 찾고자 하는 줄의 시작이 문서의 끝이더라도 이 줄은 엄연히 존재하는 줄이기 때문이다. 비록 아무 내용도 가지고 있지 않은 빈 줄이지만 말이다. 목표줄의 시작점을 이전 줄의 끝점을 참고하여 바로 구해내었으며 루프를 돌지도 않았고 간단한 연산문만 사용했으므로 엄청난 시간을 절약할 수 있게 되었다.

시작점을 찾은 후에 목표줄의 끝점을 찾는데, GetClientRect이하의 코드는 원래 GetLineSub에 있던 코드들을 그대로 가져온 것이다. 길이만 길어 보였지 앞에서 다 연구해 본 코드들이되 이제 리턴값을 리턴할 필요가 없으므로 ret 변수가 사라졌다는 점만 다를 뿐 논리는 동일하다. GetLineSub GetLine으로 통합되었으므로 지금부터는 필요가 없다. 그 동안 문장 끝을 일일이 찾아 내느라 수고 많이 했지만 삭제하자. 이 실습에서 가장 복잡했으며 최초로 삭제되는 추억의 함수가 되었다.

여기까지 작성한 후 속도 점검을 다시 해보면 다른 테스트는 별로 나아진 점이 없으나 윈도우 크기를 변경하는 4번 테스트는 3.68초에서 0.0037초로 빨라졌고 문자를 삽입하는 6번 테스트는 7.63초에서 0.23초로 시간이 대폭 단축되었다. 이제 모든 테스트가 0.2초 미만에 끝날 정도로 속도가 많이 향상된 것이다. 크기 변경시나 문자열을 삽입할 때마다 정렬정보를 새로 만드는데 GetLine이 훨씬 더 효율적으로 동작하기 때문에 재정렬 속도가 많이 향상되었다.

수정하기 전의 GetLine 함수는 n줄의 정보를 얻기 위해 n번의 정렬을 했었지만 수정 후에는 n줄이든 m줄이든 딱 한 번만 정렬한다는 점이 달라졌다. 따라서 총 n줄의 문서 전체를 정렬할 때의 정렬 횟수가 n!에서 n으로 줄어들었다. 이것이 가능한 이유는 n줄이 정렬되기 전에 n-1번째 줄이 항상 정렬되어 있거나 아니면 n 0이라는 전제가 있기 때문이다. n!번과 n번의 차이는 문서의 크기가 커질수록 더 심하게 벌어진다.