아주 오래전에 적어두었던 글을 블로그 정리중 발견하여 옮겨적습니다.(2008년 8월 27일 글)

 

그래픽 처리하는 루틴이나 라이브러리의 경우 C언어나 CPP로 아무리 컴팩트하게 프로그래밍을 했다고 해도, 컴파일러가 사람만큼 스마트 하지 못하기 때문에 비효율적이게 되는 부분이 발생하게 된다. 

컴파일러의 첫 목적은 사용자가 의도한 대로 안전하게 어셈블링 해주는 것이기 때문에 이런 비효율은 피할 수 없는 것이다.

만약 사용자가 어셈블리에 관한 지식과 해당 H/W에 대한 지식이 조금이라도 있다면, 어셈블리 레벨에서 최적화를 시도해 볼 수 있는데, 그 실제적인 예를들어 설명하겠다.

<C CODE>

typedef _DWORD DWORD
struct _DWORD
{
    unsigned short  a ;  
    unsigned short  b ;  
    unsigned short  c ;  
    unsigned short  d ;  
};

typedef _DWORD2 DWORD2
struct _DWORD2
{
    unsigned int   A;  
    unsigned int   C ;  
};

void Loop(DWORD2 *dst,DWORD *src,int loopsize)
{
    while(loopsize--)
    {
         int d = 256 - src->d;
         dst->A=(DWORD2*)src->A+((dst->A*d)>>8)&0x1F001F00;
         dst->C=(DWORD2*)src->C+((dst->C*d)>>8)&0x1F001F00;
         dst++;
         src++;
   }       
}

위의 코드를 C언어 레벨에서 분석하였을때, 구조체가 사용이 덜 최적화 되었다는 것을 알수 있다.
즉, (DWORD2*)src->C값은 src->d 값을 포함하고 있기 때문에, C코드상에서도 메모리 Access를 1회 제거하는데 큰 공헌을 할수는 있다. 

CPU상에서 연산되는 것보다, 메모리 IO에 의한 성능저하가 상당하기 때문에 위와 같이 메모리 IO를 1회 줄이는것만으로도 성능향상을 확인할 수 있다.

앞으로, 우리가 튜닝을 할때에는 C언어로 무결한 코드를 완전히 컴팩트하게 구성한다음에 어셈블리 언어로 최적화 하면 된다.

위 C코드의 컴파일 결과 : ASM 코드(armcc 사용,LE, arm1136j-S)

Loop
     SUBS    R3,R2,#1
     BXCC   R14
     PUSH   {R14}
     LDRH    R2,[R1,#6]
     LDR      R4,[R0,#0]
     LDR      R12,[R1,#0]
     RSB      R2,R2,#0X100
     SUBS    R3,R3,#1
     MUL     R4,R2,R4
     ADD      R12,R12,R4,LSR #8
     BIC       R12,R12,#0x1F000000
     BIC       R12,R12,#0x1F00
     STR      R12,[R0,#0]
     LDR      R4,[R0,#4]
     LDR      R12,[R1,#4]
     ADD      R1,R1,#8
     MUL     R2,R4,R2
     ADD      R2,R12,R2,LSR #8
     BIC       R2,R2,#0x1F000000
     BIC       R2,R2,#0x1F00
     STR      R2,[R0,#4]!
     ADD      R0,R0,#4
     BCS      {PC} -0x4C
     POP      {R14}
     BX         R14

위의 ASM 코드를 보면 상당히 많은 부분이 개선가능하다는 것을 발견할 수 있다. 

두 말 할 것 업이 바로 최적화 작업에 돌입한다.!!!

개선된 코드

Loop_opt
    SUBS     R3,R2,#1
    BXCC     R14
    PUSH     {R4-R9}   ; 레지스터를 더 많이 쓰기 위해, 스택에 저장
    MOV      R8,#0x1F00
    ORR       R8,R8,R8, LSL #16
    MOV      R9,#0xFF
    ORR       R9,R9,R9 LSL #8
INLOOP
    LDM       R1!,{R6,R7}  ; 한번에 두개씩 읽어오자!!
    LDM       R0, {R4,R5}  ; 역시 한번에 두개 씩  읽어올것!!
    AND       R2,R9,R7, ASR #16   ;위에서 말했던, 메모리 Access 1회 줄이는 방법.!!!!
    RSB       R2,R2,#0x100 
    MUL      R4,R2,R4 ; STALL제거를 위해 위로 올라옴
    MUL      R5,R2,R5 ; STALL제거를 위해 위로 올라옴
    SUBS     R3,R3,#1
    ADD       R6,R6,R4, LSR #8 ;이놈은 줄이고 싶지만... 더 연구해봐야 됨.
    BIC        R6,R6,R8  ; 이런거 말고 방법이 있을거야~~
    ADD       R7,R7,R5, LSR #8 ;이놈도...
    BIC        R7,R7,R8 ; 이런거 말고 방법이 있을거야~~
    STM      R0!, {R6,R7} ; 두개 저장.
    BCS      INLOOP
    POP      {R4-R9}
    BX         R14

적용 테스트 결과 :루틴 자체의 성능이 약 3배 개선되었다. (시스템 점유율 12 % --> 4%로 개선)
최적화 방법 :
1. 메모리 접근 횟수를 줄임
2. LOOP내 인스트럭션의 수를 줄임
3. STALL 발생 횟수를 없앰.
4. BIT 연산을 한번에 가능하도록 함.

그러나, "나는 더 개선하고 싶다." 하는 사람은 아직 한가지 더 방법이 있다.
바로 Cache의 최대한 활용하는 방법을 연구하는 것과 SIMD(Single Instruction Multiple Data)를 사용하는 것이다.  ARM의 경우 Cache Line 사이즈가 32byte이므로 한 라인에 딱 맞는 Loop Code를 작성하면 캐시 Hit Ratio가 증가할것이고 이에 따라 성능향상도 기대할 수 있다.
그러기 위해서는 Thumb code는 16개, ARM code는 8개의 instruction으로 코드를 작성해야 한다. 

그리고 SIMD를 적용한다면, 아래 코드를 개선할수 있다.

MUL      R4,R2,R4 ; 
MUL      R5,R2,R5 ; 
ADD       R6,R6,R4, LSR #8 ;
BIC        R6,R6,R8  ;
ADD       R7,R7,R5, LSR #8 ;
BIC        R7,R7,R8 ;

구조체 구조가 32bit SIMD에서는 못써먹을 구조라서, 64bit SIMD가 되면 바로 적용해 볼 수 있을것 같다.

한줄요약 : RISC에서는 생각 보다 최적화는 어렵지 않다.

블로그 이미지

커뉴

이 세상에서 꿈 이상으로 확실한 것을, 인간은 가지고 있는 것일까?

,