편견이 깨지는 어셈블리 프로그래밍 : 최적화 강좌 3 - 2  
 
 


배열대신 포인터를!
반복분기 작업이 필요하도록 만드는 것 중 하나가 배열일 것이다. 배열로 인해 고급 언어들은 프로그래머가 여러 개의 데이터를 처리하기 편하도록 만들고 있다. 하지만 조금이라도 속도를 늘리기를 원한다면 우리는 코드를 작성하는 손을 조금 더 번거롭게 해야 할 필요가 있다.

<리스트 4>와 <리스트 5>는 정수 3개의 요소가 들은 12바이트 크기의 구조체의 배열을 제어하는 예이다. <리스트 5>는 <리스트 4>에서 구조체를 포인터를 이용해 제어하도록 변경한 것이다. 이 둘의 성능을 테스트했다(<표 3>).

??<2> <리스트 2><리스트 3>의 실행 예?? * 단위: 클럭






??<표 3> 배열 제어 성능 측정



<리스트 4> 일반적인 배열 작업
void TestDimPtr( )
{
//local ---------------------
int Tv_Wk;
//code ----------------------

for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
{
V_TestDumy2[Tv_Wk].test1 = 0;
V_TestDumy2[Tv_Wk].test2 = 1;
V_TestDumy2[Tv_Wk].test3 = 2;
}

}

<리스트 5> 포인터를 이용한 배열 작업
void TestDimPtr2( )
{
//local ---------------------
int Tv_Wk;
Ptr_TestStc Tv_WkPtr;
//code ----------------------

Tv_WkPtr = (Ptr_TestStc)&V_TestDumy2;
for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
{
Tv_WkPtr->test1 = 0;
Tv_WkPtr->test2 = 1;
Tv_WkPtr->test3 = 2;

Tv_WkPtr++;
}

}

결 과를 보고 의아해하는 독자도 있을 것이다. 코드 길이나 간결도를 봐도 <리스트 5>가 더 느려보이기 때문이다. 만약 차이가 나도 약간 나야 할 것이다. 그럼 <리스트 4>와 <리스트 5>의 디스어셈블(dis-assembly)된 코드를 보면서 이야기하자.

<리스트 6> <리스트 4>의 디스어셈블
75: for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
00402E58 mov dword ptr [ebp-4],0
00402E5F jmp TestDimPtr+2Ah (00402e6a)
00402E61 mov eax,dword ptr [ebp-4]
00402E64 add eax,1
00402E67 mov dword ptr [ebp-4],eax
00402E6A cmp dword ptr [ebp-4],64h
00402E6E jge TestDimPtr+62h (00402ea2)
76: {
77: V_TestDumy2[Tv_Wk].test1 = 0;
00402E70 mov ecx,dword ptr [ebp-4]
00402E73 imul ecx,ecx,0Ch
00402E76 mov dword ptr V_TestDumy2 (00418bb0)[ecx],0
78: V_TestDumy2[Tv_Wk].test2 = 1;
00402E80 mov edx,dword ptr [ebp-4]
00402E83 imul edx,edx,0Ch
00402E86 mov dword ptr V_TestDumy2+4 (00418bb4)[edx],1
79: V_TestDumy2[Tv_Wk].test3 = 2;
00402E90 mov eax,dword ptr [ebp-4]
00402E93 imul eax,eax,0Ch
00402E96 mov dword ptr V_TestDumy2+8 (00418bb8)[eax],2
80: }
00402EA0 jmp TestDimPtr+21h (00402e61)
81:
82: }

<리스트 7> <리스트 5>의 디스어셈블
92: Tv_WkPtr = (Ptr_TestStc)&V_TestDumy2;
00402EE8 mov dword ptr [ebp-8],offset V_TestDumy2 (00418bb0)
93: for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
00402EEF mov dword ptr [ebp-4],0
00402EF6 jmp TestDimPtr2+31h (00402f01)
00402EF8 mov eax,dword ptr [ebp-4]
00402EFB add eax,1
00402EFE mov dword ptr [ebp-4],eax
00402F01 cmp dword ptr [ebp-4],64h
00402F05 jge TestDimPtr2+5Fh (00402f2f)
94: {
95: Tv_WkPtr->test1 = 0;
00402F07 mov ecx,dword ptr [ebp-8]
00402F0A mov dword ptr [ecx],0
96: Tv_WkPtr->test2 = 1;
00402F10 mov edx,dword ptr [ebp-8]
00402F13 mov dword ptr [edx+4],1
97: Tv_WkPtr->test3 = 2;
00402F1A mov eax,dword ptr [ebp-8]
00402F1D mov dword ptr [eax+8],2
98:
99: Tv_WkPtr++;
00402F24 mov ecx,dword ptr [ebp-8]
00402F27 add ecx,0Ch
00402F2A mov dword ptr [ebp-8],ecx
100: }
00402F2D jmp TestDimPtr2+28h (00402ef8)
101:
102: }

<리스트 6>의 배열 제어하는 부분과 <리스트 7>의 구조체 제어 부분을 비교해 보자. <리스트 5>에서는 배열을 제어할 때마다 배열요소의 주소를 항상 계산하게 된다.

77: V_TestDumy2[Tv_Wk].test1 = 0;
00402E70 mov ecx,dword ptr [ebp-4] // 배열의 인덱스 가져옴
00402E73 imul ecx,ecx,0Ch // 배열주소 계산
00402E76 mov dword ptr V_TestDumy2 (00418bb0)[ecx],0// 배열제어

즉, 배열의 요소를 제어할 때마다 배열의 인덱스에 배열요소의 크기를 곱하여 제어한다는 뜻이다. 반면 <리스트 7>의 포인터를 이용한 제어의 경우 다음과 같다.

