. 무한 메모리

ApiEdit는 초기화시 64KB의 고정 길이 버퍼를 할당하며 이 버퍼에서 텍스트를 편집한다. 버퍼가 애초부터 고정된 길이의 배열로 선언되었기 때문에 이 이상의 텍스트는 편집할 수 없다. 이 정도 길이면 지금까지의 예제 수준에서 기능들을 테스트해보기에는 충분한 용량이지만 실제 사용하기에는 턱없이 부족하다. 64KB라면 조금 긴 SQL 스크립트나 HTML 홈 페이지 문서를 편집하기에도 부족한 수준이고 C 소스라면 불과 몇 줄 되지도 않는다.

그렇다면 버퍼의 크기를 늘려야 하는데 얼마 정도로 늘리면 충분할까? 10배 늘려 640KB로 하면 그럭저럭 쓸만한 용량이 될 것이고 대폭 확장하여 10MB 정도라면 아주 넉넉할 것이다. 그러나 버퍼의 크기를 얼마로 늘리더라도 항상 부족할 수 있기 때문에 고정된 크기로 늘리는 것은 별 소용이 없다. 256MB 정도로 늘리면 물론 부족할 리야 없겠지만 이 정도 메모리를 내 줄 수 있는 PC가 흔하겠는가?

ApiEdit의 문제점은 버퍼가 작다는 것이 아니라 고정된 크기를 가진다는 점이다. 편집할 텍스트가 커지면 메모리 사용량이 많아지는 것은 당연하므로 텍스트의 크기에 맞게 버퍼의 크기도 동적으로 관리할 수 있다면 가장 이상적이다. 문서의 크기에 맞추어 사용 메모리를 늘려 간다면 이론적으로 버퍼의 길이는 무한대가 된다. 이렇게 메모리를 관리하는데 PC의 메모리가 부족한 상황이 된다면 이는 프로그램으로서는 어쩔 수 없는 것이다. 프로그램이 주어진 물리적 한계까지 책임질 필요는 없으며 있는 메모리만 잘 활용할 수 있도록 작성하면 된다.

동적으로 관리해야 할 주 대상은 텍스트 버퍼인 buf이지만 이외에 줄 정보 배열인 pLine도 관리 대상이다. 이 배열의 크기는 10000으로 초기화되므로 만 줄 이상 편집은 불가능하다. 문서의 길이가 길어지면 pLine 배열도 문서 길이에 맞게 같이 늘어나야 한다. 그렇다고 해서 북마크 배열인 arMark까지 동적으로 관리할 필요는 없다. ApiEdit는 애초에 스팩을 잡을 때 북마크는 문서당 100개 이하로 결정했기 때문이다.

버퍼 관리를 위해 다음 두 변수를 선언한다. 이름에서 알 수 있듯이 buflen은 텍스트 편집 버퍼의 크기이며 Linelen은 줄 정보 배열의 크기이다. int형으로 선언했으므로 두 버퍼의 이론적인 최대 크기는 2G가 된다.

 

int buflen;

int Linelen;

 

OnCreate에서 이 두 변수를 초기화함과 동시에 이 길이만큼 버퍼를 초기 할당하였다. 고정 길이의 할당문을 삭제하고 buflen Linelen 값만큼 할당하도록 수정한다.

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     buflen=1024;

     buf=(TCHAR *)malloc(buflen);

     memset(buf,0,buflen);

     Linelen=100;

     pLine=(tagLine *)malloc(sizeof(tagLine)*Linelen);

     for (i=0;i<Linelen;i++) {

          pLine[i].Start=-1;

     }

 

버퍼의 초기값은 1024(1KB)로 잡았고 buf도 이 길이에 맞게 할당했다. 어차피 부족하면 늘려줄 것이므로 처음부터 큰 버퍼를 잡을 필요가 없다. ApiEdit는 컨트롤이고 고작 사람 이름 정도를 입력받는 용도로도 사용할 수 있으며 한 윈도우에 수십 개가 생성될 수도 있으므로 처음부터 큰 버퍼를 가지는 것은 오히려 바람직하지 않다. 이런 면에서 본다면 1024바이트도 결코 작은 용량이 아니다. 줄 정보 배열도 초기값은 100줄로 축소되었다.

ApiEdit는 초기에 할당된 1024 크기의 버퍼에서 텍스트 입력을 받는다. 그러다가 텍스트 길이가 버퍼보다 더 커지면 그때 버퍼를 늘리는데 이 작업이 필요한 유일한 시점은 텍스트가 삽입되는 Insert 함수이다. 버퍼에 텍스트를 추가하는 모든 함수는 Insert를 통하도록 되어 있으므로 이 함수에서만 버퍼를 관리하면 된다.

 

