티스토리 뷰


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


< 리스트 5>는 정렬된 메모리 블럭을 Win32 API(CopyMemory), DtCopyMemory, DtCopyMemoryMacro, 일반 복사 이렇게 네 가지 방법으로 복사를 한 후 각 방법별 소요 클럭 수를 출력한다.

<리스트 5> DummyCopy.cpp 각 함수들을 테스트하기 위한 메인 코드(정렬된 주소)

.... 생략 ....

void CopyTest (int iMaxLength)
{
.... 생략 ....
char *pSrc = new char [iMaxLength] ; // 정렬된 주소
char *pDest = new char [iMaxLength] ;
.... 생략 ....

// 일반적인 char 단위의 복사(Normal)
DtGetClockCount (&iFirstClockCount) ;
for (int i = 0 ; i < iMaxLength ; ++i)
{
*pDest = *pSrc ;
}
DtGetClockCount (&iSecondClockCount) ;
.... 생략 ....

// Win32 API (CopyMemory) 사용 복사
DtGetClockCount (&iFirstClockCount) ;
CopyMemory (pDest, pSrc, iMaxLength) ;
DtGetClockCount (&iSecondClockCount) ;
.... 생략 ....

// DtCopyMemory 함수 사용 복사
.... 생략 ....

// DtCopyMemoryMacro 함수 사용 복사
.... 생략 ....

// 결과화면 출력
printf ("Normal Char Copy (C Style) = %9d Clocks " , iNormalCharCopy) ;
printf ("Win32API CopyMemory = %9d Clocks " , iCopyMemAPICopy) ;
printf ("DtCopyMemory(TeamBase) = %9d Clocks " , iDtCopyMemCopy ) ;
printf ("DtCopyMemoryFast(TeamBase) = %9d Clocks " , iDtCopyMemCopyMacro ) ;
.... 생략 ....
}

void main ()
{
CopyTest (1024) ;
}

< 리스트 6>는 <리스트 5>와는 반대로 정렬이 안된 메모리 블럭을 Win32 API(CopyMemory), DtCopyMemory, DtCopyMemoryMacro, 일반 복사 이렇게 네 가지 방법으로 복사를 한 후에 각 방법별 소요 클럭 수를 출력한다. 코드를 살펴보면 인위적으로 메모리 포인터를 1씩 증가시킴으로써 정렬되지 않는 주소를 만들어냈다.

<리스트 6> DummyCopy.cpp 각 함수들을 테스트하기 위한 메인 코드(정렬되지 않은 주소)

.... 생략 ....

void CopyTest (int iMaxLength)
{
.... 생략 ....
char *pSrc = new char [iMaxLength] ; // 정렬된 주소
char *pDest = new char [iMaxLength] ;
.... 생략 ....

pSrc++; // 정렬을 인위적으로 어긋나게 함
pDest++;

// 일반적인 char 단위의 복사
.... 생략 ....

// Win32 API (CopyMemory) 사용 복사
.... 생략 ....

// DtCopyMemory 함수 사용 복사
.... 생략 ....

// DtCopyMemoryMacro 함수 사용 복사
.... 생략 ....

// 결과화면 출력
.... 생략 ....
}

void main ()
{
CopyTest (1024) ;
}




<그림 4> 정렬된 주소의 메모리 복사 평균 소요 클럭 수



<그림 5> 정렬이 안된 주소의 메모리 복사 평균 소요 클럭 수


<표1> 정렬된 주소의 메모리 복사 소요 클럭 수





?<1> 정렬된 주소의 메모리 복사 소요 클럭 수



<표 2> 정렬 안 된 주소의 메모리 복사 소요 클럭 수





?<표 2> 정렬 안 된 주소의 메모리 복사 소요 클럭 수



수 행시간을 측정한 결과를 보자. 일반적인 메모리 복사 방법을 사용한 함수의 수행 클럭 수와 DtCopyMemoryMacro 함수의 수행 클럭 수가 크게는 15배 정도 차이가 나는 것을 알 수 있다. 버스트 모드의 혜택을 받지 못하는 일반 메모리 복사의 경우, 버스트 모드 설명 시 언급했듯이 메모리를 읽을 때 마다 읽기위한 메모리 주소를 전송해야 하므로 버스트 모드의 혜택을 받는 Win32 CopyMemory API, DtCopyMemory, DtCopyMemoryMacro와는 현격히 다른 결과치를 보인다. CPU의 속도가 3GHz를 넘어가는 현 시점에서는 메모리의 최대 속도인 333MHz는 턱없이 느린 속도에 불과하므로 한번의 메모리 엑세스를 위해서 CPU의 입장에서는 오래 기다리게 되고 그 만큼 복사 효율이 떨어진다. 그러면 버스트 모드의 혜택을 받는 함수들 중 유일하게 다른 함수와 구별되게 최고의 효율을 보인 DtCopyMemoryMacro 함수를 살펴보도록 하자. 아마도 필자가 이러한 언급을 하지 않더라도 이미 수행시간 측정의 결과치를 본 대부분의 독자들은 이 함수에 대한 호기심을 감출 수 없을 것이다.