95: Tv_WkPtr->test1 = 0;
00402F07 mov ecx,dword ptr [ebp-8] // 배열의 주소 가져옴
00402F0A mov dword ptr [ecx],0 // 배열제어

이 미 제어할 배열 위치의 포인터, 즉 주소가 계산되어 있기 때문에 제어 시에는 배열의 주소를 계산할 필요가 없는 것이다. 이 이유만은 아니다. 배열 주소를 계산하기 위해서는 배열의 인덱스에 배열 요소의 크기를 곱해야 한다. 반면 포인터를 이용한 연산에서는 매번 배열의 요소 크기만큼 더해주기만 한다. 80×86 계열 CPU에서는 곱셈
명령은 덧셈 명령보다 많은 작업을 요한다. 이런 느린 명령이 여러 번 쓰이니 속도차가 크게 날 수밖에 없는 것이다. 하지만 이 중에서 몇 가지 예외는 있다. 80×86 계열 CPU가 32비트로 들어서면서 2, 4, 8의 배수의 크기를 갖는 배열의 경우 주소 연산이 필요 없이 바로 제어할 수 있는 것이다. 따라서 요소의 크기가 2, 4, 8의 배수인 배열의 경우 그대로 배열로서 제어하는 것이 더 빠르다.

GUI에서 최적화
윈 도우처럼 그래픽을 이용해 유저가 제어하도록 하는 방식을 GUI(Graphic User Interface)라 한다. 이 말은 사용자가 제어할 수 있도록 윈도우 애플리케이션은 많은 그래픽 컨트롤을 사용한다는 말이기도 하다. 이 그래픽 컨트롤들을 사용하는 방법에 있어서도 최적화가 존재한다. 그 중 한 가지가 컨트롤의 검사 후 출력 방식이다.
MFC에서 많이 쓰는 컨트롤 중 하나가 바로 CStatic이다. CStatic은 주로 결과를 표시하거나 그래픽을 표시할 때 쓰인다. 어떤 배열의 값을 출력한다고 하자. 이때 일반적으로 배열의 값을 출력할 때 <리스트 8>과 같은 방식으로 표시할 것이다. 하지만 만약 <리스트 9>와 같이 수정해 준다면 조금 더 나은 성능을 보여 줄 수 있다.

<리스트 8> 일반적인 배열 값의 CStatic 출력
void TestDimPtr2( )
{
//local ---------------------
int Tv_Wk;
char Tv_StrTmp[32];
//code ----------------------

for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
{
sprintf(Tv_StrTmp,"%d",V_TestDumy[Tv_Wk] );
m_LblValue.SetWindowText( Tv_StrTmp );
}

}



<리스트 9> 원활한 수행을 위해 코드를 추가한 예
void TestDimPtr2( )
{
//local ---------------------
int Tv_Wk;
char Tv_StrTmp[32];
char Tv_StrTmp2[32];
//code ----------------------

for(Tv_Wk = 0;Tv_Wk < 100;Tv_Wk ++)
{
sprintf(Tv_StrTmp,"%d",V_TestDumy[Tv_Wk] );
m_LblValue.GetWindowText( Tv_StrTmp2 );
if(strcmp( Tv_StrTmp, Tv_StrTmp2 ) != 0)
{
m_LblValue.SetWindowText( Tv_StrTmp );
}
}

}


< 리스트 8>보다 오히려 작업만 늘어나 버린 <리스트 9>가 더 최적화된 코드라는 것에 의아해 할 것이다. <리스트 9>는 <리스트 8>에서 스트링 내용을 비교해 출력 여부를 판단하는 작업이 추가된 것 일뿐이기 때문이다. 여기서는 윈도우의 글자 출력에 대한 부분을 먼저 알아야 한다.

윈도우는 화면에 글자를 출력하기 위해 폰트를 읽어 비디오 메모리에 출력한다. 일반 비트맵 폰트도 있지만 더욱 매끄럽게 글자를 출력하기 위해 트루 타입 폰트를 사용한다. 트루 타입 폰트는 초기 벡터 폰트(vector font)라 불리던 방식으로 그림으로 된 폰트를 화면에 복사해주던 방식과 달리 선, 면, 곡선 등을 그리는 명령을 이용해 아무리 확대해도 모양이 거칠어지지 않도록 하는 글자 출력 체계이다. 이런 복잡한 방식을 이용하기 때문에 윈도우 컨트롤의 내용을 한꺼번에 갱신하거나 하면 화면출력이 어색해지거나 CPU 부하를 많이 필요로 하는 상황이 오게 된다. 그래서 내용이 같은지 틀린지를 비교해 출력하는 것이다. 물론 중복되는 내용이 거의 없다면 <리스트 9>와 같이 비교해 출력하는 방식은 쓸데없는 코드와 속도 낭비를 하는 것일 수도 있다. 하지만 이렇게 생각해보자. 폭이 100픽셀이고 높이가 20픽셀인 윈도우 컨트롤을 갱신한다. 비트맵을 제어해 본 독자라면 알겠지만 픽셀은 하나가 메모리 하나를 차지하는 일종의 데이터이다. 이 데이터를 출력한다는 것은 100×20으로 2000개의 명령을 최소 사용한다는 말과 동일하다. 거기다 트루 타입과 같은 복잡한 연산이 들어가는 출력 방식이라면 그 이상을 필요로 할 수 있다. 저런 큰 연산을 막기 위해 30바이트 남짓한 문자열을 검사한 후 출력하는 방식이 과연 손해라 할 수 있을까?


출처: http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=381


+ Recent posts