. 속도 측정

ApiEdit5까지 만들어 오면서 자동개행, 스크롤, 블록선택, 클립보드 처리 기능까지 작성해 넣었다. 아직 파일 입출력 기능은 없지만 이제 텍스트를 그럭저럭 편집할 수 있는 수준에까지 이르렀으며 편집 컨트롤로는 최소한의 기본 기능을 갖추게 되었다. 이 즈음에서 기능 확장은 잠시 접어두고 프로그램의 속도를 점검해보고 속도 향상을 위한 약간의 손질과 앞으로의 확장을 위한 프로그램 구조 재정비를 해보도록 하자. 일종의 중간 평가 같은 것이다.

구조를 좀 뜯어 고칠 예정이므로 ApiEdit5 프로젝트를 복사하여 ApiEdit6이라는 새로운 프로젝트를 만든다. 이 예제로 속도 측정을 해보고 어디에 무슨 문제가 있는지 보도록 할 것이다. 테스트의 편의를 위해 이번에도 몇 가지 장치를 먼저 준비하자. 우선 지금까지 사용해 왔던 300*200의 좁은 작업영역을 대폭 확대한다.

 

     hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW | WS_VSCROLL,

          CW_USEDEFAULT,CW_USEDEFAULT,500,400,

          NULL,(HMENU)NULL,hInstance,NULL);

 

WinMain CreateWindow를 수정하여 윈도우의 크기를 500*400으로 늘려 주었다. 속도를 제대로 측정해보려면 적당한 넓이의 작업영역을 가져야 하는데 500*400은 텍스트 편집에 크게 불편하지 않은 일반적인 크기이다. 그리고 제대로 속도 점검을 하기 위해 상당히 긴 문장을 미리 입력해두기로 한다. 물론 실전에서 사용되는 텍스트에 비해서는 별로 길지 않은 문장이지만 말이다. CD-ROM ApiEdit6 폴더에 있는 5645 바이트 크기의 sample.txt 파일을 실습폴더로 복사한 후 OnCreate에서 이 파일을 buf로 읽어들인다.

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     HDC hdc;

     TEXTMETRIC tm;

     int i;

 

     MySetImeMode(hWnd,TRUE);

     bComp=FALSE;

     buf=(TCHAR *)malloc(65536);

     memset(buf,0,65536);

    HANDLE hFile;

    DWORD dwRead;

    hFile=CreateFile("sample.txt",GENERIC_READ,0,NULL,

        OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

    if (hFile!=INVALID_HANDLE_VALUE) {

        ReadFile(hFile,buf,65536,&dwRead,NULL);

        CloseHandle(hFile);

    }

     ....

 

이제 프로그램을 실행한 후 잠시(?) 기다리면 이 파일의 내용이 읽혀지고 화면에 정렬되어 나타날 것이다. 지금까지 손으로 직접 입력하던 텍스트에 비해서는 상당히 긴 길이를 가지고 있기 때문에 처음 읽어들이고 정렬하는데 상당한 시간이 필요하므로 인내심을 가지고 조금 기다리도록 하자. 이 상태에서 한 번 실행해보고 편집해보아라. 직접 해보면 알겠지만 새로운 문장 입력이나 스크롤 등이 무척 느리며 윈도우 크기라도 변경하면 심하게 느려진다. 이정도 크기의 문장이라면 파일 크기로 따져서 별로 큰 편도 아닌데 벌써 속도상의 문제가 확연하게 드러났다. 편집 문장이 20KB정도만 되면 아예 편집이 불가능할 정도다.

그렇다면 이 프로그램이 얼마나 느린지 좀 더 정밀하게 테스트를 해보기 위해 속도 측정 함수를 작성해보고 각 기능별로 얼마만큼의 시간을 요구하는지 점검해보자. 다음 속도 측정 함수를 추가하되 임시 함수이므로 굳이 원형까지 선언할 필요는 없고 OnKey 함수 앞에 그냥 적당히 입력하자.

 

void SpeedTest(int type)

{

     int i;

     LARGE_INTEGER st,ed,freq;

     double gap;

     RECT crt;

     int x,y;

     TCHAR Mes[256];

     TCHAR sType[6][32]={"10번 새로 그리기","10번 내려가기", "20번 스크롤",

          "10번 크기 변경","임의 위치로 30번 이동","알파벳 입력"};

 

     wsprintf(Mes, "%s 테스트 중...",sType[type]);

     SetWindowText(hWndMain,Mes);

 

     GetClientRect(hWndMain,&crt);

     QueryPerformanceCounter(&st);

 

     switch (type)

     {

     case 0:

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

               InvalidateRect(hWndMain,NULL,TRUE);

               UpdateWindow(hWndMain);

          }

          break;

     case 1:

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

               SendMessage(hWndMain,WM_KEYDOWN,VK_DOWN,0);

               UpdateWindow(hWndMain);

          }

          break;

     case 2:

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

               SendMessage(hWndMain, WM_VSCROLL, SB_LINEDOWN, 0L);

               UpdateWindow(hWndMain);

               SendMessage(hWndMain, WM_VSCROLL, SB_LINEUP, 0L);

               UpdateWindow(hWndMain);

          }

          break;

     case 3:

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

               MoveWindow(hWndMain,10,10,300,200,TRUE);

               UpdateWindow(hWndMain);

               MoveWindow(hWndMain,10,10,500,400,TRUE);

               UpdateWindow(hWndMain);

          }

          break;

     case 4:

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

               x=rand()%crt.right;

               y=rand()%crt.bottom;

               SendMessage(hWndMain, WM_LBUTTONDOWN, 0, MAKELPARAM(x,y));

               SendMessage(hWndMain, WM_LBUTTONUP, 0, MAKELPARAM(x,y));

          }

          break;

     case 5:

          for (i=‘a’;i<=‘z’;i++) {

               SendMessage(hWndMain,WM_CHAR,(WPARAM)i,MAKELPARAM(1,0));

               UpdateWindow(hWndMain);

          }

          break;

     }

 

     QueryPerformanceCounter(&ed);

     QueryPerformanceFrequency(&freq);

     gap=((double)(ed.QuadPart-st.QuadPart))/((double)freq.QuadPart);

     sprintf(Mes, "%s 테스트에 %.4f초 경과했습니다.",sType[type],gap);

 

     SetWindowText(hWndMain,Mes);

}

 