우선 버스트 모드의 혜택을 받는 함수간 클럭 수 차이의 원인을 보자. 앞의 측정 결과에서 Win32 CopyMemory API나 DtCopyMemory 함수가 DtCopyMemoryMacro 함수보다 2~3배 정도 속도가 느리다. 그 이유는 두 함수 공히 함수 내부에서 소스 주소와 대상 주소가 4바이트 단위로 정렬되어 있지 않은 경우, 시작 주소부터 첫 번째 4의 배수 주소까지 일반 복사를 한다. 그 이후부터 마지막 4의 배수 주소까지는 버스트 모드의 혜택을 얻어 복사하고 남은 몇 개의 바이트는 일반 복사로 채우는 코드가 들어가 있다.
함수가 이렇게 작성된 이유는 프로그래머에게 소스 주소와 대상 주소 또한 복사할 데이터 블럭 크기의 4바이트 정렬 여부에 상관없이 나름대로 합리적인 퍼포먼스를 제공하기 위한 코드라 할 수 있다. 그래서 일반적인 메모리 복사에는 Win32 API인 CopyMemory를 권장하는 것이다. 하지만 최고의 퍼포먼스와 효율을 추구하는 멀티미디어 환경에서는 이러한 미세한 차이도 엄청나게 크게 부각될 수밖에 없다.

만일 메모리 생성 시 VirtualAlloc을 사용한다면 생성되는 메모리는 4의 배수 위치에 크기가 페이지 단위(x86에서 4K = 4의 배수)로 할당되기에 이러한 메모리 블럭 간 복사에서는 CopyMemory API나 DtCopyMemory에서 구현된 버스트 모드 적용을 위한 코드가 오히려 퍼포먼스를 떨어뜨리는 결과를 가져온다. 일반적으로 많이 쓰는 new나 malloc 같은 메모리 할당함수의 경우에도 메모리를 4의 배수 위치에 할당한다. 그러므로 이 역시 CopyMemory API나 DtCopyMemory를 사용한다면 필요 없는 비교 및 분기 코드가 실행되게 되는 것이다.

그럼 여기서 버스트 모드에 적합한 환경에서 최고의 효율을 낼 수 있는 <리스트 4>의 DtMemoryCopyMacro 함수를 보도록 하자. 구현 코드는 상당히 적다. esi에는 4의 배수 주소를 가진 소스의 주소를 넣고 edi에는 4의 배수 주소를 가진 목적지 주소를 넣는다. 그리고 ecx에 복사가 일어날 횟수를 넣어주고 cld를 이용하여 복사 시 주소의 방향을 증가 방향으로 셋팅한 후, rep movsd로 dword(4바이트)씩 한번에 복사한다. 이렇기 때문에 함수 호출시 인자를 4로 나눠 준 것이다.

<리스트 4>의 코드를 쓰면 <그림 4>, <표 1>에서 볼 수 있듯이 버스트 모드의 혜택을 받기 위한 분기, 비교 등의 구문이 없으므로 최고의 퍼포먼스를 낼 수 있다. 하지만 주소 정렬되지 않은 메모리 블럭에서 DtCopyMemoryMacro 함수를 쓰면 당연히 이러한 혜택을 받지 못한다. 더구나 CopyMemory API나 DtCopyMemory 함수와 같은 버스트 모드 혜택을 위한 코드가 없기 때문에 결국 <그림 5>, <표 2>에서 보다시피 두 함수보다 못한 결과를 보이게 된다.

이러한 점을 염두에 두고 DtCopyMemoryMacro 함수를 사용할 수 있는 환경이 어떤 때인가를 잘 판단하여야 할 것이다. <표 1, 2>를 보면 중간중간 수행 클럭 수가 갑자기 높아지는 경우가 종종 보이는데, 이것은 OS의 멀티쓰레드 지원으로 인한 컨텍스트 스위칭(context switching)이 일어나기 때문이다. 수행 중 수행 권한이 다른 프로세스나 쓰레드로 이동 후 되돌아 온 경우, DtGetClockCount 함수는 rdtsc를 이용해 클럭 수를 얻어오기 때문에 클럭 수가 높은 결과를 보이게 되는 것이다. rdtsc를 사용할 때도 이러한 것을 염두에 두고 갑자기 결과치가 터무니없이 높게 나오더라도 놀라지 말기를 바란다. 그럴 때는 여러 번 실행해 평균치를 사용하는 게 좋을 것이다.

이론과 실제의 차이
이번 호에서는 버스의 구조와 데이터 흐름의 최적화에 대해 중점적으로 알아보면서 한 클럭의 속도라도 빠르게 만들려고 노력하는 개발자에게 도움이 될 만한 실무적인 내용을 언급했다. 이번 연재를 통해 간단히 데이터를 옮겨오기 위한 이론과 실제의 차이를 보여주고 싶었다. 객체지향 언어와 각종 고급화된 툴이 자웅을 겨루는 이 시기에 근본을 보자고 외치는 것이 공허하게 들릴 수 있다. 하지만 그것이 우리가 작성할 프로그램의 시작과 끝임을 알기에 오늘도 그 근본을 이해하려 노력하고 있다.
다음 호에서는 지금까지 알아본 CPU 관련 최적화, 버스 관련 최적화를 이용해 어떻게 실무에 응용될 수 있는지를 알아보겠다. 잘못된 부분이나 궁금한 점은 필자의 전자우편이나 http://http://myhome.hitel.net/~DAMGI/의 질문 게시판에 써주길 바란다.

정리 | 위윤희 | iwish@korea.cnet.com



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