Darryl Gove, Geetha Vallabhaneni, 컴파일러 퍼포먼스 팀, 썬 마이크로시스템즈,
2006년 1월 5일
 

들어가는 글

VIS 명령어 셋은 여러가지 데이타를 한꺼번에 처리 할 수 있는 몇가지 명령어들을 가지고 있습니다. 이러한 명령어들은 SIMD (Single Instruction Multiple Data) 명령어라고 불립니다. VIS 명령어들은 부동소숫점 레지스터들에 있는 데이타를 가지고 연산을 하게 됩니다. 부동소숫점 레지스터들은 8바이트 사이즈를 가지고 있읍니다. 또한 VIS 명령어들은 부동소숫점 레지스터를 4바이트의 정수형 두개 또는 2바이트의 short형 4개 또는 1바이트의 문자형 8개처럼 다룰 수 있습니다. 아래에 그림을 참조하시기 바랍니다:

Frame1

VIS 명령어들을 이용해서 얻을 수 있는 장점은 서로 다른 데이타를 병렬로 조작할 수 있는 작업이 가능하다는 것입니다. 즉 1바이트 8개 데이타를 계산하는 것이나 8바이트 데이타를 계산하는 것이나 별다른 차이가 없다는 것입니다. 이론상으로 이것은 VIS 명령어를 포함한 코드가 포함되지 않은 코드에 비해 훨씬 빠르다는 것을 의미합니다.

VIS 명령어 셋에 대한 좀 더 자세한 설명은 다음의 링크에서 찾아 보실 수 있습니다.http://www.sun.com/processors/vis/ .


VIS 퍼포먼스

VIS 명령어를 사용하여 퍼포먼스 향상을 얻을 수 있습니다. 그러나 퍼포먼스를 얼마나 향상 시킬 수 있는지는 다음의 요소를 고려하기 전까지는 확실 하지 않습니다:

  • VIS 명령어는 한번에 여러개의 데이타를 조작 가능합니다. 그러므로 VIS의 8개 문자형에 대한 add 작업은 8개의 분리된 add 작업에 비해 빠릅니다.

  • VIS 명령어는 부동소숫점 레지스터들에 저장 되있는 값을 사용합니다. 그리고 값을 적재하고 저장하는데 부동소숫점의 적재,저장 명령어를 사용합니다. 이것은 UltraSPARC-III 시리즈의 프로세서에 이득을 가져다 줍니다. 왜냐하면 부동소숫점 값들은 온-칩 프리패치(prefetch) 캐쉬에 프리패치 될 수 있기 때문입니다.또한 이렇게 함으로써 정수 데이타들이 메모리로 부터 프리패치되어야 할때 프로세서가 멈추는 사태를 피할 수 있습니다.

  • VIS 명령어는 일반적으로 동일한 정수를 다루는 작업에 비해 시간이 오래 걸립니다.VIS 명령어는 종종 네번의 사이클 latency를 가질 것입니다.(VIS 명령어들은 부동소숫점 유닛들에 의해 조작됩니다. 그러므로 VIS 명령어들의 latency는 부동소숫점 명령어들의 latency와 동일합니다) 그에 반해 정수를 다루는 작업은 하나의 사이클만을 가집니다.

  • 안타깝게도 모든 정수 작업들이 VIS 명령어들을 통해 가능하지는 않습니다. 그러므로 데이타를 처리하는 동안 정수 데이타를 정수 레지스터로 옮기는 작업이 있을 수도 있습니다. 이것은 오직 메모리를 통해서만 가능하며 엄청나게 느린 작업입니다.


VIS를 이용해 컴파일하기

VIS를 이용하기 위해서는 사용하는 아키텍쳐가 적어도 v8plusa 또는 v9a이여야 합니다. 컴파일은 -xarch=v8plusa 또는-xarch=v9a 같은 컴파일러 플래그를 이용하여 가능합니다.

컴파일러가 VIS 명령어들을 생성할 수 있는 두가지 방법이 있습니다:

  • VIS 명령어들을 위한 매크로가 <vis.h> 안에 존재합니다,컴파일러는 -xvis 컴파일러 플래그에 의해 이것을 인식할 것입니다.

  • 또한 VIS 명령어들을 인라인 템플릿들에 넣는 것이 가능합니다. 인라인 템플릿에 대한 정보는 다음 글에서 찾으 실 수 있습니다:  
    http://developers.sun.com/prodtech/cc/a ··· ing.html .

