. 버퍼 길이 유지

버퍼의 크기가 커지면 문자를 삽입할 때마다 대용량을 이동시켜야 하기 때문에 시간이 많이 걸린다. ApiEdit는 단일 버퍼 방식을 채택하고 있으므로 이 시간은 버퍼의 구조를 바꾸지 않는 한은 어쩔 수 없는 것이며 애초에 감수하기로 작정을 했다. 그러나 속도 점검을 해보면 문자 삽입 속도가 느린 이유가 반드시 메모리 이동 때문만은 아니라는 것을 알 수 있다. 다음 코드를 통해 실행 시간을 측정해보자.

 

void Insert(int nPos, TCHAR *str)

{

     ....

     STARTQ

     len=lstrlen(str);

     if (len==0) return;

     movelen=lstrlen(buf+nPos)+1;

     SPOT1

     memmove(buf+nPos+len,buf+nPos,movelen);

     SPOT2

     memcpy(buf+nPos,str,len);

     SPOT3

     bLineEnd=FALSE;

     UpdateLineInfo();

     UpdateScrollInfo();

     ENDQ

     ....

 

문자를 실제로 버퍼에 삽입하는 함수인데 중간중간에 속도 측정 매크로를 삽입하여 각 동작 별로 얼마만큼 시간을 소모하는지 측정해보았다. SPOT1은 이동할 길이를 계산하는 동작의 시간이고 SPOT2는 실제로 메모리를 이동하는 동작까지의 시간이며 SPOT3은 이동된 메모리에 문자열을 삽입한 후의 시간이며 ENDQ는 재정렬과 스크롤 정보 갱신까지 걸린 시간이다. 측정 결과는 다음과 같다.

 

SPOT1

SPOT2

SPOT3

ENDQ

0.051

0.090

0.090

2.79

 

정렬은 일단 제외하고 5MB라는 큰 메모리를 다루는데 0.1초도 걸리지 않았으니 생각보다 속도가 양호한 편이다. 그런데 실제 메모리 이동 시간도 많이 걸리지만 이동할 길이를 계산하는데도 만만치 않는 시간이 걸렸다. 길이 계산에 0.051, 메모리 이동에 0.039초가 걸린 셈이다. 실제 삽입시에는 Insert에서 메모리 오버런을 점검하기 위해 길이를 한 번 더 측정하고 있기 때문에 길이 계산 두 번에 0.102, 이동에 0.039초가 걸려 총 소요 시간은 0.141초가 된다. 오히려 메모리 이동보다 길이 계산에 더 많은 시간을 낭비하고 있다.

길이 계산이 이렇게 느린 이유는 lstrlen 함수가 버퍼의 끝을 찾기 위해서는 처음부터 끝까지 NULL문자를 찾아 스캔해야 하기 때문이다. 버퍼 끝을 찾기 위한 다른 뾰족한 방법이 있을 리가 없다. 그저 첫 바이트부터 끝까지 버퍼를 읽으면서 NULL이지? 아니야? 그럼 다음. NULL이지? ! 아니네. 그럼 다음....을 반복해야 하는 것이다. 이런 식이니 끝을 찾는데 시간이 걸릴 수밖에 없다.

사실 lstrlen도 기계어 코드로 동작하기 때문에 굉장히 빠르게 동작하지만 길이가 5백만 바이트쯤 되면 제아무리 빨라도 한계가 있을 수밖에 없다. 버퍼의 길이는 예제의 곳곳에서 필요로 하는데 어떤 부분이 버퍼의 길이를 계산하는지 점검해보자. lstrlen(buf)로 검색해보면 되는데 모두 여덟 군데가 있다.

 

장소

목적

VK_RIGHT

오른쪽으로 이동할 있는지 본다. 문서 끝이면 이동 불가

VK_END

Ctrl+End입력에 대해 문서 끝으로 이동한다.

IDM_AE_SELALL

문서 전체를 선택하기 위해 SelEnd 문서 위치로 맞춘다.

GetNowWord

단어 끝이 문서 끝인지 본다.

Insert

버퍼의 실제 길이가 할당된 길이보다 큰지 검사한다.

Insert

이동시킬 메모리의 길이를 계산한다.

Delete

삭제할 곳이 문서 내인지 검사한다.

Delete

이동시킬 메모리의 길이를 계산한다.

 

이중 앞쪽 네 경우는 버퍼 길이를 실시간으로 구해 사용하더라도 크게 문제되지 않는다. 왜냐하면 명령이 반복적으로 일어나는 것이 아니고 어쩌다 한 번씩이고 그것도 사람의 손을 통해 입력되는 명령이기 때문이다. 문서 전체를 선택하는 일은 자주 일어나는 일이 아니며 좀 느려도 문제될 것이 없다.

그러나 뒤쪽 네 경우는 문서를 편집할 때 수시로 호출되며 그것도 반복적으로 여러 번 호출될 수 있다. 이때마다 문서의 길이를 실시간으로 구한다면 실행시간이 눈에 띄게 느려지게 될 것이다. 그렇다고 해서 버퍼 오버런을 점검하지 않을 수 없고 이동 길이도 모르고 메모리를 이동시킬 수는 없다. 해결책은 문서의 길이를 실시간으로 구하지 말고 항상 미리 계산해놓는 것이다. 이런 목적으로 다음 변수를 선언한다.

 

int doclen;

 

이 변수에 문서의 길이를 항상 유지할 것이다. 최초 문서 길이는 0이므로 이 변수는 OnCreate에서 0으로 초기화한다.

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     HDC hdc;

     TEXTMETRIC tm;

 

     MySetImeMode(hWnd,TRUE);

     buflen=1024;

     buf=(TCHAR *)malloc(buflen);

     memset(buf,0,buflen);

    doclen=0;

     ....

 

이후 문서에 문자열이 삽입, 삭제될 때마다 이 변수에 저장된 문서 길이를 수정할 것이다. 그런데 이 예제의 경우 doclen을 초기화할 곳이 한 군데 더 있다. VK_F8 임시 코드에서 문서를 읽기 전에도 문서 길이는 0으로 리셋되어야 한다. 물론 테스트가 끝난 후에는 삭제될 코드이지만 일단 필요하므로 넣어주도록 하자.

 

     case VK_F8:

              ....

              lstrcpy(buf,"");

           doclen=0;

              Insert(0,TextBuf);

              free(TextBuf);

 

              SetCaret();

              Invalidate(-1);

 

doclen 변수는 Insert에서 증가하며 Delete에서 감소한다. 문자열을 삽입한 만큼 그 길이를 더하고 삭제된 만큼 길이를 빼주므로 doclen은 항상 문서의 길이를 정확하게 유지할 것이다.

 

void Insert(int nPos, TCHAR *str)

{

     ....

     doclen+=len;

}

 

void Delete(int nPos, int nCount)

{

     ....

     doclen-=nCount;

}

 

이제 lstrlen(buf)가 사용된 모든 곳을 찾아 doclen 변수로 대체한다. 찾기/바꾸기 기능으로 단순 대체하면 된다. , Insert, Delete는 버퍼의 길이를 직접 필요로 하는 것이 아니라 메모리 이동 길이를 계산하므로 단순 대체가 아니라 약간의 수학식이 필요하다.

 

void Insert(int nPos, TCHAR *str)

{

     ....

    needlen=doclen+lstrlen(str)+1;

     if (needlen > buflen) {

          buflen = needlen+1024;

          buf=(TCHAR *)realloc(buf,buflen);

          if (buf == NULL) {

          }

     }

 

     STARTQ

     len=lstrlen(str);

     if (len==0) return;

    movelen=doclen-nPos+1;

     SPOT1

     ....

 

void Delete(int nPos, int nCount)

{

     ....

     if (nCount == 0) return;

    if (doclen < nPos+nCount) return;

     ....

    movelen=doclen-nPos-nCount+1;

     memmove(buf+nPos, buf+nPos+nCount, movelen);

     UpdateLineInfo();

     UpdateScrollInfo();

     doclen-=nCount;

}

 

이제 버퍼 길이는 실시간으로 구하지 않고 변수를 참조하는 것으로 간단하게 구할 수 있다. 다시 속도 점검을 해보면 SPOT10.000001, SPOT2=0.040초가 걸린다. 버퍼 길이를 계산하는 시간이 백만 분의 1초가 되었는데 이 시간은 그냥 0이라고 생각해도 무방한 정도다. 문자 삽입 시간이 0.14초에서 0.04초로 단축되어 3배 이상 빨라졌다.

이 최적화에 의한 차이는 사실 굉장히 미세하기 때문에 매크로로 측정해보지 않는 한 속도 향상을 체감하는 것은 불가능하다. 삽입 속도가 빨라져도 정렬루틴이 시간을 다 까먹고 있기 때문이다. 하지만 분명히 효과는 있고 이런 효과들이 모여 전체적으로 빠른 프로그램을 만들어 내는 것이다. 최적화가 끝났으므로 Insert에 작성한 속도 측정 매크로는 이제 삭제하도록 하자. 물론 그냥 내버려둬도 별 지장은 없다.