새로 그리기, 스크롤, 크기 변경, 문자열 입력 등 여섯 가지 테스트 항목을 준비하고 각 동작에 소요된 시간을 측정하여 타이틀바에 결과를 출력하도록 하였다. 메시지박스로 결과를 보여주는 것이 더 깔끔하기는 하지만 매번 OK 버튼을 클릭해야 하는 점이 불편해서 타이틀바에 결과를 보이도록 하였다. 타이틀바는 바로바로 업데이트되기 때문에 개발중에 간단한 디버깅 정보를 확인해보는 용도로는 아주 제격이다.

정밀한 속도 측정을 위해 운영체제의 고해상도 타이머를 사용했는데 이 내용에 대해서는 API 정복 25장에서 자세하게 다루고 있으므로 참고하기 바란다. 각 작업을 시작할 때는 시작 시간을 미리 구해놓고 작업을 완료한 후 다시 시간을 측정하여 두 시간 사이의 간격을 구해 출력하도록 하였다. 실수 연산 결과를 출력해야 하므로 wsprintf는 사용할 수 없으며 sprintf를 사용해야 한다. 정밀한 측정을 위해 소수점 이하 4자리까지 출력하도록 하였다.

속도를 측정하는 함수를 만들기는 했는데 이 함수를 호출할 수 있는 방법이 있어야 하므로 임시적으로 펑션키에 기능을 할당하도록 해보자. 임시 기능을 위해 메뉴나 버튼 따위를 만드는 것은 번거로우므로 당장 쓰지 않는 키를 사용하기로 한다. OnKey에 다음 임시 코드를 삽입하여 F1을 누르면 0번 테스트, F2를 누르면 1번 테스트를 하도록 하였다.

 

void OnKey(HWND hWnd, UINT vk, BOOL fDown, int cRepeat, UINT flags)

{

     ....

     switch (vk)

     {

     case VK_F1:

          SpeedTest(0);

          return;

     case VK_F2:

          SpeedTest(1);

          return;

     case VK_F3:

          SpeedTest(2);

          return;

     case VK_F4:

          SpeedTest(3);

          return;

     case VK_F5:

          SpeedTest(4);

          return;

     case VK_F6:

          SpeedTest(5);

          return;

 

이 코드들은 SpeedTest를 호출하기 위한 임시 코드이므로 최적화가 끝난 후에는 삭제할 것이다. 이제 프로그램을 실행해놓고 펑션키를 차례로 눌러 실제로 속도 테스트를 해보자. 시스템의 속도와 운영체제의 환경에 따라 결과는 달라지겠지만 P3-600노트북에서 측정한 결과는 다음과 같다. 시간 단위는 초이며 소수점 이하 2자리까지만 표기하였다.

 

테스트

1

2

3

4

5

6

결과()

14.29

15.4

28.89

37.29

46.52

78.03

 

보다시피 엄청난 시간이 걸린다. CPU 10억 분의 1초라는 엄청난 고속으로 동작하는데 이 프로그램은 왜 고속의 CPU를 쓰고도 속도가 저 모양인가? 편집기가 어떤 동작을 하는데 1초를 넘긴다는 것은 심각하게 느린 것이며 더구나 10초 단위를 넘어간다는 것은 아주 몹쓸 프로그램임을 증명하는 것이다. 심지어 1분을 넘어가는 테스트 항목도 있다. 라는 단위는 컴퓨터에게 있어서는 장구한 세월과 같다.

그렇다면 도대체 왜 저렇게 느리며 그 원인은 무엇일까? 해답은 간단하다. 개발자가 프로그램을 대충대충 작성했기 때문이다. 논리가 나름대로 복잡해서 안 그래도 머리가 아파 죽겠는데 속도까지 빠르게 만들려고 하면 무척 피곤하기 때문이다. 그래서 앞으로 점점 예제를 수정해 가면서 문제점을 알아 보고 속도를 끌어올리는 방법에 대해 연구해 볼 것이다.

기능 추가도 없이 코드의 여기 저기를 뜯어 고쳐야 하기 때문에 이 실습은 무척 번거롭고 귀찮을 수도 있다. 하지만 코드를 조금씩 수정할 때마다 프로그램의 속도가 개선되는 것을 지켜 보면 나름대로 재미는 있을 것이다. 또한 그다지 정밀하지는 않지만 속도를 향상시킬 수 있는 일반적인 방법들에 대해서도 살펴볼 수 있는 색다른 경험을 하게 될 것이다.