VIS 명령어들은 컴파일러에 의해 직접적으로 생성되지 않기 때문에 생성된 코드들이 최적화되지 않았을 가능성도 있습니다.(VIS 명렁어들은 일반적으로 후위 결합(late-inlined) 되기 때문입니다. inline template 문서 참조.). 그러므로 생성된 코드의 어셈블리 코드를 확인하여 코드가 정상적인지 퍼포먼스가 기대한대로 나오는지에 대해 확인 할 필요가 있습니다.


VIS 명령어에 의해 코딩된 예제 루틴

우리가 살펴볼 예제는 캐릭터가 아닌 4바이트 정수 데이타를 검색하는 간단한 예제 입니다. 이 루틴은 배열에서 특정한 값을 검색하고 얼마만큼의 캐릭터가 검색됐는지 결과를 돌려 줍니다.

int search(int value, int* array)
{
  int len=0;
  while (*array++!=value) {len++;}
  return len;
}

표 1 - C로 짜여지고 VIS를 사용하지 않은 검색 예제 루틴

또한 테스트 코드를 삽입함으로써 코드의 현재 퍼포먼스를 확인합니다.

#include <stdio.h>
#include <sys/time.h>

int array[10*1024*1024];
static hrtime_t last_time=0;

void seconds()
{
  hrtime_t time;
  time=gethrtime();
  if (last_time!=0)
  {
    printf( "Elapsed seconds = %5.3f\n",
      ((double)(time-last_time)*1.0e-9) );
  }
  last_time=time;
}

void init()
{
  int i;
  for(i=0; i<10*1024*1024; i++) {array[i]=i;}
}

void main()
{
  int loop;
  init();
  seconds();
  for (loop=0; loop <10*1024*1023; loop+=100*1024)
  {
    if (search(loop,array)!=loop)
    {
      printf("Miscompare\n");
      break;
    }
  }
  seconds();
}

표 2 - 테스트 코드

타이밍 루프가 좀 복잡해 보일 것입니다. 이 루프는 다음과 같은 특성을 가지고 있습니다:

  • 타이밍 루프는 결과의 옳고 그름또한 판별해 줍니다. 이것은 코드가 최적화 되었을때 새로운 버젼 또한 정확성이 체크되도록 할 수 있습니다.

  • 타이밍 코드는 다양한 배열 길이를 시도해서 최적화가 다양한 배열 길이에 따라 퍼포먼스 향상을 가져 오는지에 대해 확인 할 수 있습니다. 물론 타이밍의 방법은 어떠한 좋은 퍼포먼스를 가진 길이에 의해 나쁜 퍼포먼스를 가지는 길이가 묻혀 버리는 단점이 생길 수도 있스빈다.

테스트에 몇가지 약점을 지적해 보자면:

  • 이 코드는 각 테스트 수행시의 캐시를 동일한 상태로 만들지 못함으로써 초기의 테스트는 캐시의 불리함을 안고 갈 수도 있습니다.

  • 테스트는 배열 상태와 잘못배열된 코드의 퍼포먼스를 체크하지 않습니다. 예를 들어 어레이가 3바이트 경계에서 시작될 경우 퍼포먼스가 달라질 수 있습니다.


예제 코드 빌딩 및 실행하기

일단 예제 코드를 실행해 보겠습니다.

% cc test.c
% a.out
Elapsed seconds = 14.481

표 3 - 최적화를 하지 않은 상태의 퍼포먼스

최적화를 하지 않고 컴파일 했기 때문에 퍼포먼스는 형편 없을 것입니다. 루프 코드가 매우 흥미로움을 알 수 있습니다.

10cb4: inc      %i5
10cb8: ld       [%fp + 72], %o0
10cbc: ld       [%o0], %i4
10cc0: ld       [%fp + 72], %o0
10cc4: inc      4, %o0
10cc8: st       %o0, [%fp + 72]
10ccc: ld       [%fp + 68], %o0
10cd0: cmp      %i4, %o0
10cd4: bne      search+0x34 		! 0x10cb4
10cd8: st       %i4, [%fp - 24]

표 4 - 최적화 되지 않은 어셈블리 코드

이 코드가 형편없다는것은 명백해졌습니다. 루프 인덱스 값 (%i4에 저장됨 ) 과 배열 오프셋 변수 (%o0) 가 한번 루프를 돌때마다 계속해서 저장되고 다시 불러와 집니다. 이 코드를 평가하는 방법은 저장과 불러들인 횟수를 세면 됩니다. 한번 루프를 돌때마다 4번의 불러들임과 2번의 저장이 있습니다. 소스코드와 비교해 봤을때 각 반복마다 단 한번의 불러들임이 있어야 됨을 알 수 있습니다. 이러한 상황을 개선시키는 방법은 어느정도의 최적화를 사용하여 재컴파일 하는 것입니다.

