Archive for August, 2008

정태영

kldp 를 눈팅하다가 오늘 재밌는 문서를 읽게 되었습니다. 2001년도에 AMD 에서 나온 문서였는데, 그 내용이 상당히 흥미로워서 몇 가지 내용을 옮겨볼까 합니다.

메모리에서 연속된 값들을 다른 곳으로 복사하기 위해 사용할 수 있는 가장 간단한 어셈블리 코드는 다음과 같습니다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) shl ecx, 3 // convert to byte count rep movsb

rep movsb 명령은 repeat move single byte (바이트 단위로 값들을 반복해서 옮긴다.)라는 의미를 가집니다. esi 가 가리키는 곳에 있는 값을 edi 로 ecx 에 있는 개수 만큼 복사하게 됩니다. 이렇게 할 경우 일초에 약 620MB 를 복사할 수 있다고 하네요.

이 코드를 byte 단위가 아니라 4 byte 단위로 반복해서 복사를 하도록 하면 어떻게 될까요? 우선 코드는 다음과 같이 변하겠네요.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) shl ecx, 1 // convert to DWORD count rep movsd

shl 인스트럭션은 두개의 오퍼랜드를 가지며 (shl operand1 operand2) operand1 에 있는 값을 operand2 에 있는 값만큼 왼쪽으로 shift 를 시키게 됩니다. movsd 는 mov single dword[1] 라고 보시면 됩니다.

결과적으로 이렇게 코드를 수정함으로 인해 1초에 640MB 를 복사할 수 있게 됩니다. 3% 정도 성능 향상이 생기네요.

그런데 최근에 나온 프로세서들에서는 rep 같은 복잡한 인스트럭션을 내부적으로 RISC 명령으로 바꿔서 실행하다 보니, 그리 효율적이지 못하답니다. 그러므로 rep 를 사용하지 말고 반복문을 사용해보도록 합시다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) shl ecx, 1 // convert to DWORD count copyloop: mov eax, dword ptr [esi] mov dword ptr [edi], eax add esi, 4 add edi, 4 dec ecx jnz copyloop

코드가 뭔가 좀 길어졌죠? 위 코드를 c로 표현하면 아래와 같습니다.

int* src = esi; int* dst = edi; int i; for( i = len ; i != 0 ; i-- ){ dst++ = src++;

c로 표현하니 어디서 많이 쓰던 코드죠? 쨌든! 이렇게 하니 1초에 650MB 를 복사할수 있었고 결과적으로 1.5% 정도 성능이 향상되었답니다.

그럼 여기다가 Loop 코드를 최적화 하는 기법인 Loop Unrolling 을 적용해봅시다. [2]

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) shr ecx, 1 // convert to 16-byte size count // (assumes len / 16 is an integer) copyloop: mov eax, dword ptr [esi] mov dword ptr [edi], eax mov ebx, dword ptr [esi+4] mov dword ptr [edi+4], ebx mov eax, dword ptr [esi+8] mov dword ptr [edi+8], eax mov ebx, dword ptr [esi+12] mov dword ptr [edi+12], ebx add esi, 16 add edi, 16 dec ecx jnz copyloop

자 룹을 펼쳤더니 1초에 640MB 를 복사하였고, 결과적으로 1.5% 만큼 성능이 떨어졌습니다. 하지만 다행히도 Loop Unrolling 을 적용하고 나니 최적화를 할 여지가 많아졌네요.

캐쉬를 좀 더 잘 활용할 수 있도록 코드 순서를 바꿔봅시다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) shr ecx, 1 // convert to 16-byte size count copyloop: mov eax, dword ptr [esi] mov ebx, dword ptr [esi+4] mov dword ptr [edi], eax mov dword ptr [edi+4], ebx mov eax, dword ptr [esi+8] mov ebx, dword ptr [esi+12] mov dword ptr [edi+8], eax mov dword ptr [edi+12], ebx add esi, 16 add edi, 16 dec ecx jnz copyloop

이젠 1초에 660MB 를 복사할 수 있게 되었고, 3% 만큼 성능 향상이 일어났습니다.