void Insert(int nPos, TCHAR *str)

{

    int needlen;

 

     if (bReadOnly)

          return;

 

    needlen=lstrlen(buf)+lstrlen(str)+1;

    if (needlen > buflen) {

        buflen = needlen+1024;

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

        if (buf == NULL) {

        }

    }

 

현재 버퍼의 길이와 삽입될 문자열의 길이를 더해 필요한 메모리 양을 계산한다. 여기에 1을 더하는 이유는 널 종료문자의 공간을 보존하기 위해서이다. 널 종료문자는 반드시 버퍼 끝에 있어야 하므로 이 문자가 삽입되는 문자열에 의해 덮여서는 안된다. 예를 들어 10바이트가 할당되어 있고 그 중 7바이트의 문자가 들어 있다고 하자. 이때 3바이트가 삽입된다면 필요한 길이는 7+3+1 11바이트가 된다. n문자를 기억하기 위한 버퍼의 크기는 최소한 n+1이 되어야 하므로 필요한 메모리량에 널 종료문자분은 반드시 포함해야 한다.

이렇게 계산된 메모리 필요량이 할당되어 있는 버퍼 길이인 buflen보다 더 크다면 이때가 바로 버퍼를 늘려줄 때이다. 버퍼의 크기를 늘리는 방법은 아주 간단하다. realloc 함수는 지정한 크기만큼 메모리를 재할당할 뿐만 아니라 기존 버퍼 내용을 새로 할당된 곳으로 복사하므로 버퍼의 내용 자체는 관리하지 않아도 된다.

이때 꼭 필요한 만큼인 needlen만큼 크기를 늘리는 것이 아니라 여기에 1024정도의 여유를 더 준다. 이 여유분을 주지 않으면 한글자 입력할 때마다 버퍼 재할당이 발생하므로 입력 속도가 느려지게 될 것이다. 버퍼가 부족해 재할당되면 조만간 다시 재할당될 것임을 예상하고 미리 필요량보다 더 많이 할당해놓는 것이다. 버퍼를 재할당한 후는 에러 처리가 필요한데 이 부분은 일단 준비만 해두고 공란으로 남겨 두었다. Win32 환경에서 메모리 할당 함수는 거의 실패하지 않는다. 줄 버퍼 관리는 pLine 배열을 만드는 UpdateLineInfo에서 한다.

 

void UpdateLineInfo()

{

     int l,s,e;

     int nPara, nLine=0;

    int i;

 

     for (l=0, nPara=0;;l++) {

        if (l >= Linelen) {

           Linelen += 1000;

           pLine=(tagLine *)realloc(pLine,sizeof(tagLine)*Linelen);

           if (pLine == NULL) {

           }

           for (i=Linelen-1000;i<Linelen;i++) {

               pLine[i].Start=-1;

           }

        }

          GetLine(l,s,e);

          ....

 

방식은 동일하다. 정렬하다가 할당해놓은 배열크기를 넘어설 때 배열을 늘린다. 이때도 1000줄 정도의 여유분을 주어 재할당이 너무 자주 일어나지 않도록 해야 한다. 새로 할당된 pLine Start -1로 초기화한다. 이 두 함수에 의해 텍스트 버퍼와 줄 정보 배열은 문서 크기에 맞추어 버퍼를 늘려 나가므로 메모리가 허락하는 한 아무리 큰 문서라도 읽어들일 수 있다. 하지만 한 번 늘어난 메모리를 굳이 줄일 필요는 없다. 메모리가 부족한 것은 문제가 되지만 남는 것은 문제가 되지 않으며 메모리 축소를 위한 여분의 조건 점검과 재할당은 오히려 성능을 떨어뜨리기 때문이다. 축소가 반드시 필요한 상황은 확률적으로 흔하지 않다.

여기까지 작업한 후 긴 문자열을 입력해보자. 아무리 긴 문자열을 입력해도 속도가 좀 느려질 뿐 ApiEdit가 죽는다거나 하지는 않을 것이다. 아직 파일을 읽지는 못하지만 클립보드를 통해 붙여넣기가 가능하므로 긴 문서를 복사해서 여러 번 붙여 보면 된다. 무한대의 메모리를 지원하는 것이 생각보다 싱겁게 완료된 것 같은데 이는 ApiEdit가 메모리 기반의 단일 버퍼 구조를 가지고 있고 메모리를 소모하는 곳이 한 함수로 집중되어 있기 때문이다.