% cc -O test.c
% a.out 
Elapsed seconds = 8.614

표 5 - 최적화된 코드의 퍼포먼스

퍼포먼스가 거의 2배 정도 향상 되었습니다. 루프 부분을 디스어셈블한 결과는 다음과 같습니다:

10ca4:  ld         [%g5], %o4
10ca8:  cmp        %o4, %o5
10cac:  be,pn      %icc,search+0x44        ! 0x10ccc
10cb0:  inc        4, %g5
10cb4:  ld         [%g5], %o2
10cb8:  inc        %o0
10cbc:  inc        4, %g5
10cc0:  cmp        %o2, %o5
10cc4:  bne,a,pt   %icc,search+0x1c        ! 0x10ca4
10cc8:  inc        %o0

표 6 - 최적화된 어셈블리 코드

이 경우에 루프는 In this case the loop has been unrolled twice (there are two iterations performed before the predicted taken branch at the end of the loop), but not pipelined (the two iterations are not interleaved together). 가장 중요한 잇점은 인덱스 변수가 레지스터에 저장됨으로써 각 반복마다 불러들임과 저장 작업을 하지 않아도 된다는 것입니다. 이 경우에서 코드에는 두번의 불러들임 작업이 있습니다. 그러나 코드는 코드는 두개의 반복문을 가집니다. 그러므로 코드는 적장한 수준의 불러들임 작업을 하는 것이라고 볼 수 있습니다.

어쨌든 아직도 루프는 프리패치가 가능한 명령을 가지고 있지 않습니다. 프리패치가 가능한 인스트럭션을 사용한다면 프로레서가 데이타가 필요하기 전에 미리 읽어 들여 오게 하는것이 가능 합니다. 즉 프로세서가 데이타를 필요로 할때 곧바로 가져 올 수 있음으로써 데이타가 메모리로 부터 카피 되는 시간동안의 시간 손실을 없앨 수 있다는 것입니다.


프리패치를 수동으로 추가하는 방법

헤더파일 <sun_prefetch.h> 을 사용하면 수동으로 프리패치 구문을 소스코드에 추가 하는 것이 가능합니다.

#include <sun_prefetch.h>

....

int prefetch_search(int value, int* array)
{
  int len=0;
  while (*array++!=value)
  {
    len++;
    sparc_prefetch_read_many(array+16);
  }
  return len;
}

표 7 - 수동으로 프리패치 구문을 추가한 소스코드

수동으로 프리패치 구문을 추가한 소스의 퍼포먼스 결과는 다음과 같습니다.

% cc -O test.c
% a.out 
Elapsed seconds = 5.606

표 8- 프리패치 구문을 추가한 최적화된 코드의 퍼포먼스

프리패치 구문은 현재 위치의 16ints 를 배열에 미리 프리패치 하라는 것을 의미 합니다. 이것은 16*4 바이트 즉 64 바이트 (각 int는 메모리에서 4 바이트를 차지함) 또는 하나의 캐쉬라인을 의미합니다. 프리패치는 불러들임 작업을 하기 전에 충분한 시간을 가짐으로써 좀 더 효과적으로 동작 할 수 있습니다. 실험을 위해서 오프셋을 +16 에서 +64 까지 변경시켜 보겠습니다. 즉 64*4 바이트를 미리 프리패치 또는 4개의 캐쉬라인을 가짐을 의미합니다. 결과는 다음과 같습니다.

% cc -O test.c
% a.out 
Elapsed seconds = 3.566

표 9 - 좀 더 앞쪽의 데이타 까지 프리패치 한 후의 퍼포먼스

결과적으로 최적화와 수동 프리패치 구문 입력을 통해 거의 5배의 성능 향상을 이끌어 내었습니다.


소스 코드에 VIS 명령어 포함시키기

VIS 명령어를 사용할 수 있는 하나의 방법은 그것을 소스 코드안에 추가 시키는 것입니다. 이 방법은 헤더파일 <vis.h> 을 이용하고 컴파일시에 -xvis 플래그를 이용할 것을 요구합니다. 또한 VIS 명령어들은 적어도 v8plusa 이상의 아키텍쳐를 요구합니다.