여기서 끝이 아닙니다. 첨에 movsb 대신 movd 를 사용해서 1byte씩이 아닌 4byte 씩 복사를 하는 방법을 통해 최적화를 진행했었는데요, MMX 를 사용할 경우 movq 등의 인스트럭션을 이용해서 한 번에 8byte 씩을 복사하는게 가능해집니다. 또한 mm0~7 이라는 특수한 레지스터를 활용할 수 있으니 8*8 = 64 즉 한번에 64byte 씩을 복사해봅시다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) lea esi, [esi+ecx*8] // end of source lea edi, [edi+ecx*8] // end of destination neg ecx // use a negative offset copyloop: movq mm0, qword ptr [esi+ecx*8] movq mm1, qword ptr [esi+ecx*8+8] movq mm2, qword ptr [esi+ecx*8+16] movq mm3, qword ptr [esi+ecx*8+24] movq mm4, qword ptr [esi+ecx*8+32] movq mm5, qword ptr [esi+ecx*8+40] movq mm6, qword ptr [esi+ecx*8+48] movq mm7, qword ptr [esi+ecx*8+56] movq qword ptr [edi+ecx*8], mm0 movq qword ptr [edi+ecx*8+8], mm1 movq qword ptr [edi+ecx*8+16], mm2 movq qword ptr [edi+ecx*8+24], mm3 movq qword ptr [edi+ecx*8+32], mm4 movq qword ptr [edi+ecx*8+40], mm5 movq qword ptr [edi+ecx*8+48], mm6 movq qword ptr [edi+ecx*8+56], mm7 add ecx, 8 jnz copyloop emms // empty the MMX state

MMX 용 레지스터들인 mm0~7 은 FPU stack 의 일부를 활용하게 되므로, 이 레지스터 값을 바꿔주게 될 경우 FPU 와 관련해서 문제를 일으킬 수 있습니다. EMMX 는 이를 방지하기 위해 사용해야 하는 인스트럭션이 되겠습니다. 하여튼 이렇게 바꾸니 1초에 705MB 를 복사할 수 있게 되었고, 7% 만큼 성능이 향상되었습니다.

이젠 movntq 라는 인스트럭션을 통해 cache 를 우회해서 writing 을 진행해봅시다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) lea esi, [esi+ecx*8] lea edi, [edi+ecx*8] neg ecx copyloop: movq mm0, qword ptr [esi+ecx*8] movq mm1, qword ptr [esi+ecx*8+8] movq mm2, qword ptr [esi+ecx*8+16] movq mm3, qword ptr [esi+ecx*8+24] movq mm4, qword ptr [esi+ecx*8+32] movq mm5, qword ptr [esi+ecx*8+40] movq mm6, qword ptr [esi+ecx*8+48] movq mm7, qword ptr [esi+ecx*8+56] movntq qword ptr [edi+ecx*8], mm0 movntq qword ptr [edi+ecx*8+8], mm1 movntq qword ptr [edi+ecx*8+16], mm2 movntq qword ptr [edi+ecx*8+24], mm3 movntq qword ptr [edi+ecx*8+32], mm4 movntq qword ptr [edi+ecx*8+40], mm5 movntq qword ptr [edi+ecx*8+48], mm6 movntq qword ptr [edi+ecx*8+56], mm7 add ecx, 8 jnz copyloop sfence emms

movntq 를 활용한 다음에는 write buffer 를 비워주기 위해 sfence 를 사용해야 한다는군요. write 부분이 movq 에서 movntq 로 바뀌었고 emms 앞에 sfence 가 들어간 것을 제외하면 코드는 동일합니다. 하지만 성능 향상은 60% 로 굉장하네요. 1초에 1130MB 를 복사할 수 있었다고 합니다.

이젠 prefetch 도 활용해봅시다.

mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // number of QWORDS (8 bytes) lea esi, [esi+ecx*8] lea edi, [edi+ecx*8] neg ecx copyloop: prefetchnta [esi+ecx*8 + 512] // fetch ahead by 512 bytes movq mm0, qword ptr [esi+ecx*8] movq mm1, qword ptr [esi+ecx*8+8] movq mm2, qword ptr [esi+ecx*8+16] movq mm3, qword ptr [esi+ecx*8+24] movq mm4, qword ptr [esi+ecx*8+32] movq mm5, qword ptr [esi+ecx*8+40] movq mm6, qword ptr [esi+ecx*8+48] movq mm7, qword ptr [esi+ecx*8+56] movntq qword ptr [edi+ecx*8], mm0 movntq qword ptr [edi+ecx*8+8], mm1 movntq qword ptr [edi+ecx*8+16], mm2 movntq qword ptr [edi+ecx*8+24], mm3 movntq qword ptr [edi+ecx*8+32], mm4 movntq qword ptr [edi+ecx*8+40], mm5 movntq qword ptr [edi+ecx*8+48], mm6 movntq qword ptr [edi+ecx*8+56], mm7 add ecx, 8 jnz copyloop sfence emms

