최적화에 있어서 가장 중요한 것은 역시 실제 측정이다. 아무리 좋아보이는 코드라도 실제로는 그다지 성능이 나오지 않을 수도 있기때문이다. 지난번의 movqmovntq의 비교가 이미 살펴본 예이다. memcpy의 구현에 관련해서 실제로 시간을 측정해 비교해 볼만한 사항이 몇가지 있다.

조건부 분기

CPU의 파이프라인이 길어지면서 점점 더 조건부 분기 예측이 틀렸을때의 손실이 커진다. 물론, 요즘 나오는 CPU는 펜티엄같이 어처구니 없는 예측을 하지는 않아서, 예측이 맞는 경우가 더 많게 구성되어 있다. 그러므로 예측이 틀려도 그 손실보다는 맞는 경우의 이득의 합이 더 크게 되는 경우가 많다.

지금까지의 구현에서는 조건부 분기 예측을 놓고 도박을 하는 것보다는 차라리 확실하게 알려진 소요시간을 믿자라는 암묵적인 원칙이 있었다. 예컨대, 단순한 구현에서

        mov     edx,ecx
        shr     ecx,2
        and     edx,3
    rep movsd
        mov     ecx,edx
    rep movsb
ecx의 값이 0이면 굳이 rep movs를 실행할 필요가 없지만, 이것을 점검하고 다음으로 건너 뛰는 것보다는 차라리 ecx가 0일때에는 rep접두명령이 효과가 없는 것을 이용해 그냥 흐름을 따라가게 만들었다.

그러나, 조건부 분기 예측의 성능이 좋다면, ecx의 값을 점검하여 그 값이 0일때에는 복사명령을 건너뛰게 해서 속도를 증진시킬 수도 있을 것이다. 실제로 그러한지는 측정을 해서 비교해봐야 알 것이다.

루프 정렬

인텔의 최적화 설명서는 루프의 선두를 16바이트 경계에 정렬할 것을 권한다. 지금까지의 구현에서 MMX와 SSE명령을 사용한 루프는 함수의 선두가 16바이트 경계에 정렬되어 있으면 자연스럽게 루프의 선두도 16바이트 경계에 정렬되게 짜여있다. 그런데, 이 코드에 조건부 분기를 더하여 불필요한 복사주소 정렬을 건너뛰게 한다면, 루프의 선두가 16바이트 경계에 정렬되지 않게된다. 그러할때에, 그것을 그대로 두고 실행하는 것이 더 빠른가 아니면 다시 align지시자를 이용해 16바이트 경계로 정렬되도록 강제하는 것이 더 빠른가는 측정을 통해 비교해봐야 알 수 있을 것이다.

루프 크기

이미 설명된 바이지만, 루프 언롤링은 조건부 분기의 수를 줄이는 대신 루프의 크기를 크게 한다. 이는 루프를 도는 동안 코드캐쉬의 활용도를 떨어지게 할 가능성이 있다. 한편, 이런 루프를 도는 동안에는 조건부 분기 예측이 틀리는 경우가 반드시 발생한다. 예컨대, 펜티엄2의 기본 예측 알고리즘에 의하면, 루프를 도는 동안 네번에 한번은 반드시 예측이 틀린다. 이 두가지를 고려하면, 루프의 크기를 적절한 수준으로 조정해 손실을 최소화하는 지점을 찾아야 한다.

memcpy 구현 연재 목록
[1] 가장 단순한 구현
[2] 주소 정렬
[3] MMX 이용
[4] SSE로 확장
[5] 그외의 고려사항들
[6] 초보가 빠지기 쉬운 함정
Posted by movsd
,