이 코드는 VIS 명령어들을 사용하기 위해 수정될 수 있습니다. 비교 구문이 4바이트의 정수형 두개를 이용하기 때문에 두개의 정수 모두 동시에 불러들여올 수 있고 비교 될 수 있습니다. 매크로 vis_fcmpeq32 는 비교구문을 수행하는 VIS 명령어를 생성합니다. 다음의 코드는 VIS 명령어를 이용한 검색을 수행합니다:

#include <vis.h>

...

int srcvis_search(int value, int *array) {
  union {double d; int i1,i2;} tmpR;
  double* tmpI;
  int ret;
  int ind = 0;
  tmpR.i1=value;
  tmpR.i2=value;
  if(!((unsigned long)(&array[0]) & 4) 
      || array[ind++] != value) 
  {
    tmpI = (double*) &array[ind];
    for(; !(ret=vis_fcmpeq32(tmpR.d, *tmpI++)); ind+=2) 
    {
       sparc_prefetch_read_many(tmpI+32);
    }
    if (!(ret &2)) { ind++;}
  }
  return ind;
}

표 10 - C코드에서의 VIS 명령어

이 코드를 컴파일 하기 위하여:

% cc -O -xvis -xarch=v8plusa test.c
% a.out 
Elapsed seconds = 2.639

표 11 - VIS 명령어를 포함한 C코드의 퍼포먼스

VIS 코드는 그전의 정수 코드의 비해 빨리 동작함을 볼 수 있습니다.여기에는 두가지 이유가 있습니다. 두개의 정수를 한번에 비교함으로써 퍼포먼스 이득을 이끌어 냈지만 그렇게 하기 위한 명령어가 좀 더 긴 latency를 가짐으로써 아주 큰 이득을 얻진 못했습니다.(물론 코드가 8바이트로 또는 4바이트의 short형을 가지고 작업했다면 VIS 명령은 훨씬 더 큰 퍼포먼스 이득을 이끌어 냈을 것입니다). 또다른 하나는 부동소숫점 load 명령이 온-칩 프리패치 캐쉬를 이용해서 데이타를 불러 들일 수 있었기 때문입니다. 이것은 칩 외부에 있는 데이타를 불러 들이는 시간을 단축시켰다는 것을 의미합니다.

코드가 좀 더 복잡해 졌지만 한번 살펴볼 가치가 있습니다. 코드가 복잡해진 이유는 다음과 같습니다:

  • VIS가 부동소숫점 레지스터를 이용해 동작함으로써 검색되어질 값이 부동소숫점 레지스터의 상위 그리고 하위에 둘다 복사되어져야 합니다. 이것은 union 구문을 통해 수행됩니다. 이 값은 메모리에서 불러들여질 값과 비교하는 데 사용될 것입니다.

  • load 명령은 8바이트의 값을 불러옵니다. 즉 load 명령은 8바이트로 정렬되어야 합니다. 그러므로 올바른 정렬됨을 얻기위해서는 if 구문이 잘못 정렬된 것을 체크해야 하고 잘못되었다면 루프로 컨트롤을 넘기기 전에 수정 작업을 해줘야 합니다.

  • VIS 비교 명령에 의해 리턴된 값은 두개의 비교되었던 값이 서로 다름을 나타내는 비트 패턴 형태로 되어 있습니다. Bi1 은 상위 값이 달랐음을 의미하고 0은 하위 비트가 달랐음을 의미합니다. 만약 상위 값이 다르지 않았다면 The return value from the VIS compare instruction is a bit pattern which indicates which of the two values being compared was different. Bit 1 indicates that the upper value was different, bit 0 indicates that the lower value was different. If the upper value was not different, then it is necessary to add one to the return value to show that the miscompare took place with the lower value. (Note that the lower bit being set does not necessarily mean that the miscompare took place with the lower value, the upper value could also have miscompared).


VIS 사용법과 인라인 템플릿

VIS를 이용해 최상의 퍼포먼스 이득을 얻기 위해서는 인라인 템플릿을 이용해야 합니다. 아래의 코드는 기본적으로 C 소스코드와 같은 알고리즘으로 작동하지만 퍼포먼스 향상을 위해 약간의 코드 레이아웃을 변경했습니다.

/* Routine vis_search(int value, unsigned int * array); */
/* %o0 = value*/
/* %o1 = address of array*/

.inline vis_search,8
  st %o0,[%sp+0x48]    /* store search value */
  clr %o3              /* counter = 0 */
  and %o1,4,%o2        /* check for not 8-byte aligned*/
  cmp %o2,4
  bne,a 1f
  ld  [%o1],%o2        /* annul load if taken */

  cmp %o2,%o0
  be 2f                /* found */
  add %o1,4,%o1        /* dealt with misaligned int */

  add %o3,1,%o3        /* compared first character */