위 코드에는 현재 복사할 차례의 512 바이트를 미리 읽어두라는 의미의 prefetchnta 인스트럭션이 추가되었습니다. 한 번에 복사하는건 64Byte 인데 왜 512Byte 를 읽으라고 했는지 살짝 의문이네요. 제 생각에는 문서를 작성하신 분이 버그를 낸거라고 생각합니다.

하여튼 이젠 1초에 1240MB 를 복사할 수 있게 되었고, 10% 만큼 더 성능 향상이 생겼네요.

지금까지의 방법만으로도 꽤 많은 성능 향상이 있었지만, 한 번에 한 캐쉬 라인[3]만을 활용하고 있습니다. 하지만 실제 CPU 에는 훨씬 많은 캐쉬 라인이 존재하므로 이를 더 잘 활용할 수 있도록 코드를 수정해보겠습니다.

#define CACHEBLOCK 400h // number of QWORDs in a chunk mov esi, [src] // source array mov edi, [dst] // destination array mov ecx, [len] // total number of QWORDS (8 bytes) // (assumes len / CACHEBLOCK = integer) lea esi, [esi+ecx*8] lea edi, [edi+ecx*8] neg ecx mainloop: mov eax, CACHEBLOCK / 16 // note: prefetch loop is unrolled 2X add ecx, CACHEBLOCK // move up to end of block prefetchloop: mov ebx, [esi+ecx*8-64] // read one address in this cache line… mov ebx, [esi+ecx*8-128] // … and one in the previous line sub ecx, 16 // 16 QWORDS = 2 64-byte cache lines dec eax jnz prefetchloop mov eax, CACHEBLOCK / 8 writeloop: movq mm0, qword ptr [esi+ecx*8] movq mm1, qword ptr [esi+ecx*8+8] movq mm2, qword ptr [esi+ecx*8+16] movq mm3, qword ptr [esi+ecx*8+24] movq mm4, qword ptr [esi+ecx*8+32] movq mm5, qword ptr [esi+ecx*8+40] movq mm6, qword ptr [esi+ecx*8+48] movq mm7, qword ptr [esi+ecx*8+56] movntq qword ptr [edi+ecx*8], mm0 movntq qword ptr [edi+ecx*8+8], mm1 movntq qword ptr [edi+ecx*8+16], mm2 movntq qword ptr [edi+ecx*8+24], mm3 movntq qword ptr [edi+ecx*8+32], mm4 movntq qword ptr [edi+ecx*8+40], mm5 movntq qword ptr [edi+ecx*8+48], mm6 movntq qword ptr [edi+ecx*8+56], mm7 add ecx, 8 dec eax jnz writeloop or ecx, ecx // if( ecx != 0 ) jnz mainloop // goto mainloop sfence

여기서는 cache 가 1024 개의 cache line 을 가지고 있다고 가정했네요. (16진수인 400h 는 10진수로 바꿀 경어 1024 가 됩니다.) 미리 캐쉬 사이즈만큼 prefetch 명령들을 내려놓은 뒤 값들을 복사하게 될 때 쯤이면 이미 값들이 캐쉬에 올라와있게 되니 딜레이를 줄일 수 있게 되겠습니다.

효과가 있을까 싶지만, 실제 1초에 1976MB 를 복사할 수 있었고, 59% 의 성능향상이 추가로 발생했습니다. 초기 코드에 비하면 300% 의 성능 향상이라네요. 신기하죠. ;)

전 상당히 흥미롭게 읽었었는데, (비슷한 걸 해본 경험도 있고 해서) 재밌었는지 모르겠네요. sfence 나 movqnta, prefetchnta 같은 캐쉬와 관련된 명령들은 정확히 무슨 용도인지 이해하지 못하고 있었는데 이 문서를 통해 이해할 수 있었던 것 같네요. 관심이 있으신 분은 아래 문서를 읽어보시면 되겠습니다. FPU 관련된 최적화도 다루고 있는데, 관심이 없어서 그 부분은 옮기질 않았습니다. 그럼 다들 즐거운 주말 보내시길 ;)

http://kldp.org/files/AMD_block_prefetch_paper.pdf

[1] x86 호환 아키텍쳐에서는 사이즈를 byte, word, dword, qword, dqword 식으로 표현합니다. 이는 각각 1, 2, 4, 8, 16 바이트를 의미하며, word 가 2바이트이고 나머지의 앞에 붙은 알파벳들은 각각 double(*2), quad(*4), double quad(*8)를 나타내는 것이죠.

