1. 아래의 최적화를 위한 6 단계는 비단 제온 파이 보조 프로세서 뿐만 아니라 일반 제온 프로세서에서도 똑같이 적용된다.

    1. 베이스라인 성능 측정
    • 비교할 성능을 측정한다. 최적화가 제대로 되고 있는지 판단 및 성능 개선 효과 비교 측정용. 디버그 빌드 대신 릴리즈 빌드를 이용하여 측정한다.

    2. 인텔 VTune을 이용하여 핫스팟 찾기
    • 인텔 VTune을 이용하여 가장 많은 시간이 걸리는 함수를 찾아낸다. 가장 많은 시간이 걸리는 함수가 가장 먼저 최적화를 수행할 부분이다. 이 부분을 최적화하는 것이 가장 효과적이다. 일반적으로 가장 많은 시간이 걸리는 몇 개의 함수만 최적화 한다. 보통 전체 수행 시간의 10% 이상되는 함수.

    3. 인텔 컴파일러 vec-report를 이용하여 최적화할 루프 찾기
    • “벡터화 보고서” 기능은 각 루프가 벡터화가 되었는지 아닌지 알려준다. 자동 벡터화 기능을 사용하기 위해서는 컴파일러 옵션 Level2나 Level3(-O2 또는 –O3)를 사용하여야 한다. 단계 2에서 찾은 핫스팟이 벡터화를 사용하고 있는지 아닌지 확인. 만약 벡터화가 안되어 있다는 메시지가 나오게 되면 왜 안 되는지 분석.
    • 컴파일러 옵션 –vec-report2 또는 /Qvec-report2

    4. 인텔 컴파일러 GAP(Guided Auto-parallelization)을 이용하여 조언 구하기
    • 인텔 컴파일러 GAP(Guided Auto-parallelization) 보고서 기능을 실행하여 컴파일러가 루프 벡터화를 어떻게 하는지 제안 내용을 봄. 
    • 컴파일러 옵션 –guide 또는 /Qguide 

    5. GAP에 의한 조언 및 제안 적용
    • 결과값이 변하지 않도록 주의하여 GAP에 의한 조언 및 제안 적용

    6. 반복!
    • 원하는 성능까지 반복.

    참조: 자세한 정보는 Vectorization Toolkit 참조
    0

    댓글 추가

  2. 제온 파이 보조 프로세서의 성능 향상에 있어서 모든 코어를 최대로 사용하고 벡터를 충분히 사용하는 것은 가장 중요한 기본이다. 그렇지만 이번에 설명을 할 메모리에 관련된 부분도 성능에 커다란 영향을 주는 요소가 된다. 이번 글에서는 메모리 TLB에 대해서 다시 한번 간단히 설명을 하고 TLB가 성능에 미치는 영향과 2 MB 페이지를 할당하는 프로그램 예를 설명한다.

    이전 글, 제온 파이 캐시 구조 및 TLB 에서 TLB의 구조 및 특성에 대해서 간략하게 설명을 하였다. 보조 프로세서는 4KB(표준), 64KB(비표준), 2MB(거대, 표준)의 페이지 크기로 가상 메모리를 관리한다. 만약에 잦은 TLB 미스가 발생해서 보조 프로세서가 4단계의 페이지 테이블을 살펴보는 작업 수행으로 성능이 떨어진다면 사용하는 페이지의 크기를 더 큰 2MB를 사용함으로써 성능 향상을 가져올 수도 있다.

    보조 프로세서의 초기 리눅스 버전 2.6.34에서는 malloc 과 _mm_malloc의 기본 페이지 크기는 4KB이며, 2MB의 거대 페이지 지원을 위한 Transparent Huge Page(THP)를 지원하지 않았다. THP 지원은 애플리케이션이 실행되는 동안 malloc과 _mm_malloc이 자동으로 2MB 페이지를 할당할 수 있게 한다. THP는 커널 버전 2.6.38 이후에서 지원된다. 그렇지만 THP를 지원 한다고 해서 반드시 애플리케이션에 2MB의 페이지를 할당할 것이라는 보장은 없다. 따라서 애플리케이션이 거대 페이지를 사용하는 것을 보장 하기 위해서 메모리를 할당하기 전에 mmap() 함수를 이용하여 2MB 페이지를 수동으로 확보하는 것이 필요하다.

    그렇지만 모든 애플리케이션이 더 커다란 페이지 크기를 사용한다고 해서 반드시 성능 향상이 되는 것은 아니다. 일반적으로 대용량 페이지 크기를 통한 성능 향상은 데이터 접근 패턴에 따라 좌우된다. 만약 애플리케이션이 각기 다른 곳의 페이지에 있는 여러 데이터 구조를 접근한다면 보조 프로세서에 있는 2MB 페이지가 8개 엔트리밖에 없기 때문에 오히려 성능 감소도 생길 수 있으니 성능 분석 도구를 이용하여 분석을 하여야 한다.

    오프로드 모델을 위한 거대 페이지 설정

    인텔 컴파일러 오프로드 런타임은 메모리 할당 크기가 MIC_USE_2MB_BUFFERS 환경 변수값보다 초과했을때 2MB 페이지로 메모리를 할당한다. 예를 들면, 사용자가 "MIC_USE_2MB_BUFFERS=64K" 처럼 설정을 하면 거대 페이지에 할당되는 크기는 64KB보다 같거나 큰 값이다.


    • MIC_USE_2MB_BUFFERS       2M 페이지 사용. 애플리케이션 실행시(Runtime) 포인터 기반의 변수값 길이가 설정되는 지금 이 값을 초과하면 거대 페이지에 할당될 것임.
    설정 예: MIC_USE_2MB_BUFFERS=64K

    네이티브 모델을 위한 거대 페이지 설정

    애플리케이션이 거대 페이지를 사용하는 것을 보장 하기 위해서 메모리를 할당하기 전에 mmap() 함수나 libhugetlbfs 라이브러리를 이용하여 2MB 페이지를 수동으로 확보하는 것이 필요하다. 이 두가지 방법에 대한 설명은 다음과 같다.


       1. mmap 시스템 호출 사용

    아래 코드의 예는 메모리를 할당하고 시스템에 돌려주는데 사용되는 malloc_huge_pages 와 free_huge_pages 두 함수를 정의하는 방법의 한 예이다. 프로그래머들은 이 코드로 현재 코드의 malloc/free 시스템 함수를 대체하여야 한다.


    #include <assert.h> 
    #include <stdlib.h> 
    #include <sys/mman.h> 
    
    #define HUGE_PAGE_SIZE (2 * 1024 * 1024) 
    #define ALIGN_TO_PAGE_SIZE(x) \ 
       (((x) + HUGE_PAGE_SIZE - 1) / HUGE_PAGE_SIZE * HUGE_PAGE_SIZE) 
    
    void *malloc_huge_pages(size_t size) 
    { 
       // Use 1 extra page to store allocation metadata 
       // (libhugetlbfs is more efficient in this regard) 
       size_t real_size = ALIGN_TO_PAGE_SIZE(size + HUGE_PAGE_SIZE); 
       char *ptr = (char *)mmap(NULL, real_size, PROT_READ | PROT_WRITE, 
          MAP_PRIVATE | MAP_ANONYMOUS | 
          MAP_POPULATE | MAP_HUGETLB, -1, 0);
       if (ptr == MAP_FAILED) { 
          // The mmap() call failed. Try to malloc instead
          ptr = (char *)malloc(real_size);
          if (ptr == NULL) return NULL;
          real_size = 0;
    } 
    
       // Save real_size since mmunmap() requires a size parameter 
       *((size_t *)ptr) = real_size; 
    
       // Skip the page with metadata 
       return ptr + HUGE_PAGE_SIZE; 
    } 
    
    void free_huge_pages(void *ptr) 
    { 
       if (ptr == NULL) return; 
       // Jump back to the page with metadata 
       void *real_ptr = (char *)ptr - HUGE_PAGE_SIZE; 
       // Read the original allocation size 
       size_t real_size = *((size_t *)real_ptr); 
    
       assert(real_size % HUGE_PAGE_SIZE == 0); 
    
       if (real_size != 0) 
          // The memory was allocated via mmap()
          // and must be deallocated via munmap() 
          munmap(real_ptr, real_size);
       else 
          // The memory was allocated via malloc() 
          // and must be deallocated via free()
          free(real_ptr);
    }
    

    2MB 페이지 수 제어

    보조 프로세서에서 돌아가고 있는 uOS는 필요에 따라 거대 페이지 메모리를 할당한다. 거대 페이지 사용이 활성화 되어 있는지 확인하기 위해서는 아래를 실행하면 된다.

         $ ssh mic0 cat /proc/sys/vm/nr_overcommit_hugepages

    0이 아닌 한자리 수가 출력 될 것이다. 이 수는 커널이 할당할 수 있는 거대 페이지 개수이다. 마이크로 리눅스에서 기본으로 제공하는 방법은 충분한 여유 메모리가 없으면 실패한다. 좀 더 적극적인 방법은 보조 프로세서에서 거대 페이지 수를 미리 할당하는 것이다. 예를 들어 보조 프로세서에 다음과 같은 명령어를 날리면 리눅스 마이크로 OS는 다섯 개의 2MB 거대 페이지를 설정한다.

         $ ssh mic0 "echo 5 > /proc/sys/vm/nr_hugepages"

    이때, 애플리케이션에서 모든 malloc이 사용하는 페이지 수들의 총 합으로 설정하는 것을 권장한다. 만약에 애플리케이션에서 두 개의 malloc이 각각 P1, P2개의 페이지를 사용한다면 설정 해야 할 값은 P1과 P2의 합이 좋다. 총 필요한 수를 요청하는 것이 메모리 파편화를 막는 기회를 높여주고 따라서 원하는 것을 확보할 확률이 높아진다.

    즉각적으로 페이지 수를 바꿀 수 있는 방법은 없다. 이 파일에 있는 값이 줄어들었을 때, 보조 프로세서의 리눅스 OS는 메모리가 실질적으로 자유롭게 되었을 때 할당을 한다.

    보조 프로세서에서 2MB 페이지 수의 관찰

    HUGE_TLB 플래그를 이용한 mmap() 호출은 2MB 페이지를 보장한다. 만약 요청 사항이 만족 되지 않으면 MAP_FAILED를 반환한다. 따라서 보조 프로세서에서 사용되고 있는 2MB 페이지 수를 /proc/meminfo를 통해서 살펴볼 수 있다. 아래 명령어를 보조 프로세서에서 사용해 보자.

         % cat /proc/meminfo

    다음 값들이 나올 것이다.
    ….

    HugePages_Total: vvv

    HugePages_Free: www

    HugePages_Rsvd: xxx

    HugePages_Surp: yyy

    Hugepagesize: zzz kB

    ……

       2. libhugetlbfs 라이브러리를 이용

    libhugetlbfs는 메모리의 대용량 페이지에 쉽게 접근할 수 있는 방법을 제공하는 라이브러리이다. 이 방법을 쓰면 일반적인 malloc/calloc/readlloc등과 같은 메모리 할당 함수를 쓰는 동적으로 연결된 애플리케이션들은 자동으로 대용량 페이지를 얻을 수 있다. 소스를 바꾸거나 다시 컴파일을 할 필요가 없다. libhugetlbfs는 GNU LESSER GENERAL PUBLIC LICENSE 공개 소프트웨어 프로젝트로 공개되어 있다. 다음에서 받을 수 있다.
    https://sourceforge.net/project/showfiles.php?group_id=156936

    libhugetlbfs 라이브러리를 사용하는 순서는 아래와 같다.

    • 라이브러리 소스 코드를 받는다. 제온 파이를 지원하는 인텔 C/C++ 컴파일러로 빌드를 한다. CC64 변수에 "-mmic" 플래그를 추가하면 된다.

         $ make ARCH=x86_64 CC64=icc libs BUILDTYPE=NATIVEONLY

    • scp로 libhugetlbfs.so와 libimf.so를 보조 프로세서에 복사한다. 이때 의존성이 있는 libsvml.so, libintlc.so.5, libintlc.so와 libimf.so를 함께 복사한다. 복사할 곳은 LD_LIBRARY_PATH 환경변수에 정의된 곳이다.

    • libhugetlbfs 파일 시스템을 보조 프로세서에 마운트 한다.

         $ ssh mic0 "mkdir -p /mnt/hugetlbfs"
         $ ssh mic0 "mount -t hugetlbfs none /mnt/hugetlbfs"

    • 동적으로 연결된 애플리케이션을 위해서 LD_PRELOAD 환경 변수를 실행 전에 설정하라
         $ ssh mic0 "LD_LIBRARY_PATH=/path/to/lib HUGETLB_MORECORE=yes \
            LD_PRELOAD=/path/to/lib/libhugetlbfs.so ./a.out"

    • 정적으로 연결된 애플리케이션을 위해서, 다음 옵션으로 다시 컴파일해야 한다. : “-Wl,--whole-archive,libhugetlbfs.a,--no-whole-archive”. 그런 후 아래를 실행한다.

         $ ssh mic0 "HUGETLB_MORECORE=yes ./a.out"

    • .BSS, .DATA와 .TEXT 섹션이 동적으로 연결된 애플리케이션을 위해 거대 페이지로 할당하고 싶다면 다음 옵션으로 다시 컴파일해야 한다. “-Wl, -T./libhugetlbfs/ldscripts/elf_x86_64.xBDT -L ./libhugetlbfs/obj64/”. 그런 후 아래를 실행한다.

         $ ssh mic0 "LD_LIBRARY_PATH=/path/to/lib HUGETLB_MORECORE=yes ./a.out"

    • 애플리케이션이 실행되는 동안에 아래 명령을 이용하여 보조 프로세서에서 메모리가 할당되는지 아닌지 확인 가능하다.

         $ ssh mic0 "cat /proc/meminfo | grep HugePages"

    • 출력은 아래와 같을 것이다.

    HugePages_Total: vvv

    HugePages_Free: www

    HugePages_Rsvd: xxx

    HugePages_Surp: yyy

    Hugepagesize: zzz kB

    참고로 mmap 방법은 C/C++ 에만 적용되며 포트란을 네이티브 방법으로 사용하고자 한다면 libhugetlbfs 라이브러리 방법을 사용하여야 한다.

    참조: White Paper How to Use Huge Pages to Improve Application ..
    0

    댓글 추가

  3. 효율적으로 벡터화를 이루기 위해서는 데이터들의 흐름이 메모리부터 캐시, 벡터 레지스터까지 매끄럽게 이동 되어야 한다. 데이터 이동시 오버헤드가 발생하여 지연이 생기면 생길수록 벡터를 이용하는 비중이 낮아지게 된다. 효율적인 데이터 이동은 데이터 배치(layout), 정렬(alignment), 프리페치와 데이터 저장을 얼마나 효율적으로 처리하는가에 달려있다.

    왜 데이터 배치가 효율적인 벡터 처리에 영향을 미치는가?

    데이터 병렬화라는 것은 여러 데이터에 대해서 동일한 동작(처리)를 동시에 하는 것을 말한다. 몇가지 성능 최적화를 위해 이해하고 가야 할 문제들은 다음과 같다.

    • 메모리에서의 데이터 배치, 정렬 및 팩(Packed). 단정도 부동소수점 덧셈을 생각해 보자. c = a + b. 16개의 값을 각 입력 레지스터에 가져와야 한다. 이때, 메모리에 있는 데이터들이 동일 선상에 있고 512비트에 정렬이 잘 되어 있다면 한번의 벡터 읽기로 모든 데이터를 가져올 수 있다. 만약 정렬이 되어 있지 않다면 추가적인 명령어가 필요해서 더 많은 시간이 걸리게 된다. 컴파일러는 정렬을 강제하거나 변수들의 정렬을 위한 지시어들과 커멘드 라인 옵션을 제공한다. 
    • 데이터 지역성
      • 메모리가 아닌 캐시에서 데이터 읽기. 데이터는 궁극적으로 메모리에 있다. 그렇지만 벡터 읽기 명령어가 수행될 시점에 데이터들이 실행 유닛과 더 가까운 곳에서 읽혀 진다면 속도가 훨씬 더 빠를 것이다. 데이터들은 실제로 메모리에서 L2 캐시로 그리고 다시 L1 캐시로 이동된다. 이상적으로 이러한 동작은 데이터에서 L2 캐시로 미리 가져와지고(프리페치), 다시 L2에서 L1으로 그리고 마지막으로 L1 캐시에서 읽기(Load)명령어에 의해 데이터가 읽혀진다. 프리페치는 하드웨어 프리페치에 의해 시작되거나 컴파일러가 자동으로 아니면 프로그래머에 의해 수동으로 소프트웨어 프리페치에 의해 시작된다.
      • 데이터 재사용. 만약 프로그램이 한 번 이상 사용되는 데이터를 캐시로 가져온다면 그런 데이터들은 서로 가까운 곳에서 있어야 한다. 이것을 임시 지역 참조성이라 한다. 데이터가 자주 재사용되는 것을 확실히 해주면 실제로 사용되기 전에 데이터가 캐시에서 밖으로 보내지는 것을 낮춰준다.
      • Streaming Stores. 만약 프로그램이 나중에 다시 사용되지 않을 데이터를 쓴다면 Streaming Stores가 사용되게 하는 것이 중요하다. Streaming Store는 캐시 사용율을 높인다.

    1. 데이터 정렬

    벡터화가 잘 안되었다는 리포트가 나오면 데이터가 정렬되었는지를 살펴보아야 한다. 하나의 배열과 다른 배열과 의 상대적인 정렬이 중요하다. 제온 파이 보조 프로세서에서는 모든 데이터(보통 배열)을 64Byte 이내로 정렬하는 것이 중요하다.

    2. 프리페치(Prefetching)

    최대 성능을 내는 애플리케이션은 잘 정렬된 데이터를 처리할 계산 유닛에 연속해서 보내는 것이다. 인텔 제온 파이 보조 프로세서는 코어의 계산 유닛에 항상 처리할 데이터가 있도록 메모리 프리페치를 하드웨어, 소프트웨어 방식으로 지원한다. 프리페치라는 말은 미리 가져온다라는 말이다. 조만간 사용될 데이터를 메모리에서 L2 캐시로, L2 캐시에서 데이터 접근 시간이 더 빠른 L1캐시로 사용하기 전에 미리 가져다 놓는 것을 말한다. 만약 우리가 필요한 데이터가 캐시에 없고 메모리에 있다면 메모리로부터 가져오는 시간은 실행 유닛 측면에서 보면 엄청나게 느리다. 이것을 미리 하드웨어가 판단을 해서 또는 소프트웨어적으로 프로그래머나 컴파일러가 판단을 해서 미리 캐시에 가져다 놓으면 데이터가 사용될 때 데이터를 가져오는데 걸리는 대기 시간이 줄어들게 되서 속도를 향상 시킬 수 있게 된다. 소프트웨어적인 프리페치는 보조 프로세서의 벡터 명령어에서 지원한다.

    캐시에 없는 데이터를 요구하는 벡터 로드 동작을 할 때 프리페치 동작이 일어나게 하는 것이 중요하다. L1 캐시에 데이터가 없으면 L2 캐시를 확인한다. 이 때 L2에서 데이터를 가져올 때 대기 시간이 발생한다. 만약 이때 L2에도 없을 때 메모리에서 가져와야 하는데 훨씬 더 많은 시간이 걸리게 된다.  이 시간이 아주 사소한 것 같지만 성능에 커다란 영향을 미친다. 이러한 캐시 미스를 없애는 것이 성능 향상에 중요하다. 대부분의 상용 라이브러리는 프리페치를 적용하여 성능을 향상 시킨다.

    참고로 제온 파이 보조 프로세서에서 L1과 L2 캐시에서 데이터를 가져오기 위해서는 각각 1 클럭과 11 클럭이 필요하다. L2 캐시에서 미스가 발생하여 메모리에서 가져오게 되면 이 보다 훨씬 더 많은 100 클럭 이상의 시간을 소모해야 한다. 좀 더 자세한 내용은 지난 포스팅 글, 제온 파이 캐시 구조 및 TLB 을 참조하기 바란다.

    4가지 종류의 프리페치

    1. 하드웨어가 자동으로 L2 캐시로 프리페치.
    2. 컴파일러가 자동으로  L1 캐시로 프리페치하는 소프트웨어 명령어 생성
    3. 프로그래머에 의해 지시를 받아서 컴파일러에 의해 생성된 프리페치 명령어
    4. 프로그래머에 의한 수동 프리페치 명령어. L1, L2 모든 곳에서 프리페치사용 가능


    컴파일러 prefetch


    인텔 컴파일러에서 최적화 수준을 –O2 또는 그 이상으로 설정하면 자동으로 프리페치가 설정됨.

    Opt-prefetch = 3

    -opt-prefetch=n n은 1부터 4까지.(리눅스)

    /Qopt-prefetch:n (윈도우)

    n이 높으면 높을수록 더 적극적으로 프리페치를 사용함

    만약 알고리즘이 L2 캐시 크기에 맞도록 데이터를 잘 나눈다면 프리페치는 덜 중요할 수 있으나 일반적으로 제온 파이 보조 프로세서에서는 유용하다.

    일반적으로 프로그래머가 프리페치를 수동으로 하는 것보다 컴파일러가 프리페치를 자동으로 사용하기를 권장한다. 만약 이렇게 해도 잘 되지 않으면 컴파일러에게 여러 가지 힌트를 주는 방법이 있다. 그래도 성능에 문제가 있다면 마지막 방법으로는 수동으로 프로그래머가 강제하는 방법이 있다.

    Pragma 지시어를 사용하여 컴파일러가 프리페치를하도록 하는 것이 사용하기 편하고 간단하다. 우선적으로 사용하길 권장한다. 이후 필요할 때만 수동 프리페치 사용을 검토한다.

    수동 프리페치(vprefetch0 과 vprefetch1 사용)

    컴파일러가 여러 힌트를 이용해서도 프리페치를 효율적으로 사용할 수 없을 때만 사용하길 권한다. 이때 하드웨어의 제한된 프리페치 역량에서 하드웨어, 소프트웨어 둘 다 사용하면 성능 감소가 일어날 수 있기 때문에 컴파일러에 의한 자동 프리페치 사용을 하지 말아야 한다.

    3. Streaming stores

    Streaming store는 벡터화에서 특별한 경우이다. 만약에 데이터들이 다시 읽거나 인용되어 사용되지 않는다고 가정하면 이런 데이터들을 버퍼나 캐시에 저장을 할 필요가 없이 바로 메모리에 저장을 하는 것이 더 좋다. 왜냐하면 이 데이터들은 나중에 틀림없이 다른 데이터들에 의해 덧 씌어질 것이고 그동안 버퍼나 캐시를 낭비하는 것이기 때문이다. Streaming store는 나중에 메모리에서 또는 캐시에서 읽혀지지 않을 데이터들을 캐시나 버퍼가 아닌 메모리에 바로 저장을 함으로써 성능 향상을 가져오게 할 수 있다. 한정된 캐시를 낭비하지 않고 다른 것이 사용할 수 있게 함으로써 효율성도 높이는 것이다.

    예를 들어 아래와 같이 배열 B와 C를 읽은 후에 그 합을 배열 A에 저장하는 코드가 있다고 하자.

    for (I=0; I<HUGE; I++)
        A[I] = k*B[I] + C[I];

    일반적으로 메모리에 배열 A를 저장하기 위해서는 먼저 A 값을 캐시에서 읽어와야 한다. 저장만 하는 작업을 위해서 부가적인 읽기 동작이 일어나게 되는 것이다. Streaming Store 명령어는 먼저 캐시에서 읽을 필요없이 바로 메모리 주소에 저장을 한다. Streaming Store이 없다면 A, B, C를 읽은 후에 계산을 한다. 그 후에 그 결과값을 A에 다시 써야하는 과정을 거친다. 그렇지만 Streaming Store 명령어를 쓰게 되면 A를 읽지 않고  B, C를 읽은 후 계산 값을 바로 메모리 주소에 바로 A를 쓴다. 따라서 있을 때와 없을 때를 비교하면 A를 읽는 과정이 생락되게 된다. 아래 표는 위 코드 예에서 Streaming Store를 사용할 때와 그렇지 않을 때의 성능 비교이다. 30%정도의 차이가 나는 것을 보여준다.

    Streaming Store의 성능 효과

    컴파일러 옵션

    -opt-streaming-stores keyword (리눅스, OS X)

    /Qopt-streaming-stores: keyword (윈도우즈)
    0

    댓글 추가

  4. 벡터화를 효율적으로 하기 위해서는 효율적인 데이터 이동과 프로그램 코드에서 벡터화 가능한 부분을 찾는 데서 시작된다. 효율적인 벡터화를 하는데 있어서 다음과 같은 장애물은 피해야 한다. 잘못된 데이터 배치, C/C++ 언어적 제한, 보수적인 컴파일러 벡터화 사용, 그리고 프로그램 구조적인 문제 등 모든 걸림돌들을 피할 방법을 생각해야만 한다.

    벡터 처리부를 이용하는 방법은 여러 가지가 있다. 가장 많이 사용되는 5가지 방법은 다음과 같다. 가장 추천할 방법은 컴파일러가 알아서 벡터화를 하도록 맡기는 것이다. 점점 더 컴파일러가 지능화되어서 많은 부분을 자동으로 효과적으로 처리해 준다. 프로그래머는 컴파일러를 도와주기만 하면 된다. 프로그래머가 직접 어셈블리 코드로 작성을 하거나 인트린식(Intrinsic)을 사용하는 것은 코드 작성하는데 있어서 어려울 뿐만 아니라 경우에 따라서 성능도 뛰어나지 않을 뿐더러 차세대 하드웨어등에서는 성능이 나빠질 수도 있다.

    • 자동 벡터화: 컴파일러에 의해 자동화를 맡김 약간의 코드 변경이 필요할 수도 있으나 컴파일러 옵션으로 처리
    • 수학 함수(MKL, IPP) 사용: 이미 라이브러리 회사의 엔지니어가 벡터화뿐만 아니라 병렬화 및 기타 다른 모든 가능한 최적화 방법을 이용하여 모든 컴퓨팅 자원을 활용하여 최적의 성능을 낼 수 있도록 만들어 놨음. 향후 아키텍처를 위해 코드의 변경 등이 필요 없음. 인텔® MKL(Math Kernel Library) 및 인텔® IPP(Integrated Performance Primitives)는 병렬화 및 벡터처리를 모두 구현한 라이브러리의 좋은 두 가지 예. 분산 메모리 병렬 컴퓨터를 위한 MPI와 같은 병렬 프로그래밍 모델도 라이브러리로 제공됨.
    • SIMD 지시어: SIMD지시어는 벡터화를 수행하도록 하는 간단하고 효과적인 방법. 프로그래머가 모든 것을 제어함. 그렇지만 아무 루프나 병렬화 하면 위험할 수 있으며 값이 달라질 수 있으니 주의해서 사용하여야 함.
    • 배열 표기법(Array Annotation): 프로그래머의 의도를 반영하는 직관적인 구문. 포트란에서는 배열 표기법을 지원해왔으나 C/C++에서는 확장언어인 실크 플러스에서 지원함. 실크 플러스의 배열 표기법은 병렬화를 위한 유연하고 아주 강력한 방법임.
    • 실크 플러스의 벡터 함수 사용: C/C++의 확장 언어로서 안정적이며 예측 가능한 언어이다. 실크 플러스의 벡터 함수 사용 장점은 기존의 코드가 병렬로 벡터처리가 가능했지만 한번에 한번의 처리를 해 왔었다면, 이 코드들을 유지하면서 병렬화를 할 수 있다는 것이 커다란 장점임.
    0

    댓글 추가

  5. 소프트웨어 코드의 성능 향상을 위해서는 벡터화가 중요하다는 것을 지난 글에서 강조를 하였다. 그럼 이번 글에서는 벡터화라는 말은 무슨 뜻이고 성능과 어떤 관련이 있는지 알아보도록 하겠다.

    “벡터”라는 말은 수학에서 “스칼라”와 구분되는 용어이다. 스칼라는 한번 이라는 뜻이고 벡터라는 말은 여러 번 이라는 말이다. 인텔의 제온 프로세서뿐만 아니라 제온 파이 보조 프로세서는 스칼라 및 벡터 처리부가 각각 있다. 하나의 스칼라 처리부는 한 명령어에 한번의 수학적 계산 처리를 한다. 예를 들면 A 값과 B 값을 더해서 변수 C에 저장하는 아래와 같은 처리가 한번만 이루어진다.
    • A + B = C
    반면에 벡터 처리부는 한 명령어에 여러 데이터 처리를 할 수 있다. 제온 파이는 512비트 SIMD(Single Instruction Multiple Data) 벡터 처리를 할 수 있다. SIMD는 하나의 명령어로 여러 데이터를 처리할 수 있다는 말의 약어이다. 즉 제온 파이 보조 프로세서는 한번 명령어로 512비트 처리가 가능하며 이 말은 하나의 명령어로 배정도(Double Precision, DP) 부동 소수점 8개의 데이터 처리가 가능하다는 말이다. 단정도(Single Precision, SP) 부동 소수점은 두 배인 16개를 동시에 처리할 수 있다. 아래 예를 보면 여덟 개의 덧셈이 한 명령어 사이클에 수행이 되기 때문에 이론적으로 위 스칼라 처리보다 8배나 빠르게 된다.
    아래 그림은 벡터와 스칼라와 비교를 도식적으로 표현하였다. 스칼라 처리에서는 하나의 데이터를 한번의 명령어로 처리를 한다. 반면에 벡터처리는 여덟 개의 데이터를 하나의 벡터 레지스터에서 계산을 한다. 이렇게 되기 때문에 아래의 경우에서는 처리 속도가 8배가 빠르게 된다. 따라서 벡터화를 이용하여 최대의 성능을 내기 위해서는 벡터 레지스터에 빈 곳이 없이 채운 후에 처리를 하여야 한다.
    벡터와 스칼라의 도식적 비교


    아래 표는 각 세대 별 지원되는 SSE에 따라 처리할 수 있는 최대 데이터 폭 및 명령어 당 처리할 수 있는 개수를 보여준다. 현재 인텔 제온 E5-2600v2 프로세서는 AVX를 지원하고 있으며 클럭 싸이클당 처리할 수 있는 배정도 부동 소수점 양은 4개이다. 제온 파이는 8개가 된다. 이것은 지원되는 마이크로 아키텍처에 따라 달라지게 된다.

    명령어
    명령어 폭
    데이터 폭
    명령어 당 처리 수
    SSE
    128-비트
    32-비트
    4
    SSE
    128-비트
    64-비트
    2
    AVX
    256-비트
    32-비트
    8
    AVX
    256-비트
    64-비트
    4
    제온 파이
    512-비트
    32-비트
    16
    제온 파이
    512-비트
    64-비트
    8
    0

    댓글 추가

  6. 제온 파이 보조 프로세서에서 최적의 성능을 내기 위해서는 다음 세 가지 항목을 준수해야 한다. 더 높은 수준으로 코드를 최적화할수록 더 높은 성능을 내는 것은 당연합니다.

    1. 효율적인 벡터화
    2. 코드는 제온 파이에 있는 512비트 벡터를 효율적으로 사용할 수 있어야 한다. 입력 데이터 집합이 보조 프로세서가 사용할 수 있을 정도의 충분한 작업 양을 제공해야 한다.
      • 입력 배열 크기가 충분히 길어야 한다
      • 메모리에 있는 작업 데이터 집합들이 최적의 벡터 읽기(Load)와 쓰기(Store)가 되도록 적절하게 정렬(aligned) 되어야 한다
      • 작업 데이터들은 하나의 덩어리로 구성되어야 한다. 따라서 캐시 라인을 채울 때 연속적인 영역에서 연속적인 데이터들이어야 한다. 여러 곳에 있는 데이터로부터 가져오려면 성능 저하가 따른다

    3. 확장성이 있는 병렬화
    4. 코드는 모든 가용 코어에 확장할 수 있어야 함. 모든 스레드에 확장되도록 하면 더욱 좋다. 스레드 수준에서 벡터화를 해야 할 뿐만 아니라 애플리케이션의 알고리즘이 많은 수의 스레드에 확장할 수 있어야 한다.

    5. 최적의 캐시 재활용
    6. 최적의 캐시 재활용은 메모리 대역폭의 제한을 극복하고 실질 Flops/Byte를 증가시켜준다. 각 데이터 구조는 최적의 캐시 재사용을 위해 다음 특성을 가져야 한다.
      • 가능하면 보조 프로세서에서 동작하는 모든 스레드가 한번에 데이터를 접근하기 위해 배열의 구조를 정렬된 배열 구조로 바꿔라(Data Align).
      • 데이터 구조를 단위로 묶어라. 데이터는 캐시에 맞게 동작할 수 있기 위해 기본 단위로 나뉠 수 있어야 한다.
    위 첫 번째, 두 번째 항목은 문제의 크기는 제온 파이가 처리할 수 있는 역량과 맞아야 한다는 것을 말한다. 60코어짜리 제온 파이의 장비 크기는 64byte x 60코어 = 3.8KB 이다. 하드웨어를 최대 활용하기 위해서는 알고리즘은 최소 3.8KB의 데이터가 필요하다는 것을 말한다.

    추가 고려 사항들로는 다음과 같다.
    • Streaming Stores
    • Scatter/Gather
    • 거대 페이지 (2MB)
    • 프리패치 분석 / 튜닝
    다음 글부터는 위 각 항목 별로 좀 더 자세히 다루도록 하겠다.
    0

    댓글 추가

  7. 제온 파이 보조 프로세서에 관한 책들이다. 많은 책이 나와 있지는 않지만 제온 파이 프로세서뿐만 아니라 최적화에 대한 방법들이 설명되어 있다. 높은 성능을 얻고자 하는 개발자라면 꼭 읽어보길 추천한다.


    인텔® 제온 파이™ 보조 프로세서를 위한 고성능 프로그래
    Jim Jeffers, James Reinders, © 2013,
    출판사 : Morgan Kaufmann

    처음 입문자가 비교적 쉽게 읽을 수 있는 책. 아키텍처와 대한 설명뿐만 아니라 병렬화, 벡터화의 개념부터 프로그램 최적화에 대하여 예제를 통해 쉽게 설명됨.
    이 책은 모든 HPC 전문가의 책꽂이에 꼽혀 있어야 한다. 인텔 MIC 아키텍처를 사용하여 높은 성능을 얻는 방법을 쉽고 전문적으로 가르쳐줄 것이다. 또한 사고 방법과 현대적 아키텍처로 대응된 알고리즘의  성능 원리와 같은 고성능 컴퓨팅의 기초 지식을 가질 수 있게 해주며, 수년간 유용하게 사용할 수 있는 강력한 도구를 여러분 손에 쥐어줄 것이다
    — Robert J. Harrison
    컴퓨터 공학 전문 연구소, 
    스토니 브룩 대학교

    구조화된 병렬 프로그래밍(Structured parallel programming)
    저자: Michael McCool, Arch Robison, James Reinders

    새로운 패턴 기반 접근법을 이용한 프로그래밍 기법 전달. Cilk Plus 및 TBB 예제 제공. 특정 하드웨어에 국한되지 않고 일반 플랫폼에 적용 가능. 효과적인 병렬 프로그래밍에 관한 책.
    "이 책은 위대한 작품이다…   나는 주변에 스레드 처리로 골머리를 썩고 있는 친구들, 그리고 멀티스레딩 기술의 코어 개념들과 요즘 뜨고 있는 기술들을 이용하여 성능 향상을 얻고자 하는 연구자들에게 그들의 다양한 궁금증을 해결해 줄만한 최신 기술로 작성된 책을 오랫동안 꿈꾸어 왔다. 
    결국 나는 그런 책을 만났다."
    — 마틴 와트,
    기술 책임,
    드림웍스 애니메이션


    인텔 제온 파이 보조 프로세서를 위한 프로그래밍과 최적화에 대한 안내서. 제온 파이의 아키텍처부터 시작하여 프로그래밍 모델, 병렬화, 프로그램 최적화에 대해 상세히 다룸. 많은 예제를 통해 설명됨
    .


    인텔 제온 파이 보조 프로세서 아키텍처 및 툴(Intel® Xeon Phi™ Coprocessor Architecture and Tools)
    Rezaur Rahman, Copyright © 2013 by Apress Media, LLC

    애플리케이션 개발자를 위한 제온 파이 아키텍처와 프로그래밍 최적화 방법 및 기법들, 개발 환경을 상세히 다룬 책. 온라인 eBook으로는 무료
    0

    댓글 추가

  8. 제온 파이 보조 프로세서의 장점 중 첫째는 동일 소스 코드를 서로 다른 제온 프로세서 기반 플랫폼과 제온 파이 보조 프로세서 기반 플랫폼에 각각 적용할 수 있다는 것이다. 제온 프로세서와 제온 파이 보조 프로세서용 프로그램을 따로따로 개발하고 관리할 필요 없이 프로그램 소스 코드 하나만 개발하고 관리해도 된다는 것이다. 시스템 구성은 사용자에 따라 목적에 따라 달라질 수 있다. 시스템은 구성에 따라 CPU만 있는 플랫폼일 수 있고 CPU와 보조 프로세서가 함께 있는 플랫폼 일 수 있다. 이런 하드웨어 구성에 상관없이 프로그래머는 제온 파이 보조 프로세서가 있다는 가정에서 코드를  최적화 하면 코드 실행 시 시스템 구성 환경에 맞게 동적으로 동작을 한다. 제온 파이가 없으면 CPU에서 동작을 하고 제온 파이가 있는 환경에서는 파이의 장점을 취할 수 있는 구조이다. 따라서 프로그래머는 하드웨어 환경에 따라 여러 코드를 개발할 필요가 없어서 개발 과정이 훨씬 간단하고 개발 기간을 단축할 수 있으며 코드를 관리하는 데 편리한 커다란 장점이 있다. 요즘 들어서 프로그램의 관리 비용이 상당한 비중을 차지하고 점점 더 증가하는 추세인데 소스 코드 하나 만으로 개발하고 관리할 수 있다는 것은 정말로 커다란 장점이라 말할 수 있겠다.

    두 번째 장점은, 코드를 제온 파이 플랫폼에 한번만 최적화 하면 제온 프로세서 기반 플랫폼에서도 자연스럽게 성능 향상을 얻을 수 있다. 굳이 제온 프로세서 기반 플랫폼을 위해 따라 개발 및 최적화하고 제온 파이 기반 플랫폼에 또 다시 최적화하는 작업이 필요없다. 대부분의 최적화 작업을 제온 파이 기반에서 하며 제온에서도 성능향상을 이룰 수 있다.

    세 번째로는 개발자가 새로운 프로그램 언어나 개발 환경, 도구들을 배울 필요가 없다는 것이다. 지금 이미 익숙하게 사용하고 있는 x86 기반의 개발 환경에서 개발을 할 수 있다.

    마지막으로는 위의 모든 장점으로 앞으로 x86기반위에 투자한 개발비를 보호 받을 수 있다.


    0

    댓글 추가


  9. 인텔 제온 파이 보조 프로세서는 업계 표준인 MPI(Message passing Interface)를 지원하도록 설계되었다. 특히 인텔 MPI 라이브러리는 이전 블로그에 설명된 모든 제온 파이 프로그래밍 모델들을 지원한다. MPI는 클러스터에서 네트워크를 통해 연결된 여러 노드 들이나 하나의 인텔 제온 프로세서 플랫폼에서 여러 프로세서 코어들 간에 코드를 실행하고 통신하고 확장을 위한 병렬 애플리케이션을 가능하게 하는 라이브러리 기반 통신 환경의 사실상의 표준이다. 또한 오픈 패브릭 얼라이언스같은 하부 표준 레이어를 지원하는 MPI는 프로세서 뿐만 아니라 보조 프로세서 둘 다 지원한다.

    오프로드 MPI 모델

    오프로드 MPI 모델은 호스트 프로세서들 간에서만 MPI 통신이 이루어지고 보조 프로세서들은  호스트 프로세서가 지정한 특정 작업 만을 실행한다. 오프로드 코드 안에서의 MPI 라이브러리 호출은 지원하지 않는다. 즉, 보조 프로세서들 간의 점대점(Peer to Peer) 통신은 불가능하다. 애플리케이션은 보조 프로세서들 간의 데이터 이동을 위해 오프로드 API를 사용하여야 한다. 아래 그림은 MPI 오프로드 모델을 보여준다.
    오프로드 MPI 모델

    보조 프로세서만의 MPI 모델

    MPI 보조 프로세서만의 모델, 또는 네이티브 모델은 MPI가 보조 프로세서에 있으면서 실행되는 모델이다. MPI 라이브러리, 애플리케이션 그리고 기타 필요한 라이브러리들이 코드가 실행되기전에 먼저 보조 프로세서로 이동 되어야 한다. 애플리케이션이 시작되면 다른 보조 프로세서들 간의 MPI 네트워크 통신은 인텔 보조 프로세서 통신 링크(Intel Coprocessor Communication Link, Intel CCL)을 통해 관리된다. 이 보조 프로세서들은 노드 안에 있는 것일 수 있고 네트워크 패브릭에 연결된 다른 노드일 수도 있다. 인텔 CCL은 MPI 메시지를 위한 최적의 전송을 선택하기 위해 MPI 라이브러리에게 근본적인 서비스를 제공한다. 인텔 CCL 전송 메커니즘의 한 예는 점 대 점(Peer to Peer)  PCI Express DMA 지원이다. 보조 프로세서의 메모리와 함께 연결된 인피니밴드 어답터간에 호스트 메모리로 전달하는 과정 없이 직접 메시지를 전송하는 것이다. 아래 그림은 MPI 보조 프로세서만의 모델을 보여준다. 더 자세한 인텔 CCL의 구조 및 요소들은 다음 글에서 설명하겠다.
    보조 프로세서 MPI 모델

    대칭 MPI 모델

    MPI 대칭 프로그래밍 모델은 MPI 애플리케이션을 호스트 프로세서 및 보조  프로세서 양쪽에서 실행 한다. 어느 곳에서나 메시지를 전달할 수 있는 가장 유연한 모델이다. 메시지는 같은 노드안에 있거나 다른 노드에 있는 호스트 프로세서 안이나 보조 프로세서 안에서 전달할 수 있다. MPI 보조 프로세서만의 모델같이 인텔 CCL은 네트워크를 통해 프로세서나 보조 프로세서에서 또는 프로세서나 보조 프로세서로 통신 전송을 관리한다.
    대칭 MPI 모델


    0

    댓글 추가

  10. 인텔 제온 파이 보조 프로세서 기반의 플랫폼은 다양한 프로그래밍 모델을 제공하여 개발자나 프로그래머에게  유연성을 제공한다. 프로그래머는 솔루션과 프로그램의 특성에 따라 최적의 성능을 낼 수 있는 모델을 선택 이용할 수 있다. 아래 그림은 제온 파이 기반 플랫폼에서 가능한 다양한 프로그램 모델들을 보여준다.
    인텔 제온 보조 프로세서 프로그래밍 모델
    그림에서 왼쪽에 가까운 모델들은 좀 더 CPU 중심의 모델들이고 오른쪽에 가까운 모델들은 인텔 제온 파이가 더 많은 역할을 할 수 있는 모델들이다. 각 모델에 따라 어느 위치에서 어떤 코드들이 실행이 되는지 살펴보자.
    • 멀티 코어 호스팅 모델: 멀티 코어, 즉 CPU에서 범용의 직렬과 병렬 컴퓨팅 코드들이 실행되는 모델이다. 현재 대다수의 프로그램들이 이 경우에 속한다. Main() 함수와 작업 함수인 Foo() 함수가 멀티 코어 CPU에서만 수행 된다. 프로그램의 병렬화와 벡터화가 여려울 때 CPU에서 돌리는 것이 성능측면에서 좋다.
    • 오프로드 모델: 보통 많은 코드가 직렬인 부분과 병렬적인 코드가 섞여 동작하는 프로그램이 많다. 이때 직렬적인 코드들은 직렬 코드 처리의 효율성이 더 좋은 CPU에서 작업 하고 병렬성이 높은 코드를 보조 프로세서에게 작업을 일임하는 모델이다. 특정 작업을 그 일을 잘하는 전문가에게 맡기는 경우라고 할 수 있다. 그림에서 Foo()함수가 병렬성이 아주 좋은 코드이고 계산의 많은 부분을 차지하는 코드라면 이 부분을 좀 더 효율성이 높은 보조 프로세서에서 처리하게 하는 것이 전반적인 성능을 높일 수 있다. MPI 함수를 통해서 다른 시스템 간의 통신을 한다.
    • 균등 분산 모델: 모든 코드가 CPU와 보조 프로세서에서 독립적으로 함께 동작하는 모델이다. 제온 파이도 하나의 별도 독립된 시스템으로 간주하고 각각 CPU와 보조 프로세서에서 실행을 함으로써 프로그램의 최대 성능을 얻는 모델이다. 병렬화와 벡터화가 잘 돼있는 코드가 최적의 성능을 얻을 수 있다. 이 모델은 기존의 CPU 기반의 클러스터를 위한 MPI 애플리케이션 사용자에게 가장 끌리는 모델이다. 왜냐하면 데이터 오프로드를 위해 코드의 재 구성없이 보조 프로세서를 사용할 수 있고 또한 HPC 애플리케이션의 성능 향상을 얻을 수 있기 때문이다.
    • 매니코어 호스팅: 일반적으로 네이티브 모드라고 하는 모델이다. 모든 코드가 제온 파이 보조 프로세서에서 동작을 한다. 역시 모든 코드들이 병렬화와 벡터화가 잘 되어 있어야만 좋은 성능을 얻을 수 있다. 컴파일 옵션으로 코드를 쉽게 만들 수 있다. CPU만에서 동작하는 코드를 포팅할 때 코드의 특성을 보기 위해 한 번 쉽게 해볼 수 있는 모델이다.
    0

    댓글 추가

구독하기
구독하기
로드 중