1:
  ld [%sp+0x48],%f0    /* load upper word */
  fmovs %f0,%f1        /* copy to lower word */
  ldd [%o1],%f2        /* load 2 ints */
  
3:  
  add %o1,8,%o1        /* move pointer to next pair */
  prefetch [%o1+256],0 /* Prefetch four lines ahead */
  fcmpne32 %f0,%f2,%o0 /* compare ints */
  ldda [%o1]%asi,%f2   /* non-faulting load 2 ints */
  cmp %o0,3            /* check result of compare */
  be,a 3b              /* branch if not found */
  add %o3,2,%o3        /* increment count by two; annulled if found */

  andcc %o0,2,%o0      /* check bit 2 of return value from compare */
  bnz,a 2f
  add %o3,1,%o3        /* add one if the first half matched */
  
2:
  or %o3,%g0,%o0       /* Copy counter to output */
.end

표 12 - VIS 명령어를 이용한 인라인 템플릿

VIS 코드는 8바이트로 정렬되지 않은 배열들을 체크하고 수정하는 것에서 부터 시작합니다. 다음에 정수 값을 각각 부동소숫점 값으로 각각 반씩 카피 합니다. 그럼으로써 안쪽 루프는 VIS 예제 코드와 매우 비슷해 집니다. 다음의 결과는 코드를 컴파일 하고 실행 시킴으로써 VIS를 통해 약간의 퍼포먼스 이득을 얻었음을 보여 줍니다. 이러한 퍼포먼스 이득은 현재 값의 비교가 끝나기 전에 다음 값을 불러(fetch)오는 load 명령이 실패 하지 않았음을 의미합니다.

실패하지 않은 load 명령이란 보통의 load와 비슷하지만 load가 매핑되지 않은 메모리에 성공적으로 접근 했음을 의미합니다. 이러한 상황에서 정상적인 load 명령은 메모리가 맵되지 않았으므로 런타임 에러를 유발합니다. 그러나 똑같은 상황에서 실패하지 않은 load는 에러를 유발하지 않고 그냥 0 값을 리턴합니다. 이러한 명령의 장점은 배열의 끝까지 도달하지 않았더라도 load가 다음 단계로 진입 할 수 있음을 의미하고 배열의 끝을 지났더라도 어떠한 오류도 발생하지 않을것입니다.

% cc -O vis_search.c test.c
% a.out 
Elapsed seconds = 2.416

표 13 - 인라인 템플릿 코드의 퍼포먼스

It is possible to further improve the performance of the hand-coded VIS routine. For example, it would be possible to unroll and pipeline the inner loop such that two or more iterations were computed in parallel. Whilst doing that optimisation would improve performance, it would also make the code less clear, so it was not shwon here.


Concluding remarks

This article has demonstrated a number of useful techniques for improving performance. To recap:

  • Whilst the compiler generally does a good job of inserting prefetch into code, there is always the possibility of manually inserting prefetch statements into the code to cover the cases where the compiler does not.

  • It is possible to write source code that includes VIS instructions in a way that leads to performance gains.

  • If necessary, further performance can be extracted from codes by manually recoding the hot points using inline templates.


Full source code

Here is a link to the full source code test.c and to the vis_search.il inline code.

Compiling and running the program should produce results similar to that shown below.

% cc -xvis -O -xarch=v8plusa test.c vis_search.il
% a.out
Elapsed seconds = 8.628
Elapsed seconds = 5.606
Elapsed seconds = 2.645
Elapsed seconds = 2.410

Table 14 - Compile line and timing data for all routines

About the Authors
Darryl Gove is a senior staff engineer in Compiler Performance Engineering at Sun Microsystems Inc., analyzing and optimizing the performance of applications on current and future UltraSPARC systems. Darryl has an M.Sc. and Ph.D. in Operational Research from the University of Southampton in the UK. Before joining Sun, Darryl held various software architecture and development roles in the UK.

Geetha Vallabhaneni is a member of the Compiler Performance Engineering team at Sun Microsystems Inc. Her work primarily focuses on performance analysis and optimization of applications for UltraSPARC systems.

"개발자코너" 카테고리의 다른 글

2006/02/23 10:23 2006/02/23 10:23

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/90

댓글을 달아 주세요

  1. 박정숙  수정/삭제  댓글쓰기

    좋은 정보 감사해요~

    2007/09/19 04:47
[로그인][오픈아이디란?]

◀ Prev 1  ... 613 614 615 616 617 618 619 620 621  ... 806  Next ▶