[2] Loop Unrolling 은 Loop 을 펼쳐서 파이프라인의 덕을 더 많이 볼 수 있도록 코드를 수정하는 방법입니다. 예를 들어

for ( i = 0 ; i < 16 ; i++ ) dst[i] = src[i];

위와 같은 코드를

for ( i = 0 ; i < 16 ; i+=4 ){ dst[i] = src[i]; dst[i+1] = src[i+1]; dst[i+2] = src[i+2]; dst[i+3] = src[i+3]; }

이렇게 바꿀 경우 branch 로 인한 pipeline hazard 도 줄일 수 있고, add instruction 도 1/4 만큼만 사용하게 됨으로 인해 성능이 향상될 여지가 많습니다.

[3] 메모리에 어떤 값을 읽어들일 때 CPU 에서는 바이트 단위로 값을 읽지 않고, block 단위로 값들을 cache 에 복사하는데, 이 block 의 크기를 cache line 이라고 표현합니다. 펜티엄 계열의 경우 대부분 64byte 입니다.

정태영

어쩌다가 몇 년전에 kldpcdk 의 포럼에서 제가 참여했던 쓰레드들을 다시 보게 되었습니다. 유니코드나 서체와 관련된 글들에서 가끔 흥분을 했던 기억이 있는데, 지금 다시 보니까 얼굴이 화끈화끈 거리는군요. 하여튼 ‘왜 공개 글꼴이 필요한가?’ 쓰레드를 보고 생각난 게 있어서 오랫만에 포스팅을 해보려 합니다.

현재 은글꼴, ttf-alee, 서울체, 남산체, 백묵 글꼴 등등 공개 글꼴들이 하나 둘 나타나기 시작하고 있지만, 아직도 화면용(On screen display)으로 특화된 서체는 없습니다. 그 때문에 웹에서는 IR(Image Replacement) 등의 방법을 통해 이미 렌더링된 이미지로 텍스트를 대체시키는 방법을 많이 사용하고 있는데, 왜 화면용 글꼴에는 다들 관심을 가지지 않는 것일지 조금 아쉽네요.

Microsoft 의 경우 화면 용으로 Webdings, Verdana, Georgia, Trebuchet MS, Comic Sans MS, Impact, Arial, Courier New, Times New Roman 등의 서체를 공개하고 있습니다. 다음 페이지를 참고해보시면 알겠지만 이 서체들은 대부분 IE 나 Microsoft Windows 에 기본으로 번들되어 있기 때문에 대부분 기본으로 설치되게 됩니다.

http://www.microsoft.com/typography/web/fonts/fonts02.htm

또한 다음 URL 을 참고해보시면 웹을 통해 다운로드 받은 뒤 매킨토시에서도 사용할 수 있다고 되어 있습니다. (Verdana, Trebuchet MS 등은 화면용으로는 불필요한 기능들을 제외시킨 화면용 글꼴입니다.) 게다가 Linux 에서도 흔히 Corefonts 란 이름으로 패키징되어 사용되고 있죠.

http://www.microsoft.com/typography/web/fonts/verdana/default.htm

물론 아무 제약 없이 사용할 수 있는 서체의 개발을 의뢰하는 데는 많은 비용이 드는 것으로 알고 있습니다. 그렇기 때문에 서울 남산체, 한강체 처럼 (서울시에서 주도했으니) 나라에서 주도해서 자유롭게 사용가능한 화면용 서체들이 좀 개발되었으면 하는 바램이 있네요. 뭐 조금 앞어나간다면 IE 서비스팩 등에 그 새로운 화면용 글꼴들이 포함되었으면 더 좋겠구요. (이런 건 정치적인 문제다보니…)

정태영

저는 오른손으로는 멜로디를 치고 왼손으로 반주를 하는 걸 선호해왔는데, 노래 부르면서 뚱가뚱가 거리기엔 양손 모두로 반주를 하는 게 더 쉬워보이더군요. 그러던 중에 가뭄의 단비와 같은 사이트를 찾아내고 말았습니다.

그렇게 길지도 않으면서 설명을 조곤조곤 잘하시네요. 만약 코드를 모르신다면 아래 영상들을 먼저 보는게 더 좋겠네요. (사실 저도 코드는 좀 야매로 알고 있긴 하지만 -_-;; )

Major Code:

Minor Code:

딸림화음 같은 것들

Diminished Code:

Code 연습법:

빠른 곡 반주법:

더 관심있으신 분은 아래 커뮤니티에 있는 동영상 게시판을 참고하시면 될 것 같아요.

http://home.freechal.com/rachelchordpiano/

이제 남은 건 열혈 연습 뿐입니다. 후훗;

p.s) 익숙하지 않아서 더 어렵네요. Top note 맞추는게 생각처럼 쉽지가 않아요. ㅠ.ㅠ 그래도 꼭 다행이다를 아래처럼 치고 말테에요!

정태영

얼마전 개인적인 이유로 파워북을 팔려고 시도하면서, 내 개인적인 소스들이 담겨있던 ~/Source 폴더가 유실되어버렸다. 뭐 사실 대부분의 소스들은 짤막한 테스팅 코드들이었기 때문에 큰 문제는 없었지만 아쉽게도 Mac 용으로 개발해두었던 YUV Player 의 소스가 소실되어버렸다.

사실 내가 개발했던 YUV Player 의 경우 UI 랑 관련된 부분을 제외하고는 대부분 Platform independent 한 OpenGL + C 의 조합으로 개발되었기 때문에 다른 플랫폼에 이식하는게 큰 어려움은 없을거라 생각했지만 막상 MFC 기반 프로젝트로 생성해두다 보니 다른 플랫폼으로 이식하기 위해서는 소스를 복사한 뒤 또 한 번의 노가다가 필요할 수 밖에 없는 상황이 되어버렸다.

요 근래 논문을 쓰기 위한 실험 코드들도 언제 묶어놓은지 모를 아카이브들로 인해 다양한 버젼의 소스들이 공존하고 있는 상황인데 -_- 이 상황을 계속 끌어나가면 안될 것 같다.

지금까지 내 SVN Repository 는 http://svn.unfix.net/ 을 통해 공개해왔기 때문에 소스가 정리되지 않은 상황에서 commit 을 하는 건 자제해 왔지만, 이제는 비공개 svn repository 를 하나 만들어서 소스가 정리되어 있지 않거나 공개할 수 없는 소스들을 관리해야할 것 같다.

맥용 YUV Player 는 날렸지만 -_-;; Cocoa 를 이용하는 방법 등은 이미 YUV Player 를 개발하면서 이미 습득했기 때문에 금방 복구할 수 있을 듯…

귀찮더라도 Private SVN 을 업데이트하는 수고는 잊지 말아야겠다.

정태영

사실 저나 제 주위 사람들 말고는 쓰는 사람이 거의 없는거 같긴 하지만 하여튼 메모리 릭을 일으키는 몇 가지 버그를 잡았습니다.

  1. ::GetDC(hWnd) 후 ::ReleaseDC(hWnd,dc) 를 호출 하지 않아서 생기는 메모리 릭
  2. gdTexImage2D 를 반복 호출해서 생기게 되는 메모리 릭

정확하게 설명하면 위와 같구요. ::GetDC 로 받아온 Device Context 는 “꼭” ::ReleaseDC 를 호출해줘야 한다는 msdn 님의 가르침에 따라, 약간의 코드를 추가해줬습니다.

또한 gdTexImage2D 를 반복해서 호출하면 이전 텍스쳐 데이타가 사용하던 메모리 영역은 해제가 될 줄 알았는데, 실제로는 그렇지가 않네요. 텍스쳐 사이즈가 달라지는 경우엔 glDestroyTexture 후 glGenTexture, glBindTexture, glTexImage2D 를 차례로 호출해줘야 하고, 사이즈가 달라질 필요가 없는 경우라면 gdTexSubImage2D 를 사용하면 된답니다. 어려운 openGL 세상이에요.

자세한 수정 사항은 제 trac 페이지에서 확인하심 될 듯~
http://trac.unfix.net/browser/yuvplayer/win/yuvplayer/OpenGLView.cpp

p.s) trac 이 ajax 를 활용하도록 업데이트 되었네요.

정태영

며칠 전에 공개 글꼴들에 관련된 글을 포스팅했었는데요. :) 요번엔 그 글꼴들이 어떤 모양인지를 뽑아내봤습니다.

http://mytears.org/resources/font-sample/

gd 를 사용해서 샘플을 뽑아봤는데, gd 에서는 서체 래스터라이져로 freetype 을 사용하고 있고, 윈도우의 래스터라이져와 맥의 래스터라이져와는 약간의 차이를 보이기 때문에 실제로 사용할 때와 약간 차이가 있을 수도 있겠습니다.

글꼴을 뽑아내는데 사용한 프로그램은 아래에서 보실 수 있습니다.

http://trac.unfix.net/browser/snipp…t/gd_font_preview/gd_font_preview.c